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 * <li>push url</li> <li>fetch url</li> 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 * <li>UserName/Password</li> 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 RevWalk rw = new RevWalk( repository ); 240 RevCommit realParant = commit.getParentCount() > 0 ? commit.getParent( 0 ) : commit; 241 RevCommit parent = rw.parseCommit( realParant.getId() ); 242 DiffFormatter df = new DiffFormatter( DisabledOutputStream.INSTANCE ); 243 df.setRepository( repository ); 244 df.setDiffComparator( RawTextComparator.DEFAULT ); 245 df.setDetectRenames( true ); 246 List<DiffEntry> diffs = df.scan( parent.getTree(), commit.getTree() ); 247 for ( DiffEntry diff : diffs ) 248 { 249 list.add( new ScmFile( diff.getNewPath(), ScmFileStatus.CHECKED_IN ) ); 250 } 251 rw.close(); 252 } 253 return list; 254 } 255 256 /** 257 * Translate a {@code FileStatus} in the matching {@code ScmFileStatus}. 258 * 259 * @param changeType 260 * @return the matching ScmFileStatus 261 */ 262 public static ScmFileStatus getScmFileStatus( ChangeType changeType ) 263 { 264 switch ( changeType ) 265 { 266 case ADD: 267 return ScmFileStatus.ADDED; 268 case MODIFY: 269 return ScmFileStatus.MODIFIED; 270 case DELETE: 271 return ScmFileStatus.DELETED; 272 case RENAME: 273 return ScmFileStatus.RENAMED; 274 case COPY: 275 return ScmFileStatus.COPIED; 276 default: 277 return ScmFileStatus.UNKNOWN; 278 } 279 } 280 281 /** 282 * Adds all files in the given fileSet to the repository. 283 * 284 * @param git the repo to add the files to 285 * @param fileSet the set of files within the workspace, the files are added 286 * relative to the basedir of this fileset 287 * @return a list of added files 288 * @throws GitAPIException 289 * @throws NoFilepatternException 290 */ 291 public static List<ScmFile> addAllFiles( Git git, ScmFileSet fileSet ) 292 throws GitAPIException, NoFilepatternException 293 { 294 URI baseUri = fileSet.getBasedir().toURI(); 295 AddCommand add = git.add(); 296 for ( File file : fileSet.getFileList() ) 297 { 298 if ( !file.isAbsolute() ) 299 { 300 file = new File( fileSet.getBasedir().getPath(), file.getPath() ); 301 } 302 303 if ( file.exists() ) 304 { 305 String path = relativize( baseUri, file ); 306 add.addFilepattern( path ); 307 add.addFilepattern( file.getAbsolutePath() ); 308 } 309 } 310 add.call(); 311 312 Status status = git.status().call(); 313 314 Set<String> allInIndex = new HashSet<String>(); 315 allInIndex.addAll( status.getAdded() ); 316 allInIndex.addAll( status.getChanged() ); 317 318 // System.out.println("All in index: "+allInIndex.size()); 319 320 List<ScmFile> addedFiles = new ArrayList<ScmFile>( allInIndex.size() ); 321 322 // rewrite all detected files to now have status 'checked_in' 323 for ( String entry : allInIndex ) 324 { 325 ScmFile scmfile = new ScmFile( entry, ScmFileStatus.ADDED ); 326 327 // if a specific fileSet is given, we have to check if the file is 328 // really tracked 329 for ( Iterator<File> itfl = fileSet.getFileList().iterator(); itfl.hasNext(); ) 330 { 331 String path = FilenameUtils.normalizeFilename( relativize( baseUri, itfl.next() ) ); 332 if ( path.equals( FilenameUtils.normalizeFilename( scmfile.getPath() ) ) ) 333 { 334 addedFiles.add( scmfile ); 335 } 336 } 337 } 338 return addedFiles; 339 } 340 341 private static String relativize( URI baseUri, File f ) 342 { 343 String path = f.getPath(); 344 if ( f.isAbsolute() ) 345 { 346 path = baseUri.relativize( new File( path ).toURI() ).getPath(); 347 } 348 return path; 349 } 350 351 /** 352 * Get a list of commits between two revisions. 353 * 354 * @param repo the repository to work on 355 * @param sortings sorting 356 * @param fromRev start revision 357 * @param toRev if null, falls back to head 358 * @param fromDate from which date on 359 * @param toDate until which date 360 * @param maxLines max number of lines 361 * @return a list of commits, might be empty, but never <code>null</code> 362 * @throws IOException 363 * @throws MissingObjectException 364 * @throws IncorrectObjectTypeException 365 */ 366 public static List<RevCommit> getRevCommits( Repository repo, RevSort[] sortings, String fromRev, String toRev, 367 final Date fromDate, final Date toDate, int maxLines ) 368 throws IOException, MissingObjectException, IncorrectObjectTypeException 369 { 370 371 List<RevCommit> revs = new ArrayList<RevCommit>(); 372 RevWalk walk = new RevWalk( repo ); 373 374 ObjectId fromRevId = fromRev != null ? repo.resolve( fromRev ) : null; 375 ObjectId toRevId = toRev != null ? repo.resolve( toRev ) : null; 376 377 if ( sortings == null || sortings.length == 0 ) 378 { 379 sortings = new RevSort[]{ RevSort.TOPO, RevSort.COMMIT_TIME_DESC }; 380 } 381 382 for ( final RevSort s : sortings ) 383 { 384 walk.sort( s, true ); 385 } 386 387 if ( fromDate != null && toDate != null ) 388 { 389 //walk.setRevFilter( CommitTimeRevFilter.between( fromDate, toDate ) ); 390 walk.setRevFilter( new RevFilter() 391 { 392 @Override 393 public boolean include( RevWalk walker, RevCommit cmit ) 394 throws StopWalkException, MissingObjectException, IncorrectObjectTypeException, IOException 395 { 396 int cmtTime = cmit.getCommitTime(); 397 398 return ( cmtTime >= ( fromDate.getTime() / 1000 ) ) && ( cmtTime <= ( toDate.getTime() / 1000 ) ); 399 } 400 401 @Override 402 public RevFilter clone() 403 { 404 return this; 405 } 406 } ); 407 } 408 else 409 { 410 if ( fromDate != null ) 411 { 412 walk.setRevFilter( CommitTimeRevFilter.after( fromDate ) ); 413 } 414 if ( toDate != null ) 415 { 416 walk.setRevFilter( CommitTimeRevFilter.before( toDate ) ); 417 } 418 } 419 420 if ( fromRevId != null ) 421 { 422 RevCommit c = walk.parseCommit( fromRevId ); 423 c.add( RevFlag.UNINTERESTING ); 424 RevCommit real = walk.parseCommit( c ); 425 walk.markUninteresting( real ); 426 } 427 428 if ( toRevId != null ) 429 { 430 RevCommit c = walk.parseCommit( toRevId ); 431 c.remove( RevFlag.UNINTERESTING ); 432 RevCommit real = walk.parseCommit( c ); 433 walk.markStart( real ); 434 } 435 else 436 { 437 final ObjectId head = repo.resolve( Constants.HEAD ); 438 if ( head == null ) 439 { 440 throw new RuntimeException( "Cannot resolve " + Constants.HEAD ); 441 } 442 RevCommit real = walk.parseCommit( head ); 443 walk.markStart( real ); 444 } 445 446 int n = 0; 447 for ( final RevCommit c : walk ) 448 { 449 n++; 450 if ( maxLines != -1 && n > maxLines ) 451 { 452 break; 453 } 454 455 revs.add( c ); 456 } 457 return revs; 458 } 459 460}