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