Git has different conflict resolution strategies: * There is a tree merge strategy "ours" which just ignores any changes from theirs ("-s ours"). JGit also has the mirror strategy "theirs" ignoring any changes from "ours". (This doesn't exist in C git.) Adapt StashApplyCommand and CherrypickCommand to be able to use those tree merge strategies. * For the resolve/recursive tree merge strategies, there are content conflict resolution strategies "ours" and "theirs", which resolve any conflict hunks by taking the "ours" or "theirs" hunk. In C git those correspond to "-Xours" or -Xtheirs". Implement that in MergeAlgorithm, and add API to set and pass through such a strategy for resolving content conflicts. * The "ours/theirs" content conflict resolution strategies also apply for binary files. Handle these cases in ResolveMerger. Note that the content conflict resolution strategies ("-X ours/theirs") do _not_ apply to modify/delete or delete/modify conflicts. Such conflicts are always reported as conflicts by C git. They do apply, however, if one side completely clears a file's content. Bug: 501111 Change-Id: I2c9c170c61c440a2ab9c387991e7a0c3ab960e07 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch> Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>tags/v5.12.0.202105051250-m2
@@ -115,6 +115,7 @@ metaVar_configFile=FILE | |||
metaVar_connProp=conn.prop | |||
metaVar_diffAlg=ALGORITHM | |||
metaVar_directory=DIRECTORY | |||
metaVar_extraArgument=ours|theirs | |||
metaVar_file=FILE | |||
metaVar_filepattern=filepattern | |||
metaVar_gitDir=GIT_DIR | |||
@@ -217,6 +218,7 @@ timeInMilliSeconds={0} ms | |||
treeIsRequired=argument tree is required | |||
tooManyRefsGiven=Too many refs given | |||
unknownIoErrorStdout=An unknown I/O error occurred on standard output | |||
unknownExtraArgument=unknown extra argument -X {0} specified | |||
unknownMergeStrategy=unknown merge strategy {0} specified | |||
unknownSubcommand=Unknown subcommand: {0} | |||
unmergedPaths=Unmerged paths: | |||
@@ -226,6 +228,7 @@ updating=Updating {0}..{1} | |||
usage_Aggressive=This option will cause gc to more aggressively optimize the repository at the expense of taking much more time | |||
usage_AlwaysFallback=Show uniquely abbreviated commit object as fallback | |||
usage_bareClone=Make a bare Git repository. That is, instead of creating [DIRECTORY] and placing the administrative files in [DIRECTORY]/.git, make the [DIRECTORY] itself the $GIT_DIR. | |||
usage_extraArgument=Pass an extra argument to a merge driver. Currently supported are "-X ours" and "-X theirs". | |||
usage_mirrorClone=Set up a mirror of the source repository. This implies --bare. Compared to --bare, --mirror not only maps \ | |||
local branches of the source to local branches of the target, it maps all refs (including remote-tracking branches, notes etc.) \ | |||
and sets up a refspec configuration such that all these refs are overwritten by a git remote update in the target repository. |
@@ -24,6 +24,7 @@ import org.eclipse.jgit.lib.AnyObjectId; | |||
import org.eclipse.jgit.lib.Constants; | |||
import org.eclipse.jgit.lib.ObjectId; | |||
import org.eclipse.jgit.lib.Ref; | |||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||
import org.eclipse.jgit.merge.MergeStrategy; | |||
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | |||
import org.eclipse.jgit.pgm.internal.CLIText; | |||
@@ -69,6 +70,20 @@ class Merge extends TextBuiltin { | |||
@Option(name = "-m", usage = "usage_message") | |||
private String message; | |||
private ContentMergeStrategy contentStrategy = null; | |||
@Option(name = "--strategy-option", aliases = { "-X" }, | |||
metaVar = "metaVar_extraArgument", usage = "usage_extraArgument") | |||
void extraArg(String name) { | |||
if (ContentMergeStrategy.OURS.name().equalsIgnoreCase(name)) { | |||
contentStrategy = ContentMergeStrategy.OURS; | |||
} else if (ContentMergeStrategy.THEIRS.name().equalsIgnoreCase(name)) { | |||
contentStrategy = ContentMergeStrategy.THEIRS; | |||
} else { | |||
throw die(MessageFormat.format(CLIText.get().unknownExtraArgument, name)); | |||
} | |||
} | |||
/** {@inheritDoc} */ | |||
@Override | |||
protected void run() { | |||
@@ -96,8 +111,11 @@ class Merge extends TextBuiltin { | |||
Ref oldHead = getOldHead(); | |||
MergeResult result; | |||
try (Git git = new Git(db)) { | |||
MergeCommand mergeCmd = git.merge().setStrategy(mergeStrategy) | |||
.setSquash(squash).setFastForward(ff) | |||
MergeCommand mergeCmd = git.merge() | |||
.setStrategy(mergeStrategy) | |||
.setContentMergeStrategy(contentStrategy) | |||
.setSquash(squash) | |||
.setFastForward(ff) | |||
.setCommit(!noCommit); | |||
if (srcRef != null) { | |||
mergeCmd.include(srcRef); |
@@ -284,6 +284,7 @@ public class CLIText extends TranslationBundle { | |||
/***/ public String tooManyRefsGiven; | |||
/***/ public String treeIsRequired; | |||
/***/ public char[] unknownIoErrorStdout; | |||
/***/ public String unknownExtraArgument; | |||
/***/ public String unknownMergeStrategy; | |||
/***/ public String unknownSubcommand; | |||
/***/ public String unmergedPaths; |
@@ -34,6 +34,8 @@ import org.eclipse.jgit.lib.FileMode; | |||
import org.eclipse.jgit.lib.ObjectId; | |||
import org.eclipse.jgit.lib.ReflogReader; | |||
import org.eclipse.jgit.lib.RepositoryState; | |||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||
import org.eclipse.jgit.merge.MergeStrategy; | |||
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.junit.Test; | |||
@@ -193,7 +195,7 @@ public class CherryPickCommandTest extends RepositoryTestCase { | |||
} | |||
@Test | |||
public void testCherryPickConflictResolutionNoCOmmit() throws Exception { | |||
public void testCherryPickConflictResolutionNoCommit() throws Exception { | |||
Git git = new Git(db); | |||
RevCommit sideCommit = prepareCherryPick(git); | |||
@@ -279,6 +281,70 @@ public class CherryPickCommandTest extends RepositoryTestCase { | |||
} | |||
} | |||
@Test | |||
public void testCherryPickOurs() throws Exception { | |||
try (Git git = new Git(db)) { | |||
RevCommit sideCommit = prepareCherryPick(git); | |||
CherryPickResult result = git.cherryPick() | |||
.include(sideCommit.getId()) | |||
.setStrategy(MergeStrategy.OURS) | |||
.call(); | |||
assertEquals(CherryPickStatus.OK, result.getStatus()); | |||
String expected = "a(master)"; | |||
checkFile(new File(db.getWorkTree(), "a"), expected); | |||
} | |||
} | |||
@Test | |||
public void testCherryPickTheirs() throws Exception { | |||
try (Git git = new Git(db)) { | |||
RevCommit sideCommit = prepareCherryPick(git); | |||
CherryPickResult result = git.cherryPick() | |||
.include(sideCommit.getId()) | |||
.setStrategy(MergeStrategy.THEIRS) | |||
.call(); | |||
assertEquals(CherryPickStatus.OK, result.getStatus()); | |||
String expected = "a(side)"; | |||
checkFile(new File(db.getWorkTree(), "a"), expected); | |||
} | |||
} | |||
@Test | |||
public void testCherryPickXours() throws Exception { | |||
try (Git git = new Git(db)) { | |||
RevCommit sideCommit = prepareCherryPickStrategyOption(git); | |||
CherryPickResult result = git.cherryPick() | |||
.include(sideCommit.getId()) | |||
.setContentMergeStrategy(ContentMergeStrategy.OURS) | |||
.call(); | |||
assertEquals(CherryPickStatus.OK, result.getStatus()); | |||
String expected = "a\nmaster\nc\nd\n"; | |||
checkFile(new File(db.getWorkTree(), "a"), expected); | |||
} | |||
} | |||
@Test | |||
public void testCherryPickXtheirs() throws Exception { | |||
try (Git git = new Git(db)) { | |||
RevCommit sideCommit = prepareCherryPickStrategyOption(git); | |||
CherryPickResult result = git.cherryPick() | |||
.include(sideCommit.getId()) | |||
.setContentMergeStrategy(ContentMergeStrategy.THEIRS) | |||
.call(); | |||
assertEquals(CherryPickStatus.OK, result.getStatus()); | |||
String expected = "a\nside\nc\nd\n"; | |||
checkFile(new File(db.getWorkTree(), "a"), expected); | |||
} | |||
} | |||
@Test | |||
public void testCherryPickConflictMarkers() throws Exception { | |||
try (Git git = new Git(db)) { | |||
@@ -384,6 +450,31 @@ public class CherryPickCommandTest extends RepositoryTestCase { | |||
return sideCommit; | |||
} | |||
private RevCommit prepareCherryPickStrategyOption(Git git) | |||
throws Exception { | |||
// create, add and commit file a | |||
writeTrashFile("a", "a\nb\nc\n"); | |||
git.add().addFilepattern("a").call(); | |||
RevCommit firstMasterCommit = git.commit().setMessage("first master") | |||
.call(); | |||
// create and checkout side branch | |||
createBranch(firstMasterCommit, "refs/heads/side"); | |||
checkoutBranch("refs/heads/side"); | |||
// modify, add and commit file a | |||
writeTrashFile("a", "a\nside\nc\nd\n"); | |||
git.add().addFilepattern("a").call(); | |||
RevCommit sideCommit = git.commit().setMessage("side").call(); | |||
// checkout master branch | |||
checkoutBranch("refs/heads/master"); | |||
// modify, add and commit file a | |||
writeTrashFile("a", "a\nmaster\nc\n"); | |||
git.add().addFilepattern("a").call(); | |||
git.commit().setMessage("second master").call(); | |||
return sideCommit; | |||
} | |||
private void doCherryPickAndCheckResult(final Git git, | |||
final RevCommit sideCommit, final MergeFailureReason reason) | |||
throws Exception { |
@@ -14,6 +14,7 @@ import static org.eclipse.jgit.lib.Constants.MASTER; | |||
import static org.eclipse.jgit.lib.Constants.R_HEADS; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertFalse; | |||
import static org.junit.Assert.assertNotNull; | |||
import static org.junit.Assert.assertNull; | |||
import static org.junit.Assert.assertTrue; | |||
import static org.junit.Assert.fail; | |||
@@ -25,6 +26,7 @@ import java.util.regex.Pattern; | |||
import org.eclipse.jgit.api.MergeCommand.FastForwardMode; | |||
import org.eclipse.jgit.api.MergeResult.MergeStatus; | |||
import org.eclipse.jgit.api.ResetCommand.ResetType; | |||
import org.eclipse.jgit.api.errors.InvalidMergeHeadsException; | |||
import org.eclipse.jgit.junit.RepositoryTestCase; | |||
import org.eclipse.jgit.junit.TestRepository; | |||
@@ -34,6 +36,7 @@ import org.eclipse.jgit.lib.Ref; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.lib.RepositoryState; | |||
import org.eclipse.jgit.lib.Sets; | |||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||
import org.eclipse.jgit.merge.MergeStrategy; | |||
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
@@ -305,6 +308,200 @@ public class MergeCommandTest extends RepositoryTestCase { | |||
} | |||
} | |||
@Test | |||
public void testContentMergeXtheirs() throws Exception { | |||
try (Git git = new Git(db)) { | |||
writeTrashFile("a", "1\na\n3\n"); | |||
writeTrashFile("b", "1\nb\n3\n"); | |||
writeTrashFile("c/c/c", "1\nc\n3\n"); | |||
git.add().addFilepattern("a").addFilepattern("b") | |||
.addFilepattern("c/c/c").call(); | |||
RevCommit initialCommit = git.commit().setMessage("initial").call(); | |||
createBranch(initialCommit, "refs/heads/side"); | |||
checkoutBranch("refs/heads/side"); | |||
writeTrashFile("a", "1\na(side)\n3\n4\n"); | |||
writeTrashFile("b", "1\nb(side)\n3\n4\n"); | |||
git.add().addFilepattern("a").addFilepattern("b").call(); | |||
RevCommit secondCommit = git.commit().setMessage("side").call(); | |||
assertEquals("1\nb(side)\n3\n4\n", | |||
read(new File(db.getWorkTree(), "b"))); | |||
checkoutBranch("refs/heads/master"); | |||
assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b"))); | |||
writeTrashFile("a", "1\na(main)\n3\n"); | |||
writeTrashFile("c/c/c", "1\nc(main)\n3\n"); | |||
git.add().addFilepattern("a").addFilepattern("c/c/c").call(); | |||
git.commit().setMessage("main").call(); | |||
MergeResult result = git.merge().include(secondCommit.getId()) | |||
.setStrategy(MergeStrategy.RESOLVE) | |||
.setContentMergeStrategy(ContentMergeStrategy.THEIRS) | |||
.call(); | |||
assertEquals(MergeStatus.MERGED, result.getMergeStatus()); | |||
assertEquals("1\na(side)\n3\n4\n", | |||
read(new File(db.getWorkTree(), "a"))); | |||
assertEquals("1\nb(side)\n3\n4\n", | |||
read(new File(db.getWorkTree(), "b"))); | |||
assertEquals("1\nc(main)\n3\n", | |||
read(new File(db.getWorkTree(), "c/c/c"))); | |||
assertNull(result.getConflicts()); | |||
assertEquals(RepositoryState.SAFE, db.getRepositoryState()); | |||
} | |||
} | |||
@Test | |||
public void testContentMergeXours() throws Exception { | |||
try (Git git = new Git(db)) { | |||
writeTrashFile("a", "1\na\n3\n"); | |||
writeTrashFile("b", "1\nb\n3\n"); | |||
writeTrashFile("c/c/c", "1\nc\n3\n"); | |||
git.add().addFilepattern("a").addFilepattern("b") | |||
.addFilepattern("c/c/c").call(); | |||
RevCommit initialCommit = git.commit().setMessage("initial").call(); | |||
createBranch(initialCommit, "refs/heads/side"); | |||
checkoutBranch("refs/heads/side"); | |||
writeTrashFile("a", "1\na(side)\n3\n4\n"); | |||
writeTrashFile("b", "1\nb(side)\n3\n4\n"); | |||
git.add().addFilepattern("a").addFilepattern("b").call(); | |||
RevCommit secondCommit = git.commit().setMessage("side").call(); | |||
assertEquals("1\nb(side)\n3\n4\n", | |||
read(new File(db.getWorkTree(), "b"))); | |||
checkoutBranch("refs/heads/master"); | |||
assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b"))); | |||
writeTrashFile("a", "1\na(main)\n3\n"); | |||
writeTrashFile("c/c/c", "1\nc(main)\n3\n"); | |||
git.add().addFilepattern("a").addFilepattern("c/c/c").call(); | |||
git.commit().setMessage("main").call(); | |||
MergeResult result = git.merge().include(secondCommit.getId()) | |||
.setStrategy(MergeStrategy.RESOLVE) | |||
.setContentMergeStrategy(ContentMergeStrategy.OURS).call(); | |||
assertEquals(MergeStatus.MERGED, result.getMergeStatus()); | |||
assertEquals("1\na(main)\n3\n4\n", | |||
read(new File(db.getWorkTree(), "a"))); | |||
assertEquals("1\nb(side)\n3\n4\n", | |||
read(new File(db.getWorkTree(), "b"))); | |||
assertEquals("1\nc(main)\n3\n", | |||
read(new File(db.getWorkTree(), "c/c/c"))); | |||
assertNull(result.getConflicts()); | |||
assertEquals(RepositoryState.SAFE, db.getRepositoryState()); | |||
} | |||
} | |||
@Test | |||
public void testBinaryContentMerge() throws Exception { | |||
try (Git git = new Git(db)) { | |||
writeTrashFile(".gitattributes", "a binary"); | |||
writeTrashFile("a", "initial"); | |||
git.add().addFilepattern(".").call(); | |||
RevCommit initialCommit = git.commit().setMessage("initial").call(); | |||
createBranch(initialCommit, "refs/heads/side"); | |||
checkoutBranch("refs/heads/side"); | |||
writeTrashFile("a", "side"); | |||
git.add().addFilepattern("a").call(); | |||
RevCommit secondCommit = git.commit().setMessage("side").call(); | |||
checkoutBranch("refs/heads/master"); | |||
writeTrashFile("a", "main"); | |||
git.add().addFilepattern("a").call(); | |||
git.commit().setMessage("main").call(); | |||
MergeResult result = git.merge().include(secondCommit.getId()) | |||
.setStrategy(MergeStrategy.RESOLVE).call(); | |||
assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus()); | |||
assertEquals("main", read(new File(db.getWorkTree(), "a"))); | |||
// Hmmm... there doesn't seem to be a way to figure out which files | |||
// had a binary conflict from a MergeResult... | |||
assertEquals(RepositoryState.MERGING, db.getRepositoryState()); | |||
} | |||
} | |||
@Test | |||
public void testBinaryContentMergeXtheirs() throws Exception { | |||
try (Git git = new Git(db)) { | |||
writeTrashFile(".gitattributes", "a binary"); | |||
writeTrashFile("a", "initial"); | |||
git.add().addFilepattern(".").call(); | |||
RevCommit initialCommit = git.commit().setMessage("initial").call(); | |||
createBranch(initialCommit, "refs/heads/side"); | |||
checkoutBranch("refs/heads/side"); | |||
writeTrashFile("a", "side"); | |||
git.add().addFilepattern("a").call(); | |||
RevCommit secondCommit = git.commit().setMessage("side").call(); | |||
checkoutBranch("refs/heads/master"); | |||
writeTrashFile("a", "main"); | |||
git.add().addFilepattern("a").call(); | |||
git.commit().setMessage("main").call(); | |||
MergeResult result = git.merge().include(secondCommit.getId()) | |||
.setStrategy(MergeStrategy.RESOLVE) | |||
.setContentMergeStrategy(ContentMergeStrategy.THEIRS) | |||
.call(); | |||
assertEquals(MergeStatus.MERGED, result.getMergeStatus()); | |||
assertEquals("side", read(new File(db.getWorkTree(), "a"))); | |||
assertNull(result.getConflicts()); | |||
assertEquals(RepositoryState.SAFE, db.getRepositoryState()); | |||
} | |||
} | |||
@Test | |||
public void testBinaryContentMergeXours() throws Exception { | |||
try (Git git = new Git(db)) { | |||
writeTrashFile(".gitattributes", "a binary"); | |||
writeTrashFile("a", "initial"); | |||
git.add().addFilepattern(".").call(); | |||
RevCommit initialCommit = git.commit().setMessage("initial").call(); | |||
createBranch(initialCommit, "refs/heads/side"); | |||
checkoutBranch("refs/heads/side"); | |||
writeTrashFile("a", "side"); | |||
git.add().addFilepattern("a").call(); | |||
RevCommit secondCommit = git.commit().setMessage("side").call(); | |||
checkoutBranch("refs/heads/master"); | |||
writeTrashFile("a", "main"); | |||
git.add().addFilepattern("a").call(); | |||
git.commit().setMessage("main").call(); | |||
MergeResult result = git.merge().include(secondCommit.getId()) | |||
.setStrategy(MergeStrategy.RESOLVE) | |||
.setContentMergeStrategy(ContentMergeStrategy.OURS).call(); | |||
assertEquals(MergeStatus.MERGED, result.getMergeStatus()); | |||
assertEquals("main", read(new File(db.getWorkTree(), "a"))); | |||
assertNull(result.getConflicts()); | |||
assertEquals(RepositoryState.SAFE, db.getRepositoryState()); | |||
} | |||
} | |||
@Test | |||
public void testMergeTag() throws Exception { | |||
try (Git git = new Git(db)) { | |||
@@ -774,6 +971,51 @@ public class MergeCommandTest extends RepositoryTestCase { | |||
@Test | |||
public void testDeletionOnMasterConflict() throws Exception { | |||
try (Git git = new Git(db)) { | |||
writeTrashFile("a", "1\na\n3\n"); | |||
writeTrashFile("b", "1\nb\n3\n"); | |||
git.add().addFilepattern("a").addFilepattern("b").call(); | |||
RevCommit initialCommit = git.commit().setMessage("initial").call(); | |||
// create side branch and modify "a" | |||
createBranch(initialCommit, "refs/heads/side"); | |||
checkoutBranch("refs/heads/side"); | |||
writeTrashFile("a", "1\na(side)\n3\n"); | |||
git.add().addFilepattern("a").call(); | |||
RevCommit secondCommit = git.commit().setMessage("side").call(); | |||
// delete a on master to generate conflict | |||
checkoutBranch("refs/heads/master"); | |||
git.rm().addFilepattern("a").call(); | |||
RevCommit thirdCommit = git.commit().setMessage("main").call(); | |||
for (ContentMergeStrategy contentStrategy : ContentMergeStrategy | |||
.values()) { | |||
// merge side with master | |||
MergeResult result = git.merge().include(secondCommit.getId()) | |||
.setStrategy(MergeStrategy.RESOLVE) | |||
.setContentMergeStrategy(contentStrategy) | |||
.call(); | |||
assertEquals("merge -X " + contentStrategy.name(), | |||
MergeStatus.CONFLICTING, result.getMergeStatus()); | |||
// result should be 'a' conflicting with workspace content from | |||
// side | |||
assertTrue("merge -X " + contentStrategy.name(), | |||
new File(db.getWorkTree(), "a").exists()); | |||
assertEquals("merge -X " + contentStrategy.name(), | |||
"1\na(side)\n3\n", | |||
read(new File(db.getWorkTree(), "a"))); | |||
assertEquals("merge -X " + contentStrategy.name(), "1\nb\n3\n", | |||
read(new File(db.getWorkTree(), "b"))); | |||
git.reset().setMode(ResetType.HARD).setRef(thirdCommit.name()) | |||
.call(); | |||
} | |||
} | |||
} | |||
@Test | |||
public void testDeletionOnMasterTheirs() throws Exception { | |||
try (Git git = new Git(db)) { | |||
writeTrashFile("a", "1\na\n3\n"); | |||
writeTrashFile("b", "1\nb\n3\n"); | |||
@@ -794,18 +1036,102 @@ public class MergeCommandTest extends RepositoryTestCase { | |||
// merge side with master | |||
MergeResult result = git.merge().include(secondCommit.getId()) | |||
.setStrategy(MergeStrategy.RESOLVE).call(); | |||
assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus()); | |||
.setStrategy(MergeStrategy.THEIRS) | |||
.call(); | |||
assertEquals(MergeStatus.MERGED, result.getMergeStatus()); | |||
// result should be 'a' conflicting with workspace content from side | |||
// result should be 'a' | |||
assertTrue(new File(db.getWorkTree(), "a").exists()); | |||
assertEquals("1\na(side)\n3\n", read(new File(db.getWorkTree(), "a"))); | |||
assertEquals("1\na(side)\n3\n", | |||
read(new File(db.getWorkTree(), "a"))); | |||
assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b"))); | |||
assertTrue(git.status().call().isClean()); | |||
} | |||
} | |||
@Test | |||
public void testDeletionOnMasterOurs() throws Exception { | |||
try (Git git = new Git(db)) { | |||
writeTrashFile("a", "1\na\n3\n"); | |||
writeTrashFile("b", "1\nb\n3\n"); | |||
git.add().addFilepattern("a").addFilepattern("b").call(); | |||
RevCommit initialCommit = git.commit().setMessage("initial").call(); | |||
// create side branch and modify "a" | |||
createBranch(initialCommit, "refs/heads/side"); | |||
checkoutBranch("refs/heads/side"); | |||
writeTrashFile("a", "1\na(side)\n3\n"); | |||
git.add().addFilepattern("a").call(); | |||
RevCommit secondCommit = git.commit().setMessage("side").call(); | |||
// delete a on master to generate conflict | |||
checkoutBranch("refs/heads/master"); | |||
git.rm().addFilepattern("a").call(); | |||
git.commit().setMessage("main").call(); | |||
// merge side with master | |||
MergeResult result = git.merge().include(secondCommit.getId()) | |||
.setStrategy(MergeStrategy.OURS).call(); | |||
assertEquals(MergeStatus.MERGED, result.getMergeStatus()); | |||
assertFalse(new File(db.getWorkTree(), "a").exists()); | |||
assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b"))); | |||
assertTrue(git.status().call().isClean()); | |||
} | |||
} | |||
@Test | |||
public void testDeletionOnSideConflict() throws Exception { | |||
try (Git git = new Git(db)) { | |||
writeTrashFile("a", "1\na\n3\n"); | |||
writeTrashFile("b", "1\nb\n3\n"); | |||
git.add().addFilepattern("a").addFilepattern("b").call(); | |||
RevCommit initialCommit = git.commit().setMessage("initial").call(); | |||
// create side branch and delete "a" | |||
createBranch(initialCommit, "refs/heads/side"); | |||
checkoutBranch("refs/heads/side"); | |||
git.rm().addFilepattern("a").call(); | |||
RevCommit secondCommit = git.commit().setMessage("side").call(); | |||
// update a on master to generate conflict | |||
checkoutBranch("refs/heads/master"); | |||
writeTrashFile("a", "1\na(main)\n3\n"); | |||
git.add().addFilepattern("a").call(); | |||
RevCommit thirdCommit = git.commit().setMessage("main").call(); | |||
for (ContentMergeStrategy contentStrategy : ContentMergeStrategy | |||
.values()) { | |||
// merge side with master | |||
MergeResult result = git.merge().include(secondCommit.getId()) | |||
.setStrategy(MergeStrategy.RESOLVE) | |||
.setContentMergeStrategy(contentStrategy) | |||
.call(); | |||
assertEquals("merge -X " + contentStrategy.name(), | |||
MergeStatus.CONFLICTING, result.getMergeStatus()); | |||
assertTrue("merge -X " + contentStrategy.name(), | |||
new File(db.getWorkTree(), "a").exists()); | |||
assertEquals("merge -X " + contentStrategy.name(), | |||
"1\na(main)\n3\n", | |||
read(new File(db.getWorkTree(), "a"))); | |||
assertEquals("merge -X " + contentStrategy.name(), "1\nb\n3\n", | |||
read(new File(db.getWorkTree(), "b"))); | |||
assertNotNull("merge -X " + contentStrategy.name(), | |||
result.getConflicts()); | |||
assertEquals("merge -X " + contentStrategy.name(), 1, | |||
result.getConflicts().size()); | |||
assertEquals("merge -X " + contentStrategy.name(), 3, | |||
result.getConflicts().get("a")[0].length); | |||
git.reset().setMode(ResetType.HARD).setRef(thirdCommit.name()) | |||
.call(); | |||
} | |||
} | |||
} | |||
@Test | |||
public void testDeletionOnSideTheirs() throws Exception { | |||
try (Git git = new Git(db)) { | |||
writeTrashFile("a", "1\na\n3\n"); | |||
writeTrashFile("b", "1\nb\n3\n"); | |||
@@ -826,15 +1152,45 @@ public class MergeCommandTest extends RepositoryTestCase { | |||
// merge side with master | |||
MergeResult result = git.merge().include(secondCommit.getId()) | |||
.setStrategy(MergeStrategy.RESOLVE).call(); | |||
assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus()); | |||
.setStrategy(MergeStrategy.THEIRS).call(); | |||
assertEquals(MergeStatus.MERGED, result.getMergeStatus()); | |||
assertTrue(new File(db.getWorkTree(), "a").exists()); | |||
assertEquals("1\na(main)\n3\n", read(new File(db.getWorkTree(), "a"))); | |||
assertFalse(new File(db.getWorkTree(), "a").exists()); | |||
assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b"))); | |||
assertTrue(git.status().call().isClean()); | |||
} | |||
} | |||
assertEquals(1, result.getConflicts().size()); | |||
assertEquals(3, result.getConflicts().get("a")[0].length); | |||
@Test | |||
public void testDeletionOnSideOurs() throws Exception { | |||
try (Git git = new Git(db)) { | |||
writeTrashFile("a", "1\na\n3\n"); | |||
writeTrashFile("b", "1\nb\n3\n"); | |||
git.add().addFilepattern("a").addFilepattern("b").call(); | |||
RevCommit initialCommit = git.commit().setMessage("initial").call(); | |||
// create side branch and delete "a" | |||
createBranch(initialCommit, "refs/heads/side"); | |||
checkoutBranch("refs/heads/side"); | |||
git.rm().addFilepattern("a").call(); | |||
RevCommit secondCommit = git.commit().setMessage("side").call(); | |||
// update a on master to generate conflict | |||
checkoutBranch("refs/heads/master"); | |||
writeTrashFile("a", "1\na(main)\n3\n"); | |||
git.add().addFilepattern("a").call(); | |||
git.commit().setMessage("main").call(); | |||
// merge side with master | |||
MergeResult result = git.merge().include(secondCommit.getId()) | |||
.setStrategy(MergeStrategy.OURS).call(); | |||
assertEquals(MergeStatus.MERGED, result.getMergeStatus()); | |||
assertTrue(new File(db.getWorkTree(), "a").exists()); | |||
assertEquals("1\na(main)\n3\n", | |||
read(new File(db.getWorkTree(), "a"))); | |||
assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b"))); | |||
assertTrue(git.status().call().isClean()); | |||
} | |||
} | |||
@@ -34,6 +34,8 @@ import org.eclipse.jgit.lib.RefUpdate; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.lib.RepositoryState; | |||
import org.eclipse.jgit.lib.StoredConfig; | |||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||
import org.eclipse.jgit.merge.MergeStrategy; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.eclipse.jgit.revwalk.RevSort; | |||
import org.eclipse.jgit.revwalk.RevWalk; | |||
@@ -153,6 +155,75 @@ public class PullCommandTest extends RepositoryTestCase { | |||
.getRepositoryState()); | |||
} | |||
@Test | |||
public void testPullConflictTheirs() throws Exception { | |||
PullResult res = target.pull().call(); | |||
// nothing to update since we don't have different data yet | |||
assertTrue(res.getFetchResult().getTrackingRefUpdates().isEmpty()); | |||
assertTrue(res.getMergeResult().getMergeStatus() | |||
.equals(MergeStatus.ALREADY_UP_TO_DATE)); | |||
assertFileContentsEqual(targetFile, "Hello world"); | |||
// change the source file | |||
writeToFile(sourceFile, "Source change"); | |||
source.add().addFilepattern("SomeFile.txt").call(); | |||
source.commit().setMessage("Source change in remote").call(); | |||
// change the target file | |||
writeToFile(targetFile, "Target change"); | |||
target.add().addFilepattern("SomeFile.txt").call(); | |||
target.commit().setMessage("Target change in local").call(); | |||
res = target.pull().setStrategy(MergeStrategy.THEIRS).call(); | |||
assertTrue(res.isSuccessful()); | |||
assertFileContentsEqual(targetFile, "Source change"); | |||
assertEquals(RepositoryState.SAFE, | |||
target.getRepository().getRepositoryState()); | |||
assertTrue(target.status().call().isClean()); | |||
} | |||
@Test | |||
public void testPullConflictXtheirs() throws Exception { | |||
PullResult res = target.pull().call(); | |||
// nothing to update since we don't have different data yet | |||
assertTrue(res.getFetchResult().getTrackingRefUpdates().isEmpty()); | |||
assertTrue(res.getMergeResult().getMergeStatus() | |||
.equals(MergeStatus.ALREADY_UP_TO_DATE)); | |||
assertFileContentsEqual(targetFile, "Hello world"); | |||
// change the source file | |||
writeToFile(sourceFile, "a\nHello\nb\n"); | |||
source.add().addFilepattern("SomeFile.txt").call(); | |||
source.commit().setMessage("Multi-line change in remote").call(); | |||
// Pull again | |||
res = target.pull().call(); | |||
assertTrue(res.isSuccessful()); | |||
assertFileContentsEqual(targetFile, "a\nHello\nb\n"); | |||
// change the source file | |||
writeToFile(sourceFile, "a\nSource change\nb\n"); | |||
source.add().addFilepattern("SomeFile.txt").call(); | |||
source.commit().setMessage("Source change in remote").call(); | |||
// change the target file | |||
writeToFile(targetFile, "a\nTarget change\nb\nc\n"); | |||
target.add().addFilepattern("SomeFile.txt").call(); | |||
target.commit().setMessage("Target change in local").call(); | |||
res = target.pull().setContentMergeStrategy(ContentMergeStrategy.THEIRS) | |||
.call(); | |||
assertTrue(res.isSuccessful()); | |||
assertFileContentsEqual(targetFile, "a\nSource change\nb\nc\n"); | |||
assertEquals(RepositoryState.SAFE, | |||
target.getRepository().getRepositoryState()); | |||
assertTrue(target.status().call().isClean()); | |||
} | |||
@Test | |||
public void testPullWithUntrackedStash() throws Exception { | |||
target.pull().call(); |
@@ -1,5 +1,5 @@ | |||
/* | |||
* Copyright (C) 2012, GitHub Inc. and others | |||
* Copyright (C) 2012, 2021 GitHub Inc. and others | |||
* | |||
* This program and the accompanying materials are made available under the | |||
* terms of the Eclipse Distribution License v. 1.0 which is available at | |||
@@ -28,6 +28,8 @@ import org.eclipse.jgit.internal.JGitText; | |||
import org.eclipse.jgit.junit.RepositoryTestCase; | |||
import org.eclipse.jgit.lib.ObjectId; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||
import org.eclipse.jgit.merge.MergeStrategy; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.eclipse.jgit.util.FileUtils; | |||
import org.junit.After; | |||
@@ -426,6 +428,135 @@ public class StashApplyCommandTest extends RepositoryTestCase { | |||
read(PATH)); | |||
} | |||
@Test | |||
public void stashedContentMergeXtheirs() throws Exception { | |||
writeTrashFile(PATH, "content\nmore content\n"); | |||
git.add().addFilepattern(PATH).call(); | |||
git.commit().setMessage("more content").call(); | |||
writeTrashFile(PATH, "content\nhead change\nmore content\n"); | |||
git.add().addFilepattern(PATH).call(); | |||
git.commit().setMessage("even content").call(); | |||
writeTrashFile(PATH, "content\nstashed change\nmore content\n"); | |||
RevCommit stashed = git.stashCreate().call(); | |||
assertNotNull(stashed); | |||
assertEquals("content\nhead change\nmore content\n", | |||
read(committedFile)); | |||
assertTrue(git.status().call().isClean()); | |||
recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); | |||
writeTrashFile(PATH, "content\nmore content\ncommitted change\n"); | |||
git.add().addFilepattern(PATH).call(); | |||
git.commit().setMessage("committed change").call(); | |||
recorder.assertNoEvent(); | |||
git.stashApply().setContentMergeStrategy(ContentMergeStrategy.THEIRS) | |||
.call(); | |||
recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); | |||
Status status = new StatusCommand(db).call(); | |||
assertEquals('[' + PATH + ']', status.getModified().toString()); | |||
assertEquals( | |||
"content\nstashed change\nmore content\ncommitted change\n", | |||
read(PATH)); | |||
} | |||
@Test | |||
public void stashedContentMergeXours() throws Exception { | |||
writeTrashFile(PATH, "content\nmore content\n"); | |||
git.add().addFilepattern(PATH).call(); | |||
git.commit().setMessage("more content").call(); | |||
writeTrashFile(PATH, "content\nhead change\nmore content\n"); | |||
git.add().addFilepattern(PATH).call(); | |||
git.commit().setMessage("even content").call(); | |||
writeTrashFile(PATH, "content\nstashed change\nmore content\n"); | |||
RevCommit stashed = git.stashCreate().call(); | |||
assertNotNull(stashed); | |||
assertEquals("content\nhead change\nmore content\n", | |||
read(committedFile)); | |||
assertTrue(git.status().call().isClean()); | |||
recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); | |||
writeTrashFile(PATH, | |||
"content\nnew head\nmore content\ncommitted change\n"); | |||
git.add().addFilepattern(PATH).call(); | |||
git.commit().setMessage("committed change").call(); | |||
recorder.assertNoEvent(); | |||
git.stashApply().setContentMergeStrategy(ContentMergeStrategy.OURS) | |||
.call(); | |||
recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); | |||
assertTrue(git.status().call().isClean()); | |||
assertEquals("content\nnew head\nmore content\ncommitted change\n", | |||
read(PATH)); | |||
} | |||
@Test | |||
public void stashedContentMergeTheirs() throws Exception { | |||
writeTrashFile(PATH, "content\nmore content\n"); | |||
git.add().addFilepattern(PATH).call(); | |||
git.commit().setMessage("more content").call(); | |||
writeTrashFile(PATH, "content\nhead change\nmore content\n"); | |||
git.add().addFilepattern(PATH).call(); | |||
git.commit().setMessage("even content").call(); | |||
writeTrashFile(PATH, "content\nstashed change\nmore content\n"); | |||
RevCommit stashed = git.stashCreate().call(); | |||
assertNotNull(stashed); | |||
assertEquals("content\nhead change\nmore content\n", | |||
read(committedFile)); | |||
assertTrue(git.status().call().isClean()); | |||
recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); | |||
writeTrashFile(PATH, "content\nmore content\ncommitted change\n"); | |||
git.add().addFilepattern(PATH).call(); | |||
git.commit().setMessage("committed change").call(); | |||
recorder.assertNoEvent(); | |||
git.stashApply().setStrategy(MergeStrategy.THEIRS).call(); | |||
recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); | |||
Status status = new StatusCommand(db).call(); | |||
assertEquals('[' + PATH + ']', status.getModified().toString()); | |||
assertEquals("content\nstashed change\nmore content\n", read(PATH)); | |||
} | |||
@Test | |||
public void stashedContentMergeOurs() throws Exception { | |||
writeTrashFile(PATH, "content\nmore content\n"); | |||
git.add().addFilepattern(PATH).call(); | |||
git.commit().setMessage("more content").call(); | |||
writeTrashFile(PATH, "content\nhead change\nmore content\n"); | |||
git.add().addFilepattern(PATH).call(); | |||
git.commit().setMessage("even content").call(); | |||
writeTrashFile(PATH, "content\nstashed change\nmore content\n"); | |||
RevCommit stashed = git.stashCreate().call(); | |||
assertNotNull(stashed); | |||
assertEquals("content\nhead change\nmore content\n", | |||
read(committedFile)); | |||
assertTrue(git.status().call().isClean()); | |||
recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); | |||
writeTrashFile(PATH, "content\nmore content\ncommitted change\n"); | |||
git.add().addFilepattern(PATH).call(); | |||
git.commit().setMessage("committed change").call(); | |||
recorder.assertNoEvent(); | |||
// Doesn't make any sense... should be a no-op | |||
git.stashApply().setStrategy(MergeStrategy.OURS).call(); | |||
recorder.assertNoEvent(); | |||
assertTrue(git.status().call().isClean()); | |||
assertEquals("content\nmore content\ncommitted change\n", read(PATH)); | |||
} | |||
@Test | |||
public void stashedApplyOnOtherBranch() throws Exception { | |||
writeTrashFile(PATH, "content\nmore content\n"); |
@@ -1,5 +1,5 @@ | |||
/* | |||
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> and others | |||
* Copyright (C) 2010, 2021 Christian Halstrick <christian.halstrick@sap.com> and others | |||
* | |||
* This program and the accompanying materials are made available under the | |||
* terms of the Eclipse Distribution License v. 1.0 which is available at | |||
@@ -13,6 +13,7 @@ import java.io.IOException; | |||
import java.text.MessageFormat; | |||
import java.util.LinkedList; | |||
import java.util.List; | |||
import java.util.Map; | |||
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; | |||
import org.eclipse.jgit.api.errors.GitAPIException; | |||
@@ -35,9 +36,12 @@ import org.eclipse.jgit.lib.ProgressMonitor; | |||
import org.eclipse.jgit.lib.Ref; | |||
import org.eclipse.jgit.lib.Ref.Storage; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||
import org.eclipse.jgit.merge.MergeMessageFormatter; | |||
import org.eclipse.jgit.merge.MergeStrategy; | |||
import org.eclipse.jgit.merge.Merger; | |||
import org.eclipse.jgit.merge.ResolveMerger; | |||
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.eclipse.jgit.revwalk.RevWalk; | |||
import org.eclipse.jgit.treewalk.FileTreeIterator; | |||
@@ -61,6 +65,8 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { | |||
private MergeStrategy strategy = MergeStrategy.RECURSIVE; | |||
private ContentMergeStrategy contentStrategy; | |||
private Integer mainlineParentNumber; | |||
private boolean noCommit = false; | |||
@@ -121,16 +127,30 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { | |||
String cherryPickName = srcCommit.getId().abbreviate(7).name() | |||
+ " " + srcCommit.getShortMessage(); //$NON-NLS-1$ | |||
ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo); | |||
merger.setWorkingTreeIterator(new FileTreeIterator(repo)); | |||
merger.setBase(srcParent.getTree()); | |||
merger.setCommitNames(new String[] { "BASE", ourName, //$NON-NLS-1$ | |||
cherryPickName }); | |||
if (merger.merge(newHead, srcCommit)) { | |||
if (!merger.getModifiedFiles().isEmpty()) { | |||
Merger merger = strategy.newMerger(repo); | |||
merger.setProgressMonitor(monitor); | |||
boolean noProblems; | |||
Map<String, MergeFailureReason> failingPaths = null; | |||
List<String> unmergedPaths = null; | |||
if (merger instanceof ResolveMerger) { | |||
ResolveMerger resolveMerger = (ResolveMerger) merger; | |||
resolveMerger.setContentMergeStrategy(contentStrategy); | |||
resolveMerger.setCommitNames( | |||
new String[] { "BASE", ourName, cherryPickName }); //$NON-NLS-1$ | |||
resolveMerger | |||
.setWorkingTreeIterator(new FileTreeIterator(repo)); | |||
resolveMerger.setBase(srcParent.getTree()); | |||
noProblems = merger.merge(newHead, srcCommit); | |||
failingPaths = resolveMerger.getFailingPaths(); | |||
unmergedPaths = resolveMerger.getUnmergedPaths(); | |||
if (!resolveMerger.getModifiedFiles().isEmpty()) { | |||
repo.fireEvent(new WorkingTreeModifiedEvent( | |||
merger.getModifiedFiles(), null)); | |||
resolveMerger.getModifiedFiles(), null)); | |||
} | |||
} else { | |||
noProblems = merger.merge(newHead, srcCommit); | |||
} | |||
if (noProblems) { | |||
if (AnyObjectId.isEqual(newHead.getTree().getId(), | |||
merger.getResultTreeId())) { | |||
continue; | |||
@@ -153,24 +173,26 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { | |||
} | |||
cherryPickedRefs.add(src); | |||
} else { | |||
if (merger.failed()) { | |||
return new CherryPickResult(merger.getFailingPaths()); | |||
if (failingPaths != null && !failingPaths.isEmpty()) { | |||
return new CherryPickResult(failingPaths); | |||
} | |||
// there are merge conflicts | |||
String message = new MergeMessageFormatter() | |||
String message; | |||
if (unmergedPaths != null) { | |||
message = new MergeMessageFormatter() | |||
.formatWithConflicts(srcCommit.getFullMessage(), | |||
merger.getUnmergedPaths()); | |||
unmergedPaths); | |||
} else { | |||
message = srcCommit.getFullMessage(); | |||
} | |||
if (!noCommit) { | |||
repo.writeCherryPickHead(srcCommit.getId()); | |||
} | |||
repo.writeMergeCommitMsg(message); | |||
repo.fireEvent(new WorkingTreeModifiedEvent( | |||
merger.getModifiedFiles(), null)); | |||
return CherryPickResult.CONFLICT; | |||
} | |||
} | |||
@@ -290,6 +312,22 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { | |||
return this; | |||
} | |||
/** | |||
* Sets the content merge strategy to use if the | |||
* {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or | |||
* "recursive". | |||
* | |||
* @param strategy | |||
* the {@link ContentMergeStrategy} to be used | |||
* @return {@code this} | |||
* @since 5.12 | |||
*/ | |||
public CherryPickCommand setContentMergeStrategy( | |||
ContentMergeStrategy strategy) { | |||
this.contentStrategy = strategy; | |||
return this; | |||
} | |||
/** | |||
* Set the (1-based) parent number to diff against | |||
* |
@@ -1,7 +1,7 @@ | |||
/* | |||
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> | |||
* Copyright (C) 2010-2014, Stefan Lay <stefan.lay@sap.com> | |||
* Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> and others | |||
* Copyright (C) 2010, 2014, Stefan Lay <stefan.lay@sap.com> | |||
* Copyright (C) 2016, 2021 Laurent Delaigue <laurent.delaigue@obeo.fr> and others | |||
* | |||
* This program and the accompanying materials are made available under the | |||
* terms of the Eclipse Distribution License v. 1.0 which is available at | |||
@@ -45,6 +45,7 @@ import org.eclipse.jgit.lib.Ref.Storage; | |||
import org.eclipse.jgit.lib.RefUpdate; | |||
import org.eclipse.jgit.lib.RefUpdate.Result; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||
import org.eclipse.jgit.merge.MergeConfig; | |||
import org.eclipse.jgit.merge.MergeMessageFormatter; | |||
import org.eclipse.jgit.merge.MergeStrategy; | |||
@@ -71,6 +72,8 @@ public class MergeCommand extends GitCommand<MergeResult> { | |||
private MergeStrategy mergeStrategy = MergeStrategy.RECURSIVE; | |||
private ContentMergeStrategy contentStrategy; | |||
private List<Ref> commits = new LinkedList<>(); | |||
private Boolean squash; | |||
@@ -320,6 +323,7 @@ public class MergeCommand extends GitCommand<MergeResult> { | |||
List<String> unmergedPaths = null; | |||
if (merger instanceof ResolveMerger) { | |||
ResolveMerger resolveMerger = (ResolveMerger) merger; | |||
resolveMerger.setContentMergeStrategy(contentStrategy); | |||
resolveMerger.setCommitNames(new String[] { | |||
"BASE", "HEAD", ref.getName() }); //$NON-NLS-1$ //$NON-NLS-2$ | |||
resolveMerger.setWorkingTreeIterator(new FileTreeIterator(repo)); | |||
@@ -472,6 +476,22 @@ public class MergeCommand extends GitCommand<MergeResult> { | |||
return this; | |||
} | |||
/** | |||
* Sets the content merge strategy to use if the | |||
* {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or | |||
* "recursive". | |||
* | |||
* @param strategy | |||
* the {@link ContentMergeStrategy} to be used | |||
* @return {@code this} | |||
* @since 5.12 | |||
*/ | |||
public MergeCommand setContentMergeStrategy(ContentMergeStrategy strategy) { | |||
checkCallable(); | |||
this.contentStrategy = strategy; | |||
return this; | |||
} | |||
/** | |||
* Reference to a commit to be merged with the current head | |||
* |
@@ -1,7 +1,7 @@ | |||
/* | |||
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> | |||
* Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com> | |||
* Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> and others | |||
* Copyright (C) 2016, 2021 Laurent Delaigue <laurent.delaigue@obeo.fr> and others | |||
* | |||
* This program and the accompanying materials are made available under the | |||
* terms of the Eclipse Distribution License v. 1.0 which is available at | |||
@@ -43,6 +43,7 @@ import org.eclipse.jgit.lib.RefUpdate.Result; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.lib.RepositoryState; | |||
import org.eclipse.jgit.lib.SubmoduleConfig.FetchRecurseSubmodulesMode; | |||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||
import org.eclipse.jgit.merge.MergeStrategy; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.eclipse.jgit.revwalk.RevWalk; | |||
@@ -69,6 +70,8 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> { | |||
private MergeStrategy strategy = MergeStrategy.RECURSIVE; | |||
private ContentMergeStrategy contentStrategy; | |||
private TagOpt tagOption; | |||
private FastForwardMode fastForwardMode; | |||
@@ -275,8 +278,7 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> { | |||
JGitText.get().pullTaskName)); | |||
// we check the updates to see which of the updated branches | |||
// corresponds | |||
// to the remote branch name | |||
// corresponds to the remote branch name | |||
AnyObjectId commitToMerge; | |||
if (isRemote) { | |||
Ref r = null; | |||
@@ -354,8 +356,11 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> { | |||
} | |||
RebaseCommand rebase = new RebaseCommand(repo); | |||
RebaseResult rebaseRes = rebase.setUpstream(commitToMerge) | |||
.setUpstreamName(upstreamName).setProgressMonitor(monitor) | |||
.setOperation(Operation.BEGIN).setStrategy(strategy) | |||
.setProgressMonitor(monitor) | |||
.setUpstreamName(upstreamName) | |||
.setOperation(Operation.BEGIN) | |||
.setStrategy(strategy) | |||
.setContentMergeStrategy(contentStrategy) | |||
.setPreserveMerges( | |||
pullRebaseMode == BranchRebaseMode.PRESERVE) | |||
.call(); | |||
@@ -363,7 +368,9 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> { | |||
} else { | |||
MergeCommand merge = new MergeCommand(repo); | |||
MergeResult mergeRes = merge.include(upstreamName, commitToMerge) | |||
.setStrategy(strategy).setProgressMonitor(monitor) | |||
.setProgressMonitor(monitor) | |||
.setStrategy(strategy) | |||
.setContentMergeStrategy(contentStrategy) | |||
.setFastForward(getFastForwardMode()).call(); | |||
monitor.update(1); | |||
result = new PullResult(fetchRes, remote, mergeRes); | |||
@@ -441,6 +448,21 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> { | |||
return this; | |||
} | |||
/** | |||
* Sets the content merge strategy to use if the | |||
* {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or | |||
* "recursive". | |||
* | |||
* @param strategy | |||
* the {@link ContentMergeStrategy} to be used | |||
* @return {@code this} | |||
* @since 5.12 | |||
*/ | |||
public PullCommand setContentMergeStrategy(ContentMergeStrategy strategy) { | |||
this.contentStrategy = strategy; | |||
return this; | |||
} | |||
/** | |||
* Set the specification of annotated tag behavior during fetch | |||
* |
@@ -1,6 +1,6 @@ | |||
/* | |||
* Copyright (C) 2010, 2013 Mathias Kinzler <mathias.kinzler@sap.com> | |||
* Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> and others | |||
* Copyright (C) 2016, 2021 Laurent Delaigue <laurent.delaigue@obeo.fr> and others | |||
* | |||
* This program and the accompanying materials are made available under the | |||
* terms of the Eclipse Distribution License v. 1.0 which is available at | |||
@@ -65,6 +65,7 @@ import org.eclipse.jgit.lib.Ref; | |||
import org.eclipse.jgit.lib.RefUpdate; | |||
import org.eclipse.jgit.lib.RefUpdate.Result; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||
import org.eclipse.jgit.merge.MergeStrategy; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.eclipse.jgit.revwalk.RevSort; | |||
@@ -212,6 +213,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
private MergeStrategy strategy = MergeStrategy.RECURSIVE; | |||
private ContentMergeStrategy contentStrategy; | |||
private boolean preserveMerges = false; | |||
/** | |||
@@ -501,8 +504,11 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
String ourCommitName = getOurCommitName(); | |||
try (Git git = new Git(repo)) { | |||
CherryPickResult cherryPickResult = git.cherryPick() | |||
.include(commitToPick).setOurCommitName(ourCommitName) | |||
.setReflogPrefix(REFLOG_PREFIX).setStrategy(strategy) | |||
.include(commitToPick) | |||
.setOurCommitName(ourCommitName) | |||
.setReflogPrefix(REFLOG_PREFIX) | |||
.setStrategy(strategy) | |||
.setContentMergeStrategy(contentStrategy) | |||
.call(); | |||
switch (cherryPickResult.getStatus()) { | |||
case FAILED: | |||
@@ -556,7 +562,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
.include(commitToPick) | |||
.setOurCommitName(ourCommitName) | |||
.setReflogPrefix(REFLOG_PREFIX) | |||
.setStrategy(strategy); | |||
.setStrategy(strategy) | |||
.setContentMergeStrategy(contentStrategy); | |||
if (isMerge) { | |||
pickCommand.setMainlineParentNumber(1); | |||
// We write a MERGE_HEAD and later commit explicitly | |||
@@ -592,6 +599,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
MergeCommand merge = git.merge() | |||
.setFastForward(MergeCommand.FastForwardMode.NO_FF) | |||
.setProgressMonitor(monitor) | |||
.setStrategy(strategy) | |||
.setContentMergeStrategy(contentStrategy) | |||
.setCommit(false); | |||
for (int i = 1; i < commitToPick.getParentCount(); i++) | |||
merge.include(newParents.get(i)); | |||
@@ -1137,7 +1146,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
} | |||
private List<RevCommit> calculatePickList(RevCommit headCommit) | |||
throws GitAPIException, NoHeadException, IOException { | |||
throws IOException { | |||
List<RevCommit> cherryPickList = new ArrayList<>(); | |||
try (RevWalk r = new RevWalk(repo)) { | |||
r.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true); | |||
@@ -1586,6 +1595,21 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
return this; | |||
} | |||
/** | |||
* Sets the content merge strategy to use if the | |||
* {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or | |||
* "recursive". | |||
* | |||
* @param strategy | |||
* the {@link ContentMergeStrategy} to be used | |||
* @return {@code this} | |||
* @since 5.12 | |||
*/ | |||
public RebaseCommand setContentMergeStrategy(ContentMergeStrategy strategy) { | |||
this.contentStrategy = strategy; | |||
return this; | |||
} | |||
/** | |||
* Whether to preserve merges during rebase | |||
* |
@@ -1,5 +1,5 @@ | |||
/* | |||
* Copyright (C) 2012, 2017 GitHub Inc. and others | |||
* Copyright (C) 2012, 2021 GitHub Inc. and others | |||
* | |||
* This program and the accompanying materials are made available under the | |||
* terms of the Eclipse Distribution License v. 1.0 which is available at | |||
@@ -38,7 +38,9 @@ import org.eclipse.jgit.lib.ObjectId; | |||
import org.eclipse.jgit.lib.ObjectReader; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.lib.RepositoryState; | |||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||
import org.eclipse.jgit.merge.MergeStrategy; | |||
import org.eclipse.jgit.merge.Merger; | |||
import org.eclipse.jgit.merge.ResolveMerger; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.eclipse.jgit.revwalk.RevTree; | |||
@@ -71,6 +73,8 @@ public class StashApplyCommand extends GitCommand<ObjectId> { | |||
private MergeStrategy strategy = MergeStrategy.RECURSIVE; | |||
private ContentMergeStrategy contentStrategy; | |||
/** | |||
* Create command to apply the changes of a stashed commit | |||
* | |||
@@ -166,16 +170,25 @@ public class StashApplyCommand extends GitCommand<ObjectId> { | |||
if (restoreUntracked && stashCommit.getParentCount() == 3) | |||
untrackedCommit = revWalk.parseCommit(stashCommit.getParent(2)); | |||
ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo); | |||
merger.setCommitNames(new String[] { "stashed HEAD", "HEAD", //$NON-NLS-1$ //$NON-NLS-2$ | |||
"stash" }); //$NON-NLS-1$ | |||
merger.setBase(stashHeadCommit); | |||
merger.setWorkingTreeIterator(new FileTreeIterator(repo)); | |||
boolean mergeSucceeded = merger.merge(headCommit, stashCommit); | |||
List<String> modifiedByMerge = merger.getModifiedFiles(); | |||
if (!modifiedByMerge.isEmpty()) { | |||
repo.fireEvent( | |||
new WorkingTreeModifiedEvent(modifiedByMerge, null)); | |||
Merger merger = strategy.newMerger(repo); | |||
boolean mergeSucceeded; | |||
if (merger instanceof ResolveMerger) { | |||
ResolveMerger resolveMerger = (ResolveMerger) merger; | |||
resolveMerger | |||
.setCommitNames(new String[] { "stashed HEAD", "HEAD", //$NON-NLS-1$ //$NON-NLS-2$ | |||
"stash" }); //$NON-NLS-1$ | |||
resolveMerger.setBase(stashHeadCommit); | |||
resolveMerger | |||
.setWorkingTreeIterator(new FileTreeIterator(repo)); | |||
resolveMerger.setContentMergeStrategy(contentStrategy); | |||
mergeSucceeded = resolveMerger.merge(headCommit, stashCommit); | |||
List<String> modifiedByMerge = resolveMerger.getModifiedFiles(); | |||
if (!modifiedByMerge.isEmpty()) { | |||
repo.fireEvent(new WorkingTreeModifiedEvent(modifiedByMerge, | |||
null)); | |||
} | |||
} else { | |||
mergeSucceeded = merger.merge(headCommit, stashCommit); | |||
} | |||
if (mergeSucceeded) { | |||
DirCache dc = repo.lockDirCache(); | |||
@@ -184,11 +197,14 @@ public class StashApplyCommand extends GitCommand<ObjectId> { | |||
dco.setFailOnConflict(true); | |||
dco.checkout(); // Ignoring failed deletes.... | |||
if (restoreIndex) { | |||
ResolveMerger ixMerger = (ResolveMerger) strategy | |||
.newMerger(repo, true); | |||
ixMerger.setCommitNames(new String[] { "stashed HEAD", //$NON-NLS-1$ | |||
"HEAD", "stashed index" }); //$NON-NLS-1$//$NON-NLS-2$ | |||
ixMerger.setBase(stashHeadCommit); | |||
Merger ixMerger = strategy.newMerger(repo, true); | |||
if (ixMerger instanceof ResolveMerger) { | |||
ResolveMerger resolveMerger = (ResolveMerger) ixMerger; | |||
resolveMerger.setCommitNames(new String[] { "stashed HEAD", //$NON-NLS-1$ | |||
"HEAD", "stashed index" }); //$NON-NLS-1$//$NON-NLS-2$ | |||
resolveMerger.setBase(stashHeadCommit); | |||
resolveMerger.setContentMergeStrategy(contentStrategy); | |||
} | |||
boolean ok = ixMerger.merge(headCommit, stashIndexCommit); | |||
if (ok) { | |||
resetIndex(revWalk | |||
@@ -200,16 +216,20 @@ public class StashApplyCommand extends GitCommand<ObjectId> { | |||
} | |||
if (untrackedCommit != null) { | |||
ResolveMerger untrackedMerger = (ResolveMerger) strategy | |||
.newMerger(repo, true); | |||
untrackedMerger.setCommitNames(new String[] { | |||
"null", "HEAD", "untracked files" }); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$ | |||
// There is no common base for HEAD & untracked files | |||
// because the commit for untracked files has no parent. If | |||
// we use stashHeadCommit as common base (as in the other | |||
// merges) we potentially report conflicts for files | |||
// which are not even member of untracked files commit | |||
untrackedMerger.setBase(null); | |||
Merger untrackedMerger = strategy.newMerger(repo, true); | |||
if (untrackedMerger instanceof ResolveMerger) { | |||
ResolveMerger resolveMerger = (ResolveMerger) untrackedMerger; | |||
resolveMerger.setCommitNames(new String[] { "null", "HEAD", //$NON-NLS-1$//$NON-NLS-2$ | |||
"untracked files" }); //$NON-NLS-1$ | |||
// There is no common base for HEAD & untracked files | |||
// because the commit for untracked files has no parent. | |||
// If we use stashHeadCommit as common base (as in the | |||
// other merges) we potentially report conflicts for | |||
// files which are not even member of untracked files | |||
// commit. | |||
resolveMerger.setBase(null); | |||
resolveMerger.setContentMergeStrategy(contentStrategy); | |||
} | |||
boolean ok = untrackedMerger.merge(headCommit, | |||
untrackedCommit); | |||
if (ok) { | |||
@@ -278,6 +298,23 @@ public class StashApplyCommand extends GitCommand<ObjectId> { | |||
return this; | |||
} | |||
/** | |||
* Sets the content merge strategy to use if the | |||
* {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or | |||
* "recursive". | |||
* | |||
* @param strategy | |||
* the {@link ContentMergeStrategy} to be used | |||
* @return {@code this} | |||
* @since 5.12 | |||
*/ | |||
public StashApplyCommand setContentMergeStrategy( | |||
ContentMergeStrategy strategy) { | |||
checkCallable(); | |||
this.contentStrategy = strategy; | |||
return this; | |||
} | |||
/** | |||
* Whether the command should restore untracked files | |||
* |
@@ -0,0 +1,27 @@ | |||
/* | |||
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others | |||
* | |||
* This program and the accompanying materials are made available under the | |||
* terms of the Eclipse Distribution License v. 1.0 which is available at | |||
* https://www.eclipse.org/org/documents/edl-v10.php. | |||
* | |||
* SPDX-License-Identifier: BSD-3-Clause | |||
*/ | |||
package org.eclipse.jgit.merge; | |||
/** | |||
* How to handle content conflicts. | |||
* | |||
* @since 5.12 | |||
*/ | |||
public enum ContentMergeStrategy { | |||
/** Produce a conflict. */ | |||
CONFLICT, | |||
/** Resolve the conflict hunk using the ours version. */ | |||
OURS, | |||
/** Resolve the conflict hunk using the theirs version. */ | |||
THEIRS | |||
} |
@@ -14,6 +14,7 @@ import java.util.ArrayList; | |||
import java.util.Iterator; | |||
import java.util.List; | |||
import org.eclipse.jgit.annotations.NonNull; | |||
import org.eclipse.jgit.diff.DiffAlgorithm; | |||
import org.eclipse.jgit.diff.Edit; | |||
import org.eclipse.jgit.diff.EditList; | |||
@@ -28,8 +29,12 @@ import org.eclipse.jgit.merge.MergeChunk.ConflictState; | |||
* diff algorithm. | |||
*/ | |||
public final class MergeAlgorithm { | |||
private final DiffAlgorithm diffAlg; | |||
@NonNull | |||
private ContentMergeStrategy strategy = ContentMergeStrategy.CONFLICT; | |||
/** | |||
* Creates a new MergeAlgorithm which uses | |||
* {@link org.eclipse.jgit.diff.HistogramDiff} as diff algorithm | |||
@@ -48,6 +53,30 @@ public final class MergeAlgorithm { | |||
this.diffAlg = diff; | |||
} | |||
/** | |||
* Retrieves the {@link ContentMergeStrategy}. | |||
* | |||
* @return the {@link ContentMergeStrategy} in effect | |||
* @since 5.12 | |||
*/ | |||
@NonNull | |||
public ContentMergeStrategy getContentMergeStrategy() { | |||
return strategy; | |||
} | |||
/** | |||
* Sets the {@link ContentMergeStrategy}. | |||
* | |||
* @param strategy | |||
* {@link ContentMergeStrategy} to set; if {@code null}, set | |||
* {@link ContentMergeStrategy#CONFLICT} | |||
* @since 5.12 | |||
*/ | |||
public void setContentMergeStrategy(ContentMergeStrategy strategy) { | |||
this.strategy = strategy == null ? ContentMergeStrategy.CONFLICT | |||
: strategy; | |||
} | |||
// An special edit which acts as a sentinel value by marking the end the | |||
// list of edits | |||
private static final Edit END_EDIT = new Edit(Integer.MAX_VALUE, | |||
@@ -79,29 +108,54 @@ public final class MergeAlgorithm { | |||
if (theirs.size() != 0) { | |||
EditList theirsEdits = diffAlg.diff(cmp, base, theirs); | |||
if (!theirsEdits.isEmpty()) { | |||
// we deleted, they modified -> Let their complete content | |||
// conflict with empty text | |||
result.add(1, 0, 0, ConflictState.FIRST_CONFLICTING_RANGE); | |||
result.add(2, 0, theirs.size(), | |||
ConflictState.NEXT_CONFLICTING_RANGE); | |||
} else | |||
// we deleted, they modified | |||
switch (strategy) { | |||
case OURS: | |||
result.add(1, 0, 0, ConflictState.NO_CONFLICT); | |||
break; | |||
case THEIRS: | |||
result.add(2, 0, theirs.size(), | |||
ConflictState.NO_CONFLICT); | |||
break; | |||
default: | |||
// Let their complete content conflict with empty text | |||
result.add(1, 0, 0, | |||
ConflictState.FIRST_CONFLICTING_RANGE); | |||
result.add(2, 0, theirs.size(), | |||
ConflictState.NEXT_CONFLICTING_RANGE); | |||
break; | |||
} | |||
} else { | |||
// we deleted, they didn't modify -> Let our deletion win | |||
result.add(1, 0, 0, ConflictState.NO_CONFLICT); | |||
} else | |||
} | |||
} else { | |||
// we and they deleted -> return a single chunk of nothing | |||
result.add(1, 0, 0, ConflictState.NO_CONFLICT); | |||
} | |||
return result; | |||
} else if (theirs.size() == 0) { | |||
EditList oursEdits = diffAlg.diff(cmp, base, ours); | |||
if (!oursEdits.isEmpty()) { | |||
// we modified, they deleted -> Let our complete content | |||
// conflict with empty text | |||
result.add(1, 0, ours.size(), | |||
ConflictState.FIRST_CONFLICTING_RANGE); | |||
result.add(2, 0, 0, ConflictState.NEXT_CONFLICTING_RANGE); | |||
} else | |||
// we modified, they deleted | |||
switch (strategy) { | |||
case OURS: | |||
result.add(1, 0, ours.size(), ConflictState.NO_CONFLICT); | |||
break; | |||
case THEIRS: | |||
result.add(2, 0, 0, ConflictState.NO_CONFLICT); | |||
break; | |||
default: | |||
// Let our complete content conflict with empty text | |||
result.add(1, 0, ours.size(), | |||
ConflictState.FIRST_CONFLICTING_RANGE); | |||
result.add(2, 0, 0, ConflictState.NEXT_CONFLICTING_RANGE); | |||
break; | |||
} | |||
} else { | |||
// they deleted, we didn't modify -> Let their deletion win | |||
result.add(2, 0, 0, ConflictState.NO_CONFLICT); | |||
} | |||
return result; | |||
} | |||
@@ -249,12 +303,26 @@ public final class MergeAlgorithm { | |||
// Add the conflict (Only if there is a conflict left to report) | |||
if (minBSize > 0 || BSizeDelta != 0) { | |||
result.add(1, oursBeginB + commonPrefix, oursEndB | |||
- commonSuffix, | |||
ConflictState.FIRST_CONFLICTING_RANGE); | |||
result.add(2, theirsBeginB + commonPrefix, theirsEndB | |||
- commonSuffix, | |||
ConflictState.NEXT_CONFLICTING_RANGE); | |||
switch (strategy) { | |||
case OURS: | |||
result.add(1, oursBeginB + commonPrefix, | |||
oursEndB - commonSuffix, | |||
ConflictState.NO_CONFLICT); | |||
break; | |||
case THEIRS: | |||
result.add(2, theirsBeginB + commonPrefix, | |||
theirsEndB - commonSuffix, | |||
ConflictState.NO_CONFLICT); | |||
break; | |||
default: | |||
result.add(1, oursBeginB + commonPrefix, | |||
oursEndB - commonSuffix, | |||
ConflictState.FIRST_CONFLICTING_RANGE); | |||
result.add(2, theirsBeginB + commonPrefix, | |||
theirsEndB - commonSuffix, | |||
ConflictState.NEXT_CONFLICTING_RANGE); | |||
break; | |||
} | |||
} | |||
// Add the common lines at end of conflict |
@@ -37,6 +37,7 @@ import java.util.LinkedList; | |||
import java.util.List; | |||
import java.util.Map; | |||
import org.eclipse.jgit.annotations.NonNull; | |||
import org.eclipse.jgit.attributes.Attributes; | |||
import org.eclipse.jgit.diff.DiffAlgorithm; | |||
import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm; | |||
@@ -267,6 +268,13 @@ public class ResolveMerger extends ThreeWayMerger { | |||
*/ | |||
private int inCoreLimit; | |||
/** | |||
* The {@link ContentMergeStrategy} to use for "resolve" and "recursive" | |||
* merges. | |||
*/ | |||
@NonNull | |||
private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT; | |||
/** | |||
* Keeps {@link CheckoutMetadata} for {@link #checkout()} and | |||
* {@link #cleanUp()}. | |||
@@ -344,6 +352,29 @@ public class ResolveMerger extends ThreeWayMerger { | |||
dircache = DirCache.newInCore(); | |||
} | |||
/** | |||
* Retrieves the content merge strategy for content conflicts. | |||
* | |||
* @return the {@link ContentMergeStrategy} in effect | |||
* @since 5.12 | |||
*/ | |||
@NonNull | |||
public ContentMergeStrategy getContentMergeStrategy() { | |||
return contentStrategy; | |||
} | |||
/** | |||
* Sets the content merge strategy for content conflicts. | |||
* | |||
* @param strategy | |||
* {@link ContentMergeStrategy} to use | |||
* @since 5.12 | |||
*/ | |||
public void setContentMergeStrategy(ContentMergeStrategy strategy) { | |||
contentStrategy = strategy == null ? ContentMergeStrategy.CONFLICT | |||
: strategy; | |||
} | |||
/** {@inheritDoc} */ | |||
@Override | |||
protected boolean mergeImpl() throws IOException { | |||
@@ -654,7 +685,8 @@ public class ResolveMerger extends ThreeWayMerger { | |||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); | |||
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); | |||
unmergedPaths.add(tw.getPathString()); | |||
mergeResults.put(tw.getPathString(), new MergeResult<>(Collections.<RawText>emptyList())); | |||
mergeResults.put(tw.getPathString(), | |||
new MergeResult<>(Collections.emptyList())); | |||
} | |||
return true; | |||
} | |||
@@ -760,6 +792,19 @@ public class ResolveMerger extends ThreeWayMerger { | |||
unmergedPaths.add(tw.getPathString()); | |||
return true; | |||
} else if (!attributes.canBeContentMerged()) { | |||
// File marked as binary | |||
switch (getContentMergeStrategy()) { | |||
case OURS: | |||
keep(ourDce); | |||
return true; | |||
case THEIRS: | |||
DirCacheEntry theirEntry = add(tw.getRawPath(), theirs, | |||
DirCacheEntry.STAGE_0, EPOCH, 0); | |||
addToCheckout(tw.getPathString(), theirEntry, attributes); | |||
return true; | |||
default: | |||
break; | |||
} | |||
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); | |||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); | |||
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); | |||
@@ -774,8 +819,26 @@ public class ResolveMerger extends ThreeWayMerger { | |||
return false; | |||
} | |||
MergeResult<RawText> result = contentMerge(base, ours, theirs, | |||
attributes); | |||
MergeResult<RawText> result = null; | |||
try { | |||
result = contentMerge(base, ours, theirs, attributes, | |||
getContentMergeStrategy()); | |||
} catch (BinaryBlobException e) { | |||
switch (getContentMergeStrategy()) { | |||
case OURS: | |||
keep(ourDce); | |||
return true; | |||
case THEIRS: | |||
DirCacheEntry theirEntry = add(tw.getRawPath(), theirs, | |||
DirCacheEntry.STAGE_0, EPOCH, 0); | |||
addToCheckout(tw.getPathString(), theirEntry, attributes); | |||
return true; | |||
default: | |||
result = new MergeResult<>(Collections.emptyList()); | |||
result.setContainsConflicts(true); | |||
break; | |||
} | |||
} | |||
if (ignoreConflicts) { | |||
result.setContainsConflicts(false); | |||
} | |||
@@ -802,9 +865,16 @@ public class ResolveMerger extends ThreeWayMerger { | |||
mergeResults.put(tw.getPathString(), result); | |||
unmergedPaths.add(tw.getPathString()); | |||
} else { | |||
MergeResult<RawText> result = contentMerge(base, ours, | |||
theirs, attributes); | |||
// Content merge strategy does not apply to delete-modify | |||
// conflicts! | |||
MergeResult<RawText> result; | |||
try { | |||
result = contentMerge(base, ours, theirs, attributes, | |||
ContentMergeStrategy.CONFLICT); | |||
} catch (BinaryBlobException e) { | |||
result = new MergeResult<>(Collections.emptyList()); | |||
result.setContainsConflicts(true); | |||
} | |||
if (ignoreConflicts) { | |||
// In case a conflict is detected the working tree file | |||
// is again filled with new content (containing conflict | |||
@@ -866,32 +936,26 @@ public class ResolveMerger extends ThreeWayMerger { | |||
* @param ours | |||
* @param theirs | |||
* @param attributes | |||
* @param strategy | |||
* | |||
* @return the result of the content merge | |||
* @throws BinaryBlobException | |||
* if any of the blobs looks like a binary blob | |||
* @throws IOException | |||
*/ | |||
private MergeResult<RawText> contentMerge(CanonicalTreeParser base, | |||
CanonicalTreeParser ours, CanonicalTreeParser theirs, | |||
Attributes attributes) | |||
throws IOException { | |||
RawText baseText; | |||
RawText ourText; | |||
RawText theirsText; | |||
try { | |||
baseText = base == null ? RawText.EMPTY_TEXT : getRawText( | |||
base.getEntryObjectId(), attributes); | |||
ourText = ours == null ? RawText.EMPTY_TEXT : getRawText( | |||
ours.getEntryObjectId(), attributes); | |||
theirsText = theirs == null ? RawText.EMPTY_TEXT : getRawText( | |||
theirs.getEntryObjectId(), attributes); | |||
} catch (BinaryBlobException e) { | |||
MergeResult<RawText> r = new MergeResult<>(Collections.<RawText>emptyList()); | |||
r.setContainsConflicts(true); | |||
return r; | |||
} | |||
return (mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText, | |||
ourText, theirsText)); | |||
Attributes attributes, ContentMergeStrategy strategy) | |||
throws BinaryBlobException, IOException { | |||
RawText baseText = base == null ? RawText.EMPTY_TEXT | |||
: getRawText(base.getEntryObjectId(), attributes); | |||
RawText ourText = ours == null ? RawText.EMPTY_TEXT | |||
: getRawText(ours.getEntryObjectId(), attributes); | |||
RawText theirsText = theirs == null ? RawText.EMPTY_TEXT | |||
: getRawText(theirs.getEntryObjectId(), attributes); | |||
mergeAlgorithm.setContentMergeStrategy(strategy); | |||
return mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText, | |||
ourText, theirsText); | |||
} | |||
private boolean isIndexDirty() { |