aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties3
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java22
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java93
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java376
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java71
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java133
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java70
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java24
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java34
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java34
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java89
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java27
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java106
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java116
15 files changed, 1085 insertions, 114 deletions
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index 83846ee8e9..38deab99a0 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -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.
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
index fdc449e063..ca4877fb34 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
@@ -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);
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
index 991b3ba58a..8e49a76a33 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
@@ -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;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
index 9dd129c335..f4f0ecd689 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
@@ -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);
@@ -280,6 +282,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)) {
RevCommit sideCommit = prepareCherryPick(git);
@@ -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 {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
index 8747c85dec..bc4e9405ea 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
@@ -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;
@@ -306,6 +309,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)) {
writeTrashFile("a", "a");
@@ -790,17 +987,96 @@ public class MergeCommandTest extends RepositoryTestCase {
// 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");
+ 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.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());
}
}
@@ -822,19 +1098,99 @@ public class MergeCommandTest extends RepositoryTestCase {
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");
+ 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.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());
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
index e4af44e6f8..9af77aa3e8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
@@ -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;
@@ -154,6 +156,75 @@ public class PullCommandTest extends RepositoryTestCase {
}
@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();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java
index f109cbf50f..49b31b1c4c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java
@@ -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;
@@ -427,6 +429,135 @@ public class StashApplyCommandTest extends RepositoryTestCase {
}
@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");
git.add().addFilepattern(PATH).call();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
index 5d0154c6dc..7922f9e729 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
@@ -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;
}
}
@@ -291,6 +313,22 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> {
}
/**
+ * 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
*
* @param mainlineParentNumber
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
index d88f4ec561..c611f915ae 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
@@ -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));
@@ -473,6 +477,22 @@ public class MergeCommand extends GitCommand<MergeResult> {
}
/**
+ * 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
*
* @param aCommit
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
index 449250890c..281ecfd011 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
@@ -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);
@@ -442,6 +449,21 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> {
}
/**
+ * 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
*
* @param tagOpt
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
index 836175dcea..a26ffc2e66 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -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);
@@ -1587,6 +1596,21 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
/**
+ * 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
*
* @param preserve
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
index 56b3992fcd..1004d3e50f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
@@ -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) {
@@ -279,6 +299,23 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
}
/**
+ * 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
*
* @param applyUntracked
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java
new file mode 100644
index 0000000000..6d568643d5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java
@@ -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
+} \ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
index 27141c12c4..80607351ae 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
@@ -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
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
index b011258981..7767662867 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -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;
@@ -268,6 +269,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() {