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