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 * 094 * @param basedir the directory to start with 095 * @throws IOException if the repository cannot be opened 096 */ 097 public static Git openRepo(File basedir) throws IOException { 098 return new Git(new RepositoryBuilder() 099 .readEnvironment() 100 .findGitDir(basedir) 101 .setMustExist(true) 102 .build()); 103 } 104 105 /** 106 * Closes the repository wrapped by the passed git object. 107 * 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 credential information is configured in the repository 135 */ 136 public static CredentialsProvider prepareSession(Git git, GitScmProviderRepository repository) { 137 StoredConfig config = git.getRepository().getConfig(); 138 config.setString("remote", "origin", "url", repository.getFetchUrl()); 139 config.setString("remote", "origin", "pushURL", repository.getPushUrl()); 140 141 // make sure we do not log any passwords to the output 142 LOGGER.info("fetch url: " + repository.getFetchUrlWithMaskedPassword()); 143 LOGGER.info("push url: " + repository.getPushUrlWithMaskedPassword()); 144 return getCredentials(repository); 145 } 146 147 /** 148 * Creates a credentials provider from the information passed in the 149 * repository. Current implementation supports: <br> 150 * <ul><li>UserName/Password</li></ul> 151 * <p> 152 * 153 * @param repository the config to get the details from 154 * @return <code>null</code> if there is not enough info to create a 155 * provider with 156 */ 157 public static CredentialsProvider getCredentials(GitScmProviderRepository repository) { 158 if (StringUtils.isNotBlank(repository.getUser()) && StringUtils.isNotBlank(repository.getPassword())) { 159 return new UsernamePasswordCredentialsProvider( 160 repository.getUser().trim(), repository.getPassword().trim()); 161 } 162 163 return null; 164 } 165 166 public static Iterable<PushResult> push( 167 Git git, 168 GitScmProviderRepository repo, 169 RefSpec refSpec, 170 Set<RemoteRefUpdate.Status> successfulStatuses, 171 Optional<TransportConfigCallback> transportConfigCallback) 172 throws PushException { 173 CredentialsProvider credentials = prepareSession(git, repo); 174 PushCommand command = git.push().setRefSpecs(refSpec).setCredentialsProvider(credentials); 175 transportConfigCallback.ifPresent(command::setTransportConfigCallback); 176 177 Iterable<PushResult> pushResultList; 178 try { 179 pushResultList = command.call(); 180 } catch (GitAPIException e) { 181 throw new PushException(repo.getPushUrlWithMaskedPassword(), e); 182 } 183 for (PushResult pushResult : pushResultList) { 184 Collection<RemoteRefUpdate> ru = pushResult.getRemoteUpdates(); 185 for (RemoteRefUpdate remoteRefUpdate : ru) { 186 if (!successfulStatuses.contains(remoteRefUpdate.getStatus())) { 187 throw new PushException(repo.getPushUrlWithMaskedPassword(), remoteRefUpdate); 188 } 189 LOGGER.debug("Push succeeded {}", remoteRefUpdate); 190 } 191 } 192 return pushResultList; 193 } 194 195 /** 196 * Does the Repository have any commits? 197 * 198 * @param repo 199 * @return false if there are no commits 200 */ 201 public static boolean hasCommits(Repository repo) { 202 if (repo != null && repo.getDirectory().exists()) { 203 return (new File(repo.getDirectory(), "objects").list().length > 2) 204 || (new File(repo.getDirectory(), "objects/pack").list().length > 0); 205 } 206 return false; 207 } 208 209 /** 210 * Get a list of all files in the given commit. 211 * 212 * @param repository the repo 213 * @param commit the commit to get the files from 214 * @return a list of files included in the commit 215 * @throws CorruptObjectException 216 * @throws IncorrectObjectTypeException 217 * @throws IOException 218 * @throws MissingObjectException 219 */ 220 public static List<ScmFile> getFilesInCommit(Repository repository, RevCommit commit) 221 throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { 222 return getFilesInCommit(repository, commit, null); 223 } 224 225 /** 226 * Get a list of all files in the given commit. 227 * 228 * @param repository the repo 229 * @param commit the commit to get the files from 230 * @param baseDir the directory to which the returned files should be relative. 231 * May be {@code null} in case they should be relative to the working directory root. 232 * @return a list of files included in the commit 233 * @throws CorruptObjectException 234 * @throws IncorrectObjectTypeException 235 * @throws IOException 236 * @throws MissingObjectException 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 * 341 * @param workingCopyDirectory the working copy root directory 342 * @param fileSet the file set to convert 343 */ 344 public static List<File> getWorkingCopyRelativePaths(File workingCopyDirectory, ScmFileSet fileSet) { 345 List<File> repositoryRelativePaths = new ArrayList<>(); 346 for (File path : fileSet.getFileList()) { 347 if (!path.isAbsolute()) { 348 path = new File(fileSet.getBasedir().getPath(), path.getPath()); 349 } 350 File repositoryRelativePath = relativize(workingCopyDirectory, path); 351 repositoryRelativePaths.add(repositoryRelativePath); 352 } 353 return repositoryRelativePaths; 354 } 355 356 /** 357 * Converts the given file to a string only containing forward slashes. 358 * 359 * @param file 360 * @return the normalized file path 361 */ 362 public static String toNormalizedFilePath(File file) { 363 return FilenameUtils.normalizeFilename(file); 364 } 365 366 private static List<ScmFile> getScmFilesForAllFileSetFilesContainedInRepoPath( 367 File workingCopyDirectory, ScmFileSet fileSet, Set<String> repoFilePaths, ScmFileStatus fileStatus) { 368 List<ScmFile> files = new ArrayList<>(repoFilePaths.size()); 369 getWorkingCopyRelativePaths(workingCopyDirectory, fileSet).stream().forEach((relativeFile) -> { 370 // check if repo relative path is contained 371 if (repoFilePaths.contains(toNormalizedFilePath(relativeFile))) { 372 // returned ScmFiles should be relative to given fileset's basedir 373 ScmFile scmfile = new ScmFile( 374 relativize(fileSet.getBasedir(), new File(workingCopyDirectory, relativeFile.getPath())) 375 .getPath(), 376 fileStatus); 377 files.add(scmfile); 378 } 379 }); 380 return files; 381 } 382 383 private static File relativize(File baseDir, File file) { 384 if (file.isAbsolute()) { 385 return baseDir.toPath().relativize(file.toPath()).toFile(); 386 } 387 return file; 388 } 389 390 /** 391 * Get a list of commits between two revisions. 392 * 393 * @param repo the repository to work on 394 * @param sortings sorting 395 * @param fromRev start revision 396 * @param toRev if null, falls back to head 397 * @param fromDate from which date on 398 * @param toDate until which date 399 * @param maxLines max number of lines 400 * @return a list of commits, might be empty, but never <code>null</code> 401 * @throws IncorrectObjectTypeException 402 * @throws IOException 403 * @throws MissingObjectException 404 */ 405 public static List<RevCommit> getRevCommits( 406 Repository repo, 407 RevSort[] sortings, 408 String fromRev, 409 String toRev, 410 final Date fromDate, 411 final Date toDate, 412 int maxLines) 413 throws IOException, MissingObjectException, IncorrectObjectTypeException { 414 415 List<RevCommit> revs = new ArrayList<>(); 416 417 ObjectId fromRevId = fromRev != null ? repo.resolve(fromRev) : null; 418 ObjectId toRevId = toRev != null ? repo.resolve(toRev) : null; 419 420 if (sortings == null || sortings.length == 0) { 421 sortings = new RevSort[] {RevSort.TOPO, RevSort.COMMIT_TIME_DESC}; 422 } 423 424 try (RevWalk walk = new RevWalk(repo)) { 425 for (final RevSort s : sortings) { 426 walk.sort(s, true); 427 } 428 429 if (fromDate != null && toDate != null) { 430 // walk.setRevFilter( CommitTimeRevFilter.between( fromDate, toDate ) ); 431 walk.setRevFilter(new RevFilter() { 432 @Override 433 public boolean include(RevWalk walker, RevCommit cmit) 434 throws StopWalkException, MissingObjectException, IncorrectObjectTypeException, 435 IOException { 436 int cmtTime = cmit.getCommitTime(); 437 438 return (cmtTime >= (fromDate.getTime() / 1000)) && (cmtTime <= (toDate.getTime() / 1000)); 439 } 440 441 @Override 442 public RevFilter clone() { 443 return this; 444 } 445 }); 446 } else { 447 if (fromDate != null) { 448 walk.setRevFilter(CommitTimeRevFilter.after(fromDate)); 449 } 450 if (toDate != null) { 451 walk.setRevFilter(CommitTimeRevFilter.before(toDate)); 452 } 453 } 454 455 if (fromRevId != null) { 456 RevCommit c = walk.parseCommit(fromRevId); 457 c.add(RevFlag.UNINTERESTING); 458 RevCommit real = walk.parseCommit(c); 459 walk.markUninteresting(real); 460 } 461 462 if (toRevId != null) { 463 RevCommit c = walk.parseCommit(toRevId); 464 c.remove(RevFlag.UNINTERESTING); 465 RevCommit real = walk.parseCommit(c); 466 walk.markStart(real); 467 } else { 468 final ObjectId head = repo.resolve(Constants.HEAD); 469 if (head == null) { 470 throw new IOException("Cannot resolve " + Constants.HEAD); 471 } 472 RevCommit real = walk.parseCommit(head); 473 walk.markStart(real); 474 } 475 476 int n = 0; 477 for (final RevCommit c : walk) { 478 n++; 479 if (maxLines != -1 && n > maxLines) { 480 break; 481 } 482 483 revs.add(c); 484 } 485 return revs; 486 } 487 } 488 489 /** 490 * Get a list of tags that has been set in the specified commit. 491 * 492 * @param repo the repository to work on 493 * @param commit the commit for which we want the tags 494 * @return a list of tags, might be empty, and never <code>null</code> 495 */ 496 public static List<String> getTags(Repository repo, RevCommit commit) throws IOException { 497 List<Ref> refList = repo.getRefDatabase().getRefsByPrefix(R_TAGS); 498 499 try (RevWalk revWalk = new RevWalk(repo)) { 500 ObjectId commitId = commit.getId(); 501 List<String> result = new ArrayList<>(); 502 503 for (Ref ref : refList) { 504 ObjectId tagId = ref.getObjectId(); 505 RevCommit tagCommit = revWalk.parseCommit(tagId); 506 if (commitId.equals(tagCommit.getId())) { 507 result.add(ref.getName().substring(R_TAGS.length())); 508 } 509 } 510 return result; 511 } 512 } 513}