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