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