001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.scm.provider.git.jgit.command; 020 021import java.io.File; 022import java.io.IOException; 023import java.io.UnsupportedEncodingException; 024import java.net.URLEncoder; 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Date; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Set; 031 032import org.apache.commons.lang3.StringUtils; 033import org.apache.maven.scm.ScmFile; 034import org.apache.maven.scm.ScmFileSet; 035import org.apache.maven.scm.ScmFileStatus; 036import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository; 037import org.apache.maven.scm.util.FilenameUtils; 038import org.eclipse.jgit.api.AddCommand; 039import org.eclipse.jgit.api.Git; 040import org.eclipse.jgit.api.PushCommand; 041import org.eclipse.jgit.api.RmCommand; 042import org.eclipse.jgit.api.Status; 043import org.eclipse.jgit.api.errors.GitAPIException; 044import org.eclipse.jgit.api.errors.InvalidRemoteException; 045import org.eclipse.jgit.api.errors.TransportException; 046import org.eclipse.jgit.diff.DiffEntry; 047import org.eclipse.jgit.diff.DiffEntry.ChangeType; 048import org.eclipse.jgit.diff.DiffFormatter; 049import org.eclipse.jgit.diff.RawTextComparator; 050import org.eclipse.jgit.errors.CorruptObjectException; 051import org.eclipse.jgit.errors.IncorrectObjectTypeException; 052import org.eclipse.jgit.errors.MissingObjectException; 053import org.eclipse.jgit.errors.StopWalkException; 054import org.eclipse.jgit.lib.Constants; 055import org.eclipse.jgit.lib.ObjectId; 056import org.eclipse.jgit.lib.ProgressMonitor; 057import org.eclipse.jgit.lib.Ref; 058import org.eclipse.jgit.lib.Repository; 059import org.eclipse.jgit.lib.RepositoryBuilder; 060import org.eclipse.jgit.lib.StoredConfig; 061import org.eclipse.jgit.lib.TextProgressMonitor; 062import org.eclipse.jgit.revwalk.RevCommit; 063import org.eclipse.jgit.revwalk.RevFlag; 064import org.eclipse.jgit.revwalk.RevSort; 065import org.eclipse.jgit.revwalk.RevWalk; 066import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter; 067import org.eclipse.jgit.revwalk.filter.RevFilter; 068import org.eclipse.jgit.transport.CredentialsProvider; 069import org.eclipse.jgit.transport.PushResult; 070import org.eclipse.jgit.transport.RefSpec; 071import org.eclipse.jgit.transport.RemoteRefUpdate; 072import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; 073import org.eclipse.jgit.util.io.DisabledOutputStream; 074import org.slf4j.Logger; 075import org.slf4j.LoggerFactory; 076 077import static org.eclipse.jgit.lib.Constants.R_TAGS; 078 079/** 080 * JGit utility functions. 081 * 082 * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a> 083 * @author Dominik Bartholdi (imod) 084 * @since 1.9 085 */ 086public class JGitUtils { 087 private static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class); 088 089 private JGitUtils() { 090 // no op 091 } 092 093 /** 094 * Opens a JGit repository in the current directory or a parent directory. 095 * @param basedir The directory to start with 096 * @throws IOException If the repository cannot be opened 097 */ 098 public static Git openRepo(File basedir) throws IOException { 099 return new Git(new RepositoryBuilder() 100 .readEnvironment() 101 .findGitDir(basedir) 102 .setMustExist(true) 103 .build()); 104 } 105 106 /** 107 * Closes the repository wrapped by the passed git object 108 * @param git 109 */ 110 public static void closeRepo(Git git) { 111 if (git != null && git.getRepository() != null) { 112 git.getRepository().close(); 113 } 114 } 115 116 /** 117 * Construct a logging ProgressMonitor for all JGit operations. 118 * 119 * @return a ProgressMonitor for use 120 */ 121 public static ProgressMonitor getMonitor() { 122 // X TODO write an own ProgressMonitor which logs to ScmLogger! 123 return new TextProgressMonitor(); 124 } 125 126 /** 127 * Prepares the in memory configuration of git to connect to the configured 128 * repository. It configures the following settings in memory: <br> 129 * <ul><li>push url</li> <li>fetch url</li></ul> 130 * <p> 131 * 132 * @param git the instance to configure (only in memory, not saved) 133 * @param repository the repo config to be used 134 * @return {@link CredentialsProvider} in case there are credentials 135 * informations configured in the repository. 136 */ 137 public static CredentialsProvider prepareSession(Git git, GitScmProviderRepository repository) { 138 StoredConfig config = git.getRepository().getConfig(); 139 config.setString("remote", "origin", "url", repository.getFetchUrl()); 140 config.setString("remote", "origin", "pushURL", repository.getPushUrl()); 141 142 // make sure we do not log any passwords to the output 143 String password = StringUtils.isNotBlank(repository.getPassword()) 144 ? repository.getPassword().trim() 145 : "no-pwd-defined"; 146 // if password contains special characters it won't match below. 147 // Try encoding before match. (Passwords without will be unaffected) 148 try { 149 password = URLEncoder.encode(password, "UTF-8"); 150 } catch (UnsupportedEncodingException e) { 151 // UTF-8 should be valid 152 // TODO use a logger 153 System.out.println("Ignore UnsupportedEncodingException when trying to encode password"); 154 } 155 LOGGER.info("fetch url: " + repository.getFetchUrl().replace(password, "******")); 156 LOGGER.info("push url: " + repository.getPushUrl().replace(password, "******")); 157 return getCredentials(repository); 158 } 159 160 /** 161 * Creates a credentials provider from the information passed in the 162 * repository. Current implementation supports: <br> 163 * <ul><li>UserName/Password</li></ul> 164 * <p> 165 * 166 * @param repository the config to get the details from 167 * @return <code>null</code> if there is not enough info to create a 168 * provider with 169 */ 170 public static CredentialsProvider getCredentials(GitScmProviderRepository repository) { 171 if (StringUtils.isNotBlank(repository.getUser()) && StringUtils.isNotBlank(repository.getPassword())) { 172 return new UsernamePasswordCredentialsProvider( 173 repository.getUser().trim(), repository.getPassword().trim()); 174 } 175 176 return null; 177 } 178 179 public static Iterable<PushResult> push(Git git, GitScmProviderRepository repo, RefSpec refSpec) 180 throws GitAPIException, InvalidRemoteException, TransportException { 181 CredentialsProvider credentials = prepareSession(git, repo); 182 PushCommand command = git.push() 183 .setRefSpecs(refSpec) 184 .setCredentialsProvider(credentials) 185 .setTransportConfigCallback( 186 new JGitTransportConfigCallback(new ScmProviderAwareSshdSessionFactory(repo, LOGGER))); 187 188 Iterable<PushResult> pushResultList = command.call(); 189 for (PushResult pushResult : pushResultList) { 190 Collection<RemoteRefUpdate> ru = pushResult.getRemoteUpdates(); 191 for (RemoteRefUpdate remoteRefUpdate : ru) { 192 LOGGER.info(remoteRefUpdate.getStatus() + " - " + remoteRefUpdate); 193 } 194 } 195 return pushResultList; 196 } 197 198 /** 199 * Does the Repository have any commits? 200 * 201 * @param repo 202 * @return false if there are no commits 203 */ 204 public static boolean hasCommits(Repository repo) { 205 if (repo != null && repo.getDirectory().exists()) { 206 return (new File(repo.getDirectory(), "objects").list().length > 2) 207 || (new File(repo.getDirectory(), "objects/pack").list().length > 0); 208 } 209 return false; 210 } 211 212 /** 213 * get a list of all files in the given commit 214 * 215 * @param repository the repo 216 * @param commit the commit to get the files from 217 * @return a list of files included in the commit 218 * @throws MissingObjectException 219 * @throws IncorrectObjectTypeException 220 * @throws CorruptObjectException 221 * @throws IOException 222 */ 223 public static List<ScmFile> getFilesInCommit(Repository repository, RevCommit commit) 224 throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { 225 return getFilesInCommit(repository, commit, null); 226 } 227 228 /** 229 * get a list of all files in the given commit 230 * 231 * @param repository the repo 232 * @param commit the commit to get the files from 233 * @param baseDir the directory to which the returned files should be relative. 234 * May be {@code null} in case they should be relative to the working directory root. 235 * @return a list of files included in the commit 236 * 237 * @throws MissingObjectException 238 * @throws IncorrectObjectTypeException 239 * @throws CorruptObjectException 240 * @throws IOException 241 */ 242 public static List<ScmFile> getFilesInCommit(Repository repository, RevCommit commit, File baseDir) 243 throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { 244 List<ScmFile> list = new ArrayList<>(); 245 if (JGitUtils.hasCommits(repository)) { 246 247 try (RevWalk rw = new RevWalk(repository); 248 DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) { 249 RevCommit realParent = commit.getParentCount() > 0 ? commit.getParent(0) : commit; 250 RevCommit parent = rw.parseCommit(realParent.getId()); 251 df.setRepository(repository); 252 df.setDiffComparator(RawTextComparator.DEFAULT); 253 df.setDetectRenames(true); 254 List<DiffEntry> diffs = df.scan(parent.getTree(), commit.getTree()); 255 for (DiffEntry diff : diffs) { 256 final String path; 257 if (baseDir != null) { 258 path = relativize(baseDir, new File(repository.getWorkTree(), diff.getNewPath())) 259 .getPath(); 260 } else { 261 path = diff.getNewPath(); 262 } 263 list.add(new ScmFile(path, ScmFileStatus.CHECKED_IN)); 264 } 265 } 266 } 267 return list; 268 } 269 270 /** 271 * Translate a {@code FileStatus} in the matching {@code ScmFileStatus}. 272 * 273 * @param changeType 274 * @return the matching ScmFileStatus 275 */ 276 public static ScmFileStatus getScmFileStatus(ChangeType changeType) { 277 switch (changeType) { 278 case ADD: 279 return ScmFileStatus.ADDED; 280 case MODIFY: 281 return ScmFileStatus.MODIFIED; 282 case DELETE: 283 return ScmFileStatus.DELETED; 284 case RENAME: 285 return ScmFileStatus.RENAMED; 286 case COPY: 287 return ScmFileStatus.COPIED; 288 default: 289 return ScmFileStatus.UNKNOWN; 290 } 291 } 292 293 /** 294 * Adds all files in the given fileSet to the repository. 295 * 296 * @param git the repo to add the files to 297 * @param fileSet the set of files within the workspace, the files are added 298 * relative to the basedir of this fileset 299 * @return a list of added files 300 * @throws GitAPIException 301 */ 302 public static List<ScmFile> addAllFiles(Git git, ScmFileSet fileSet) throws GitAPIException { 303 File workingCopyRootDirectory = git.getRepository().getWorkTree(); 304 AddCommand add = git.add(); 305 getWorkingCopyRelativePaths(workingCopyRootDirectory, fileSet).stream() 306 .forEach(f -> add.addFilepattern(toNormalizedFilePath(f))); 307 add.call(); 308 309 Status status = git.status().call(); 310 311 Set<String> allInIndex = new HashSet<>(); 312 allInIndex.addAll(status.getAdded()); 313 allInIndex.addAll(status.getChanged()); 314 return getScmFilesForAllFileSetFilesContainedInRepoPath( 315 workingCopyRootDirectory, fileSet, allInIndex, ScmFileStatus.ADDED); 316 } 317 318 /** 319 * Remove all files in the given fileSet from the repository. 320 * 321 * @param git the repo to remove the files from 322 * @param fileSet the set of files within the workspace, the files are removed 323 * relative to the basedir of this fileset 324 * @return a list of removed files 325 * @throws GitAPIException 326 */ 327 public static List<ScmFile> removeAllFiles(Git git, ScmFileSet fileSet) throws GitAPIException { 328 File workingCopyRootDirectory = git.getRepository().getWorkTree(); 329 RmCommand remove = git.rm(); 330 getWorkingCopyRelativePaths(workingCopyRootDirectory, fileSet).stream() 331 .forEach(f -> remove.addFilepattern(toNormalizedFilePath(f))); 332 remove.call(); 333 334 Status status = git.status().call(); 335 336 Set<String> allInIndex = new HashSet<>(status.getRemoved()); 337 return getScmFilesForAllFileSetFilesContainedInRepoPath( 338 workingCopyRootDirectory, fileSet, allInIndex, ScmFileStatus.DELETED); 339 } 340 341 /** 342 * Convert each file in the {@code fileSet} to their relative file path to workingCopyDirectory 343 * and return them in a list. 344 * @param workingCopyDirectory the working copy root directory 345 * @param fileSet the file set to convert 346 */ 347 public static List<File> getWorkingCopyRelativePaths(File workingCopyDirectory, ScmFileSet fileSet) { 348 List<File> repositoryRelativePaths = new ArrayList<>(); 349 for (File path : fileSet.getFileList()) { 350 if (!path.isAbsolute()) { 351 path = new File(fileSet.getBasedir().getPath(), path.getPath()); 352 } 353 File repositoryRelativePath = relativize(workingCopyDirectory, path); 354 repositoryRelativePaths.add(repositoryRelativePath); 355 } 356 return repositoryRelativePaths; 357 } 358 359 /** 360 * Converts the given file to a string only containing forward slashes 361 * @param file 362 * @return the normalized file path 363 */ 364 public static String toNormalizedFilePath(File file) { 365 return FilenameUtils.normalizeFilename(file); 366 } 367 368 private static List<ScmFile> getScmFilesForAllFileSetFilesContainedInRepoPath( 369 File workingCopyDirectory, ScmFileSet fileSet, Set<String> repoFilePaths, ScmFileStatus fileStatus) { 370 List<ScmFile> files = new ArrayList<>(repoFilePaths.size()); 371 getWorkingCopyRelativePaths(workingCopyDirectory, fileSet).stream().forEach((relativeFile) -> { 372 // check if repo relative path is contained 373 if (repoFilePaths.contains(toNormalizedFilePath(relativeFile))) { 374 // returned ScmFiles should be relative to given fileset's basedir 375 ScmFile scmfile = new ScmFile( 376 relativize(fileSet.getBasedir(), new File(workingCopyDirectory, relativeFile.getPath())) 377 .getPath(), 378 fileStatus); 379 files.add(scmfile); 380 } 381 }); 382 return files; 383 } 384 385 private static File relativize(File baseDir, File file) { 386 if (file.isAbsolute()) { 387 return baseDir.toPath().relativize(file.toPath()).toFile(); 388 } 389 return file; 390 } 391 392 /** 393 * Get a list of commits between two revisions. 394 * 395 * @param repo the repository to work on 396 * @param sortings sorting 397 * @param fromRev start revision 398 * @param toRev if null, falls back to head 399 * @param fromDate from which date on 400 * @param toDate until which date 401 * @param maxLines max number of lines 402 * @return a list of commits, might be empty, but never <code>null</code> 403 * @throws IOException 404 * @throws MissingObjectException 405 * @throws IncorrectObjectTypeException 406 */ 407 public static List<RevCommit> getRevCommits( 408 Repository repo, 409 RevSort[] sortings, 410 String fromRev, 411 String toRev, 412 final Date fromDate, 413 final Date toDate, 414 int maxLines) 415 throws IOException, MissingObjectException, IncorrectObjectTypeException { 416 417 List<RevCommit> revs = new ArrayList<>(); 418 419 ObjectId fromRevId = fromRev != null ? repo.resolve(fromRev) : null; 420 ObjectId toRevId = toRev != null ? repo.resolve(toRev) : null; 421 422 if (sortings == null || sortings.length == 0) { 423 sortings = new RevSort[] {RevSort.TOPO, RevSort.COMMIT_TIME_DESC}; 424 } 425 426 try (RevWalk walk = new RevWalk(repo)) { 427 for (final RevSort s : sortings) { 428 walk.sort(s, true); 429 } 430 431 if (fromDate != null && toDate != null) { 432 // walk.setRevFilter( CommitTimeRevFilter.between( fromDate, toDate ) ); 433 walk.setRevFilter(new RevFilter() { 434 @Override 435 public boolean include(RevWalk walker, RevCommit cmit) 436 throws StopWalkException, MissingObjectException, IncorrectObjectTypeException, 437 IOException { 438 int cmtTime = cmit.getCommitTime(); 439 440 return (cmtTime >= (fromDate.getTime() / 1000)) && (cmtTime <= (toDate.getTime() / 1000)); 441 } 442 443 @Override 444 public RevFilter clone() { 445 return this; 446 } 447 }); 448 } else { 449 if (fromDate != null) { 450 walk.setRevFilter(CommitTimeRevFilter.after(fromDate)); 451 } 452 if (toDate != null) { 453 walk.setRevFilter(CommitTimeRevFilter.before(toDate)); 454 } 455 } 456 457 if (fromRevId != null) { 458 RevCommit c = walk.parseCommit(fromRevId); 459 c.add(RevFlag.UNINTERESTING); 460 RevCommit real = walk.parseCommit(c); 461 walk.markUninteresting(real); 462 } 463 464 if (toRevId != null) { 465 RevCommit c = walk.parseCommit(toRevId); 466 c.remove(RevFlag.UNINTERESTING); 467 RevCommit real = walk.parseCommit(c); 468 walk.markStart(real); 469 } else { 470 final ObjectId head = repo.resolve(Constants.HEAD); 471 if (head == null) { 472 throw new RuntimeException("Cannot resolve " + Constants.HEAD); 473 } 474 RevCommit real = walk.parseCommit(head); 475 walk.markStart(real); 476 } 477 478 int n = 0; 479 for (final RevCommit c : walk) { 480 n++; 481 if (maxLines != -1 && n > maxLines) { 482 break; 483 } 484 485 revs.add(c); 486 } 487 return revs; 488 } 489 } 490 491 /** 492 * Get a list of tags that has been set in the specified commit. 493 * 494 * @param repo the repository to work on 495 * @param commit the commit for which we want the tags 496 * @return a list of tags, might be empty, and never <code>null</code> 497 */ 498 public static List<String> getTags(Repository repo, RevCommit commit) throws IOException { 499 List<Ref> refList = repo.getRefDatabase().getRefsByPrefix(R_TAGS); 500 501 try (RevWalk revWalk = new RevWalk(repo)) { 502 ObjectId commitId = commit.getId(); 503 List<String> result = new ArrayList<>(); 504 505 for (Ref ref : refList) { 506 ObjectId tagId = ref.getObjectId(); 507 RevCommit tagCommit = revWalk.parseCommit(tagId); 508 if (commitId.equals(tagCommit.getId())) { 509 result.add(ref.getName().substring(R_TAGS.length())); 510 } 511 } 512 return result; 513 } 514 } 515}