@ -52,6 +52,7 @@ import java.util.ArrayList;
import java.util.Collection ;
import java.util.Collections ;
import java.util.HashMap ;
import java.util.Iterator ;
import java.util.LinkedList ;
import java.util.List ;
import java.util.Map ;
@ -77,6 +78,7 @@ import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.dircache.DirCache ;
import org.eclipse.jgit.dircache.DirCacheCheckout ;
import org.eclipse.jgit.dircache.DirCacheIterator ;
import org.eclipse.jgit.errors.RevisionSyntaxException ;
import org.eclipse.jgit.internal.JGitText ;
import org.eclipse.jgit.lib.AbbreviatedObjectId ;
import org.eclipse.jgit.lib.AnyObjectId ;
@ -96,6 +98,7 @@ import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.MergeStrategy ;
import org.eclipse.jgit.revwalk.RevCommit ;
import org.eclipse.jgit.revwalk.RevWalk ;
import org.eclipse.jgit.revwalk.filter.RevFilter ;
import org.eclipse.jgit.treewalk.TreeWalk ;
import org.eclipse.jgit.treewalk.filter.TreeFilter ;
import org.eclipse.jgit.util.FileUtils ;
@ -167,6 +170,20 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
private static final String AUTOSTASH_MSG = "On {0}: autostash" ; //$NON-NLS-1$
/ * *
* The folder containing the hashes of ( potentially ) rewritten commits when
* - - preserve - merges is used .
* /
private static final String REWRITTEN = "rewritten" ; //$NON-NLS-1$
/ * *
* File containing the current commit ( s ) to cherry pick when - - preserve - merges
* is used .
* /
private static final String CURRENT_COMMIT = "current-commit" ; //$NON-NLS-1$
private static final String REFLOG_PREFIX = "rebase:" ; //$NON-NLS-1$
/ * *
* The available operations
* /
@ -216,6 +233,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
private MergeStrategy strategy = MergeStrategy . RECURSIVE ;
private boolean preserveMerges = false ;
/ * *
* @param repo
* /
@ -266,6 +285,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
this . upstreamCommit = walk . parseCommit ( repo
. resolve ( upstreamCommitId ) ) ;
preserveMerges = rebaseState . getRewrittenDir ( ) . exists ( ) ;
break ;
case BEGIN :
autoStash ( ) ;
@ -412,6 +432,12 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
throws IOException , GitAPIException {
if ( Action . COMMENT . equals ( step . getAction ( ) ) )
return null ;
if ( preserveMerges
& & shouldPick
& & ( Action . EDIT . equals ( step . getAction ( ) ) | | Action . PICK
. equals ( step . getAction ( ) ) ) ) {
writeRewrittenHashes ( ) ;
}
ObjectReader or = repo . newObjectReader ( ) ;
Collection < ObjectId > ids = or . resolve ( step . getCommit ( ) ) ;
@ -468,19 +494,87 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
monitor . beginTask ( MessageFormat . format (
JGitText . get ( ) . applyingCommit ,
commitToPick . getShortMessage ( ) ) , ProgressMonitor . UNKNOWN ) ;
// if the first parent of commitToPick is the current HEAD,
// we do a fast-forward instead of cherry-pick to avoid
// unnecessary object rewriting
newHead = tryFastForward ( commitToPick ) ;
lastStepWasForward = newHead ! = null ;
if ( ! lastStepWasForward ) {
// TODO if the content of this commit is already merged
// here we should skip this step in order to avoid
// confusing pseudo-changed
if ( preserveMerges )
return cherryPickCommitPreservingMerges ( commitToPick ) ;
else
return cherryPickCommitFlattening ( commitToPick ) ;
} finally {
monitor . endTask ( ) ;
}
}
private RebaseResult cherryPickCommitFlattening ( RevCommit commitToPick )
throws IOException , GitAPIException , NoMessageException ,
UnmergedPathsException , ConcurrentRefUpdateException ,
WrongRepositoryStateException , NoHeadException {
// If the first parent of commitToPick is the current HEAD,
// we do a fast-forward instead of cherry-pick to avoid
// unnecessary object rewriting
newHead = tryFastForward ( commitToPick ) ;
lastStepWasForward = newHead ! = null ;
if ( ! lastStepWasForward ) {
// TODO if the content of this commit is already merged
// here we should skip this step in order to avoid
// confusing pseudo-changed
String ourCommitName = getOurCommitName ( ) ;
CherryPickResult cherryPickResult = new Git ( repo ) . cherryPick ( )
. include ( commitToPick ) . setOurCommitName ( ourCommitName )
. setReflogPrefix ( REFLOG_PREFIX ) . setStrategy ( strategy )
. call ( ) ;
switch ( cherryPickResult . getStatus ( ) ) {
case FAILED :
if ( operation = = Operation . BEGIN )
return abort ( RebaseResult . failed ( cherryPickResult
. getFailingPaths ( ) ) ) ;
else
return stop ( commitToPick , Status . STOPPED ) ;
case CONFLICTING :
return stop ( commitToPick , Status . STOPPED ) ;
case OK :
newHead = cherryPickResult . getNewHead ( ) ;
}
}
return null ;
}
private RebaseResult cherryPickCommitPreservingMerges ( RevCommit commitToPick )
throws IOException , GitAPIException , NoMessageException ,
UnmergedPathsException , ConcurrentRefUpdateException ,
WrongRepositoryStateException , NoHeadException {
writeCurrentCommit ( commitToPick ) ;
List < RevCommit > newParents = getNewParents ( commitToPick ) ;
boolean otherParentsUnchanged = true ;
for ( int i = 1 ; i < commitToPick . getParentCount ( ) ; i + + )
otherParentsUnchanged & = newParents . get ( i ) . equals (
commitToPick . getParent ( i ) ) ;
// If the first parent of commitToPick is the current HEAD,
// we do a fast-forward instead of cherry-pick to avoid
// unnecessary object rewriting
newHead = otherParentsUnchanged ? tryFastForward ( commitToPick ) : null ;
lastStepWasForward = newHead ! = null ;
if ( ! lastStepWasForward ) {
ObjectId headId = getHead ( ) . getObjectId ( ) ;
if ( ! AnyObjectId . equals ( headId , newParents . get ( 0 ) ) )
checkoutCommit ( headId . getName ( ) , newParents . get ( 0 ) ) ;
// Use the cherry-pick strategy if all non-first parents did not
// change. This is different from C Git, which always uses the merge
// strategy (see below).
if ( otherParentsUnchanged ) {
boolean isMerge = commitToPick . getParentCount ( ) > 1 ;
String ourCommitName = getOurCommitName ( ) ;
CherryPickResult cherryPickResult = new Git ( repo ) . cherryPick ( )
CherryPickCommand pickCommand = new Git ( repo ) . cherryPick ( )
. include ( commitToPick ) . setOurCommitName ( ourCommitName )
. setReflogPrefix ( "rebase:" ) . setStrategy ( strategy ) . call ( ) ; //$NON-NLS-1$
. setReflogPrefix ( REFLOG_PREFIX ) . setStrategy ( strategy ) ;
if ( isMerge ) {
pickCommand . setMainlineParentNumber ( 1 ) ;
// We write a MERGE_HEAD and later commit explicitly
pickCommand . setNoCommit ( true ) ;
writeMergeInfo ( commitToPick , newParents ) ;
}
CherryPickResult cherryPickResult = pickCommand . call ( ) ;
switch ( cherryPickResult . getStatus ( ) ) {
case FAILED :
if ( operation = = Operation . BEGIN )
@ -491,13 +585,91 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
case CONFLICTING :
return stop ( commitToPick , Status . STOPPED ) ;
case OK :
newHead = cherryPickResult . getNewHead ( ) ;
if ( isMerge ) {
// Commit the merge (setup above using writeMergeInfo())
CommitCommand commit = new Git ( repo ) . commit ( ) ;
commit . setAuthor ( commitToPick . getAuthorIdent ( ) ) ;
commit . setReflogComment ( REFLOG_PREFIX + " " //$NON-NLS-1$
+ commitToPick . getShortMessage ( ) ) ;
newHead = commit . call ( ) ;
} else
newHead = cherryPickResult . getNewHead ( ) ;
break ;
}
} else {
// Use the merge strategy to redo merges, which had some of
// their non-first parents rewritten
MergeCommand merge = new Git ( repo ) . merge ( )
. setFastForward ( MergeCommand . FastForwardMode . NO_FF )
. setCommit ( false ) ;
for ( int i = 1 ; i < commitToPick . getParentCount ( ) ; i + + )
merge . include ( newParents . get ( i ) ) ;
MergeResult mergeResult = merge . call ( ) ;
if ( mergeResult . getMergeStatus ( ) . isSuccessful ( ) ) {
CommitCommand commit = new Git ( repo ) . commit ( ) ;
commit . setAuthor ( commitToPick . getAuthorIdent ( ) ) ;
commit . setMessage ( commitToPick . getFullMessage ( ) ) ;
commit . setReflogComment ( REFLOG_PREFIX + " " //$NON-NLS-1$
+ commitToPick . getShortMessage ( ) ) ;
newHead = commit . call ( ) ;
} else {
if ( operation = = Operation . BEGIN
& & mergeResult . getMergeStatus ( ) = = MergeResult . MergeStatus . FAILED )
return abort ( RebaseResult . failed ( mergeResult
. getFailingPaths ( ) ) ) ;
return stop ( commitToPick , Status . STOPPED ) ;
}
}
return null ;
} finally {
monitor . endTask ( ) ;
}
return null ;
}
// Prepare MERGE_HEAD and message for the next commit
private void writeMergeInfo ( RevCommit commitToPick ,
List < RevCommit > newParents ) throws IOException {
repo . writeMergeHeads ( newParents . subList ( 1 , newParents . size ( ) ) ) ;
repo . writeMergeCommitMsg ( commitToPick . getFullMessage ( ) ) ;
}
// Get the rewritten equivalents for the parents of the given commit
private List < RevCommit > getNewParents ( RevCommit commitToPick )
throws IOException {
List < RevCommit > newParents = new ArrayList < RevCommit > ( ) ;
for ( int p = 0 ; p < commitToPick . getParentCount ( ) ; p + + ) {
String parentHash = commitToPick . getParent ( p ) . getName ( ) ;
if ( ! new File ( rebaseState . getRewrittenDir ( ) , parentHash ) . exists ( ) )
newParents . add ( commitToPick . getParent ( p ) ) ;
else {
String newParent = RebaseState . readFile (
rebaseState . getRewrittenDir ( ) , parentHash ) ;
if ( newParent . length ( ) = = 0 )
newParents . add ( walk . parseCommit ( repo
. resolve ( Constants . HEAD ) ) ) ;
else
newParents . add ( walk . parseCommit ( ObjectId
. fromString ( newParent ) ) ) ;
}
}
return newParents ;
}
private void writeCurrentCommit ( RevCommit commit ) throws IOException {
RebaseState . appendToFile ( rebaseState . getFile ( CURRENT_COMMIT ) ,
commit . name ( ) ) ;
}
private void writeRewrittenHashes ( ) throws RevisionSyntaxException ,
IOException {
File currentCommitFile = rebaseState . getFile ( CURRENT_COMMIT ) ;
if ( ! currentCommitFile . exists ( ) )
return ;
String head = repo . resolve ( Constants . HEAD ) . getName ( ) ;
String currentCommits = rebaseState . readFile ( CURRENT_COMMIT ) ;
for ( String current : currentCommits . split ( "\n" ) ) //$NON-NLS-1$
RebaseState
. createFile ( rebaseState . getRewrittenDir ( ) , current , head ) ;
FileUtils . delete ( currentCommitFile ) ;
}
private RebaseResult finishRebase ( RevCommit newHead ,
@ -908,19 +1080,6 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
monitor . beginTask ( JGitText . get ( ) . obtainingCommitsForCherryPick ,
ProgressMonitor . UNKNOWN ) ;
// determine the commits to be applied
LogCommand cmd = new Git ( repo ) . log ( ) . addRange ( upstreamCommit ,
headCommit ) ;
Iterable < RevCommit > commitsToUse = cmd . call ( ) ;
List < RevCommit > cherryPickList = new ArrayList < RevCommit > ( ) ;
for ( RevCommit commit : commitsToUse ) {
if ( commit . getParentCount ( ) ! = 1 )
continue ;
cherryPickList . add ( commit ) ;
}
Collections . reverse ( cherryPickList ) ;
// create the folder for the meta information
FileUtils . mkdir ( rebaseState . getDir ( ) , true ) ;
@ -935,6 +1094,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
ArrayList < RebaseTodoLine > toDoSteps = new ArrayList < RebaseTodoLine > ( ) ;
toDoSteps . add ( new RebaseTodoLine ( "# Created by EGit: rebasing " + headId . name ( ) //$NON-NLS-1$
+ " onto " + upstreamCommit . name ( ) ) ) ; //$NON-NLS-1$
// determine the commits to be applied
List < RevCommit > cherryPickList = calculatePickList ( headCommit ) ;
ObjectReader reader = walk . getObjectReader ( ) ;
for ( RevCommit commit : cherryPickList )
toDoSteps . add ( new RebaseTodoLine ( Action . PICK , reader
@ -959,6 +1120,50 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return null ;
}
private List < RevCommit > calculatePickList ( RevCommit headCommit )
throws GitAPIException , NoHeadException , IOException {
LogCommand cmd = new Git ( repo ) . log ( ) . addRange ( upstreamCommit ,
headCommit ) ;
Iterable < RevCommit > commitsToUse = cmd . call ( ) ;
List < RevCommit > cherryPickList = new ArrayList < RevCommit > ( ) ;
for ( RevCommit commit : commitsToUse ) {
if ( preserveMerges | | commit . getParentCount ( ) = = 1 )
cherryPickList . add ( commit ) ;
}
Collections . reverse ( cherryPickList ) ;
if ( preserveMerges ) {
// When preserving merges we only rewrite commits which have at
// least one parent that is itself rewritten (or a merge base)
File rewrittenDir = rebaseState . getRewrittenDir ( ) ;
FileUtils . mkdir ( rewrittenDir , false ) ;
walk . reset ( ) ;
walk . setRevFilter ( RevFilter . MERGE_BASE ) ;
walk . markStart ( upstreamCommit ) ;
walk . markStart ( headCommit ) ;
RevCommit base ;
while ( ( base = walk . next ( ) ) ! = null )
RebaseState . createFile ( rewrittenDir , base . getName ( ) ,
upstreamCommit . getName ( ) ) ;
Iterator < RevCommit > iterator = cherryPickList . iterator ( ) ;
pickLoop : while ( iterator . hasNext ( ) ) {
RevCommit commit = iterator . next ( ) ;
for ( int i = 0 ; i < commit . getParentCount ( ) ; i + + ) {
boolean parentRewritten = new File ( rewrittenDir , commit
. getParent ( i ) . getName ( ) ) . exists ( ) ;
if ( parentRewritten ) {
new File ( rewrittenDir , commit . getName ( ) ) . createNewFile ( ) ;
continue pickLoop ;
}
}
// commit is only merged in, needs not be rewritten
iterator . remove ( ) ;
}
}
return cherryPickList ;
}
private static String getHeadName ( Ref head ) {
String headName ;
if ( head . isSymbolic ( ) )
@ -1139,6 +1344,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
// cleanup the files
FileUtils . delete ( rebaseState . getDir ( ) , FileUtils . RECURSIVE ) ;
repo . writeCherryPickHead ( null ) ;
repo . writeMergeHeads ( null ) ;
if ( stashConflicts )
return RebaseResult . STASH_APPLY_CONFLICTS_RESULT ;
return result ;
@ -1320,6 +1526,18 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return this ;
}
/ * *
* @param preserve
* True to re - create merges during rebase . Defaults to false , a
* flattening rebase .
* @return { @code this }
* @since 3 . 5
* /
public RebaseCommand setPreserveMerges ( boolean preserve ) {
this . preserveMerges = preserve ;
return this ;
}
/ * *
* Allows configure rebase interactive process and modify commit message
* /
@ -1408,6 +1626,14 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return dir ;
}
/ * *
* @return Directory with rewritten commit hashes , usually exists if
* { @link RebaseCommand # preserveMerges } is true
* * /
public File getRewrittenDir ( ) {
return new File ( getDir ( ) , REWRITTEN ) ;
}
public String readFile ( String name ) throws IOException {
return readFile ( getDir ( ) , name ) ;
}
@ -1444,5 +1670,16 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
fos . close ( ) ;
}
}
private static void appendToFile ( File file , String content )
throws IOException {
FileOutputStream fos = new FileOutputStream ( file , true ) ;
try {
fos . write ( content . getBytes ( Constants . CHARACTER_ENCODING ) ) ;
fos . write ( '\n' ) ;
} finally {
fos . close ( ) ;
}
}
}
}