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