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
metaVar_connProp=conn.prop | metaVar_connProp=conn.prop | ||||
metaVar_diffAlg=ALGORITHM | metaVar_diffAlg=ALGORITHM | ||||
metaVar_directory=DIRECTORY | metaVar_directory=DIRECTORY | ||||
metaVar_extraArgument=ours|theirs | |||||
metaVar_file=FILE | metaVar_file=FILE | ||||
metaVar_filepattern=filepattern | metaVar_filepattern=filepattern | ||||
metaVar_gitDir=GIT_DIR | metaVar_gitDir=GIT_DIR | ||||
treeIsRequired=argument tree is required | treeIsRequired=argument tree is required | ||||
tooManyRefsGiven=Too many refs given | tooManyRefsGiven=Too many refs given | ||||
unknownIoErrorStdout=An unknown I/O error occurred on standard output | unknownIoErrorStdout=An unknown I/O error occurred on standard output | ||||
unknownExtraArgument=unknown extra argument -X {0} specified | |||||
unknownMergeStrategy=unknown merge strategy {0} specified | unknownMergeStrategy=unknown merge strategy {0} specified | ||||
unknownSubcommand=Unknown subcommand: {0} | unknownSubcommand=Unknown subcommand: {0} | ||||
unmergedPaths=Unmerged paths: | unmergedPaths=Unmerged paths: | ||||
usage_Aggressive=This option will cause gc to more aggressively optimize the repository at the expense of taking much more time | 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_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_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 \ | 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.) \ | 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. | and sets up a refspec configuration such that all these refs are overwritten by a git remote update in the target repository. |
import org.eclipse.jgit.lib.Constants; | import org.eclipse.jgit.lib.Constants; | ||||
import org.eclipse.jgit.lib.ObjectId; | import org.eclipse.jgit.lib.ObjectId; | ||||
import org.eclipse.jgit.lib.Ref; | import org.eclipse.jgit.lib.Ref; | ||||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||||
import org.eclipse.jgit.merge.MergeStrategy; | import org.eclipse.jgit.merge.MergeStrategy; | ||||
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | ||||
import org.eclipse.jgit.pgm.internal.CLIText; | import org.eclipse.jgit.pgm.internal.CLIText; | ||||
@Option(name = "-m", usage = "usage_message") | @Option(name = "-m", usage = "usage_message") | ||||
private String 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} */ | /** {@inheritDoc} */ | ||||
@Override | @Override | ||||
protected void run() { | protected void run() { | ||||
Ref oldHead = getOldHead(); | Ref oldHead = getOldHead(); | ||||
MergeResult result; | MergeResult result; | ||||
try (Git git = new Git(db)) { | 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); | .setCommit(!noCommit); | ||||
if (srcRef != null) { | if (srcRef != null) { | ||||
mergeCmd.include(srcRef); | mergeCmd.include(srcRef); |
/***/ public String tooManyRefsGiven; | /***/ public String tooManyRefsGiven; | ||||
/***/ public String treeIsRequired; | /***/ public String treeIsRequired; | ||||
/***/ public char[] unknownIoErrorStdout; | /***/ public char[] unknownIoErrorStdout; | ||||
/***/ public String unknownExtraArgument; | |||||
/***/ public String unknownMergeStrategy; | /***/ public String unknownMergeStrategy; | ||||
/***/ public String unknownSubcommand; | /***/ public String unknownSubcommand; | ||||
/***/ public String unmergedPaths; | /***/ public String unmergedPaths; |
import org.eclipse.jgit.lib.ObjectId; | import org.eclipse.jgit.lib.ObjectId; | ||||
import org.eclipse.jgit.lib.ReflogReader; | import org.eclipse.jgit.lib.ReflogReader; | ||||
import org.eclipse.jgit.lib.RepositoryState; | 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.merge.ResolveMerger.MergeFailureReason; | ||||
import org.eclipse.jgit.revwalk.RevCommit; | import org.eclipse.jgit.revwalk.RevCommit; | ||||
import org.junit.Test; | import org.junit.Test; | ||||
} | } | ||||
@Test | @Test | ||||
public void testCherryPickConflictResolutionNoCOmmit() throws Exception { | |||||
public void testCherryPickConflictResolutionNoCommit() throws Exception { | |||||
Git git = new Git(db); | Git git = new Git(db); | ||||
RevCommit sideCommit = prepareCherryPick(git); | RevCommit sideCommit = prepareCherryPick(git); | ||||
} | } | ||||
} | } | ||||
@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 | @Test | ||||
public void testCherryPickConflictMarkers() throws Exception { | public void testCherryPickConflictMarkers() throws Exception { | ||||
try (Git git = new Git(db)) { | try (Git git = new Git(db)) { | ||||
return sideCommit; | 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, | private void doCherryPickAndCheckResult(final Git git, | ||||
final RevCommit sideCommit, final MergeFailureReason reason) | final RevCommit sideCommit, final MergeFailureReason reason) | ||||
throws Exception { | throws Exception { |
import static org.eclipse.jgit.lib.Constants.R_HEADS; | import static org.eclipse.jgit.lib.Constants.R_HEADS; | ||||
import static org.junit.Assert.assertEquals; | import static org.junit.Assert.assertEquals; | ||||
import static org.junit.Assert.assertFalse; | import static org.junit.Assert.assertFalse; | ||||
import static org.junit.Assert.assertNotNull; | |||||
import static org.junit.Assert.assertNull; | import static org.junit.Assert.assertNull; | ||||
import static org.junit.Assert.assertTrue; | import static org.junit.Assert.assertTrue; | ||||
import static org.junit.Assert.fail; | import static org.junit.Assert.fail; | ||||
import org.eclipse.jgit.api.MergeCommand.FastForwardMode; | import org.eclipse.jgit.api.MergeCommand.FastForwardMode; | ||||
import org.eclipse.jgit.api.MergeResult.MergeStatus; | 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.api.errors.InvalidMergeHeadsException; | ||||
import org.eclipse.jgit.junit.RepositoryTestCase; | import org.eclipse.jgit.junit.RepositoryTestCase; | ||||
import org.eclipse.jgit.junit.TestRepository; | import org.eclipse.jgit.junit.TestRepository; | ||||
import org.eclipse.jgit.lib.Repository; | import org.eclipse.jgit.lib.Repository; | ||||
import org.eclipse.jgit.lib.RepositoryState; | import org.eclipse.jgit.lib.RepositoryState; | ||||
import org.eclipse.jgit.lib.Sets; | import org.eclipse.jgit.lib.Sets; | ||||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||||
import org.eclipse.jgit.merge.MergeStrategy; | import org.eclipse.jgit.merge.MergeStrategy; | ||||
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | ||||
import org.eclipse.jgit.revwalk.RevCommit; | import org.eclipse.jgit.revwalk.RevCommit; | ||||
} | } | ||||
} | } | ||||
@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 | @Test | ||||
public void testMergeTag() throws Exception { | public void testMergeTag() throws Exception { | ||||
try (Git git = new Git(db)) { | try (Git git = new Git(db)) { | ||||
@Test | @Test | ||||
public void testDeletionOnMasterConflict() throws Exception { | 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)) { | try (Git git = new Git(db)) { | ||||
writeTrashFile("a", "1\na\n3\n"); | writeTrashFile("a", "1\na\n3\n"); | ||||
writeTrashFile("b", "1\nb\n3\n"); | writeTrashFile("b", "1\nb\n3\n"); | ||||
// merge side with master | // merge side with master | ||||
MergeResult result = git.merge().include(secondCommit.getId()) | 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()); | 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"))); | assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b"))); | ||||
assertTrue(git.status().call().isClean()); | |||||
} | } | ||||
} | } | ||||
@Test | @Test | ||||
public void testDeletionOnSideConflict() throws Exception { | 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)) { | try (Git git = new Git(db)) { | ||||
writeTrashFile("a", "1\na\n3\n"); | writeTrashFile("a", "1\na\n3\n"); | ||||
writeTrashFile("b", "1\nb\n3\n"); | writeTrashFile("b", "1\nb\n3\n"); | ||||
// merge side with master | // merge side with master | ||||
MergeResult result = git.merge().include(secondCommit.getId()) | 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"))); | 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()); | |||||
} | } | ||||
} | } | ||||
import org.eclipse.jgit.lib.Repository; | import org.eclipse.jgit.lib.Repository; | ||||
import org.eclipse.jgit.lib.RepositoryState; | import org.eclipse.jgit.lib.RepositoryState; | ||||
import org.eclipse.jgit.lib.StoredConfig; | 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.RevCommit; | ||||
import org.eclipse.jgit.revwalk.RevSort; | import org.eclipse.jgit.revwalk.RevSort; | ||||
import org.eclipse.jgit.revwalk.RevWalk; | import org.eclipse.jgit.revwalk.RevWalk; | ||||
.getRepositoryState()); | .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 | @Test | ||||
public void testPullWithUntrackedStash() throws Exception { | public void testPullWithUntrackedStash() throws Exception { | ||||
target.pull().call(); | target.pull().call(); |
/* | /* | ||||
* 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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
import org.eclipse.jgit.junit.RepositoryTestCase; | import org.eclipse.jgit.junit.RepositoryTestCase; | ||||
import org.eclipse.jgit.lib.ObjectId; | import org.eclipse.jgit.lib.ObjectId; | ||||
import org.eclipse.jgit.lib.Repository; | 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.RevCommit; | ||||
import org.eclipse.jgit.util.FileUtils; | import org.eclipse.jgit.util.FileUtils; | ||||
import org.junit.After; | import org.junit.After; | ||||
read(PATH)); | 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 | @Test | ||||
public void stashedApplyOnOtherBranch() throws Exception { | public void stashedApplyOnOtherBranch() throws Exception { | ||||
writeTrashFile(PATH, "content\nmore content\n"); | writeTrashFile(PATH, "content\nmore content\n"); |
/* | /* | ||||
* 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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
import java.text.MessageFormat; | import java.text.MessageFormat; | ||||
import java.util.LinkedList; | import java.util.LinkedList; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | |||||
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; | import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; | ||||
import org.eclipse.jgit.api.errors.GitAPIException; | import org.eclipse.jgit.api.errors.GitAPIException; | ||||
import org.eclipse.jgit.lib.Ref; | import org.eclipse.jgit.lib.Ref; | ||||
import org.eclipse.jgit.lib.Ref.Storage; | import org.eclipse.jgit.lib.Ref.Storage; | ||||
import org.eclipse.jgit.lib.Repository; | import org.eclipse.jgit.lib.Repository; | ||||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||||
import org.eclipse.jgit.merge.MergeMessageFormatter; | import org.eclipse.jgit.merge.MergeMessageFormatter; | ||||
import org.eclipse.jgit.merge.MergeStrategy; | import org.eclipse.jgit.merge.MergeStrategy; | ||||
import org.eclipse.jgit.merge.Merger; | |||||
import org.eclipse.jgit.merge.ResolveMerger; | import org.eclipse.jgit.merge.ResolveMerger; | ||||
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | |||||
import org.eclipse.jgit.revwalk.RevCommit; | import org.eclipse.jgit.revwalk.RevCommit; | ||||
import org.eclipse.jgit.revwalk.RevWalk; | import org.eclipse.jgit.revwalk.RevWalk; | ||||
import org.eclipse.jgit.treewalk.FileTreeIterator; | import org.eclipse.jgit.treewalk.FileTreeIterator; | ||||
private MergeStrategy strategy = MergeStrategy.RECURSIVE; | private MergeStrategy strategy = MergeStrategy.RECURSIVE; | ||||
private ContentMergeStrategy contentStrategy; | |||||
private Integer mainlineParentNumber; | private Integer mainlineParentNumber; | ||||
private boolean noCommit = false; | private boolean noCommit = false; | ||||
String cherryPickName = srcCommit.getId().abbreviate(7).name() | String cherryPickName = srcCommit.getId().abbreviate(7).name() | ||||
+ " " + srcCommit.getShortMessage(); //$NON-NLS-1$ | + " " + 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( | repo.fireEvent(new WorkingTreeModifiedEvent( | ||||
merger.getModifiedFiles(), null)); | |||||
resolveMerger.getModifiedFiles(), null)); | |||||
} | } | ||||
} else { | |||||
noProblems = merger.merge(newHead, srcCommit); | |||||
} | |||||
if (noProblems) { | |||||
if (AnyObjectId.isEqual(newHead.getTree().getId(), | if (AnyObjectId.isEqual(newHead.getTree().getId(), | ||||
merger.getResultTreeId())) { | merger.getResultTreeId())) { | ||||
continue; | continue; | ||||
} | } | ||||
cherryPickedRefs.add(src); | cherryPickedRefs.add(src); | ||||
} else { | } else { | ||||
if (merger.failed()) { | |||||
return new CherryPickResult(merger.getFailingPaths()); | |||||
if (failingPaths != null && !failingPaths.isEmpty()) { | |||||
return new CherryPickResult(failingPaths); | |||||
} | } | ||||
// there are merge conflicts | // there are merge conflicts | ||||
String message = new MergeMessageFormatter() | |||||
String message; | |||||
if (unmergedPaths != null) { | |||||
message = new MergeMessageFormatter() | |||||
.formatWithConflicts(srcCommit.getFullMessage(), | .formatWithConflicts(srcCommit.getFullMessage(), | ||||
merger.getUnmergedPaths()); | |||||
unmergedPaths); | |||||
} else { | |||||
message = srcCommit.getFullMessage(); | |||||
} | |||||
if (!noCommit) { | if (!noCommit) { | ||||
repo.writeCherryPickHead(srcCommit.getId()); | repo.writeCherryPickHead(srcCommit.getId()); | ||||
} | } | ||||
repo.writeMergeCommitMsg(message); | repo.writeMergeCommitMsg(message); | ||||
repo.fireEvent(new WorkingTreeModifiedEvent( | |||||
merger.getModifiedFiles(), null)); | |||||
return CherryPickResult.CONFLICT; | return CherryPickResult.CONFLICT; | ||||
} | } | ||||
} | } | ||||
return this; | 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 | * Set the (1-based) parent number to diff against | ||||
* | * |
/* | /* | ||||
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> | * 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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
import org.eclipse.jgit.lib.RefUpdate; | import org.eclipse.jgit.lib.RefUpdate; | ||||
import org.eclipse.jgit.lib.RefUpdate.Result; | import org.eclipse.jgit.lib.RefUpdate.Result; | ||||
import org.eclipse.jgit.lib.Repository; | import org.eclipse.jgit.lib.Repository; | ||||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||||
import org.eclipse.jgit.merge.MergeConfig; | import org.eclipse.jgit.merge.MergeConfig; | ||||
import org.eclipse.jgit.merge.MergeMessageFormatter; | import org.eclipse.jgit.merge.MergeMessageFormatter; | ||||
import org.eclipse.jgit.merge.MergeStrategy; | import org.eclipse.jgit.merge.MergeStrategy; | ||||
private MergeStrategy mergeStrategy = MergeStrategy.RECURSIVE; | private MergeStrategy mergeStrategy = MergeStrategy.RECURSIVE; | ||||
private ContentMergeStrategy contentStrategy; | |||||
private List<Ref> commits = new LinkedList<>(); | private List<Ref> commits = new LinkedList<>(); | ||||
private Boolean squash; | private Boolean squash; | ||||
List<String> unmergedPaths = null; | List<String> unmergedPaths = null; | ||||
if (merger instanceof ResolveMerger) { | if (merger instanceof ResolveMerger) { | ||||
ResolveMerger resolveMerger = (ResolveMerger) merger; | ResolveMerger resolveMerger = (ResolveMerger) merger; | ||||
resolveMerger.setContentMergeStrategy(contentStrategy); | |||||
resolveMerger.setCommitNames(new String[] { | resolveMerger.setCommitNames(new String[] { | ||||
"BASE", "HEAD", ref.getName() }); //$NON-NLS-1$ //$NON-NLS-2$ | "BASE", "HEAD", ref.getName() }); //$NON-NLS-1$ //$NON-NLS-2$ | ||||
resolveMerger.setWorkingTreeIterator(new FileTreeIterator(repo)); | resolveMerger.setWorkingTreeIterator(new FileTreeIterator(repo)); | ||||
return this; | 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 | * Reference to a commit to be merged with the current head | ||||
* | * |
/* | /* | ||||
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> | * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> | ||||
* Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
import org.eclipse.jgit.lib.Repository; | import org.eclipse.jgit.lib.Repository; | ||||
import org.eclipse.jgit.lib.RepositoryState; | import org.eclipse.jgit.lib.RepositoryState; | ||||
import org.eclipse.jgit.lib.SubmoduleConfig.FetchRecurseSubmodulesMode; | import org.eclipse.jgit.lib.SubmoduleConfig.FetchRecurseSubmodulesMode; | ||||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||||
import org.eclipse.jgit.merge.MergeStrategy; | import org.eclipse.jgit.merge.MergeStrategy; | ||||
import org.eclipse.jgit.revwalk.RevCommit; | import org.eclipse.jgit.revwalk.RevCommit; | ||||
import org.eclipse.jgit.revwalk.RevWalk; | import org.eclipse.jgit.revwalk.RevWalk; | ||||
private MergeStrategy strategy = MergeStrategy.RECURSIVE; | private MergeStrategy strategy = MergeStrategy.RECURSIVE; | ||||
private ContentMergeStrategy contentStrategy; | |||||
private TagOpt tagOption; | private TagOpt tagOption; | ||||
private FastForwardMode fastForwardMode; | private FastForwardMode fastForwardMode; | ||||
JGitText.get().pullTaskName)); | JGitText.get().pullTaskName)); | ||||
// we check the updates to see which of the updated branches | // 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; | AnyObjectId commitToMerge; | ||||
if (isRemote) { | if (isRemote) { | ||||
Ref r = null; | Ref r = null; | ||||
} | } | ||||
RebaseCommand rebase = new RebaseCommand(repo); | RebaseCommand rebase = new RebaseCommand(repo); | ||||
RebaseResult rebaseRes = rebase.setUpstream(commitToMerge) | 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( | .setPreserveMerges( | ||||
pullRebaseMode == BranchRebaseMode.PRESERVE) | pullRebaseMode == BranchRebaseMode.PRESERVE) | ||||
.call(); | .call(); | ||||
} else { | } else { | ||||
MergeCommand merge = new MergeCommand(repo); | MergeCommand merge = new MergeCommand(repo); | ||||
MergeResult mergeRes = merge.include(upstreamName, commitToMerge) | MergeResult mergeRes = merge.include(upstreamName, commitToMerge) | ||||
.setStrategy(strategy).setProgressMonitor(monitor) | |||||
.setProgressMonitor(monitor) | |||||
.setStrategy(strategy) | |||||
.setContentMergeStrategy(contentStrategy) | |||||
.setFastForward(getFastForwardMode()).call(); | .setFastForward(getFastForwardMode()).call(); | ||||
monitor.update(1); | monitor.update(1); | ||||
result = new PullResult(fetchRes, remote, mergeRes); | result = new PullResult(fetchRes, remote, mergeRes); | ||||
return this; | 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 | * Set the specification of annotated tag behavior during fetch | ||||
* | * |
/* | /* | ||||
* Copyright (C) 2010, 2013 Mathias Kinzler <mathias.kinzler@sap.com> | * 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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
import org.eclipse.jgit.lib.RefUpdate; | import org.eclipse.jgit.lib.RefUpdate; | ||||
import org.eclipse.jgit.lib.RefUpdate.Result; | import org.eclipse.jgit.lib.RefUpdate.Result; | ||||
import org.eclipse.jgit.lib.Repository; | import org.eclipse.jgit.lib.Repository; | ||||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||||
import org.eclipse.jgit.merge.MergeStrategy; | import org.eclipse.jgit.merge.MergeStrategy; | ||||
import org.eclipse.jgit.revwalk.RevCommit; | import org.eclipse.jgit.revwalk.RevCommit; | ||||
import org.eclipse.jgit.revwalk.RevSort; | import org.eclipse.jgit.revwalk.RevSort; | ||||
private MergeStrategy strategy = MergeStrategy.RECURSIVE; | private MergeStrategy strategy = MergeStrategy.RECURSIVE; | ||||
private ContentMergeStrategy contentStrategy; | |||||
private boolean preserveMerges = false; | private boolean preserveMerges = false; | ||||
/** | /** | ||||
String ourCommitName = getOurCommitName(); | String ourCommitName = getOurCommitName(); | ||||
try (Git git = new Git(repo)) { | try (Git git = new Git(repo)) { | ||||
CherryPickResult cherryPickResult = git.cherryPick() | 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(); | .call(); | ||||
switch (cherryPickResult.getStatus()) { | switch (cherryPickResult.getStatus()) { | ||||
case FAILED: | case FAILED: | ||||
.include(commitToPick) | .include(commitToPick) | ||||
.setOurCommitName(ourCommitName) | .setOurCommitName(ourCommitName) | ||||
.setReflogPrefix(REFLOG_PREFIX) | .setReflogPrefix(REFLOG_PREFIX) | ||||
.setStrategy(strategy); | |||||
.setStrategy(strategy) | |||||
.setContentMergeStrategy(contentStrategy); | |||||
if (isMerge) { | if (isMerge) { | ||||
pickCommand.setMainlineParentNumber(1); | pickCommand.setMainlineParentNumber(1); | ||||
// We write a MERGE_HEAD and later commit explicitly | // We write a MERGE_HEAD and later commit explicitly | ||||
MergeCommand merge = git.merge() | MergeCommand merge = git.merge() | ||||
.setFastForward(MergeCommand.FastForwardMode.NO_FF) | .setFastForward(MergeCommand.FastForwardMode.NO_FF) | ||||
.setProgressMonitor(monitor) | .setProgressMonitor(monitor) | ||||
.setStrategy(strategy) | |||||
.setContentMergeStrategy(contentStrategy) | |||||
.setCommit(false); | .setCommit(false); | ||||
for (int i = 1; i < commitToPick.getParentCount(); i++) | for (int i = 1; i < commitToPick.getParentCount(); i++) | ||||
merge.include(newParents.get(i)); | merge.include(newParents.get(i)); | ||||
} | } | ||||
private List<RevCommit> calculatePickList(RevCommit headCommit) | private List<RevCommit> calculatePickList(RevCommit headCommit) | ||||
throws GitAPIException, NoHeadException, IOException { | |||||
throws IOException { | |||||
List<RevCommit> cherryPickList = new ArrayList<>(); | List<RevCommit> cherryPickList = new ArrayList<>(); | ||||
try (RevWalk r = new RevWalk(repo)) { | try (RevWalk r = new RevWalk(repo)) { | ||||
r.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true); | r.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true); | ||||
return this; | 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 | * Whether to preserve merges during rebase | ||||
* | * |
/* | /* | ||||
* 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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
import org.eclipse.jgit.lib.ObjectReader; | import org.eclipse.jgit.lib.ObjectReader; | ||||
import org.eclipse.jgit.lib.Repository; | import org.eclipse.jgit.lib.Repository; | ||||
import org.eclipse.jgit.lib.RepositoryState; | import org.eclipse.jgit.lib.RepositoryState; | ||||
import org.eclipse.jgit.merge.ContentMergeStrategy; | |||||
import org.eclipse.jgit.merge.MergeStrategy; | import org.eclipse.jgit.merge.MergeStrategy; | ||||
import org.eclipse.jgit.merge.Merger; | |||||
import org.eclipse.jgit.merge.ResolveMerger; | import org.eclipse.jgit.merge.ResolveMerger; | ||||
import org.eclipse.jgit.revwalk.RevCommit; | import org.eclipse.jgit.revwalk.RevCommit; | ||||
import org.eclipse.jgit.revwalk.RevTree; | import org.eclipse.jgit.revwalk.RevTree; | ||||
private MergeStrategy strategy = MergeStrategy.RECURSIVE; | private MergeStrategy strategy = MergeStrategy.RECURSIVE; | ||||
private ContentMergeStrategy contentStrategy; | |||||
/** | /** | ||||
* Create command to apply the changes of a stashed commit | * Create command to apply the changes of a stashed commit | ||||
* | * | ||||
if (restoreUntracked && stashCommit.getParentCount() == 3) | if (restoreUntracked && stashCommit.getParentCount() == 3) | ||||
untrackedCommit = revWalk.parseCommit(stashCommit.getParent(2)); | 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) { | if (mergeSucceeded) { | ||||
DirCache dc = repo.lockDirCache(); | DirCache dc = repo.lockDirCache(); | ||||
dco.setFailOnConflict(true); | dco.setFailOnConflict(true); | ||||
dco.checkout(); // Ignoring failed deletes.... | dco.checkout(); // Ignoring failed deletes.... | ||||
if (restoreIndex) { | 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); | boolean ok = ixMerger.merge(headCommit, stashIndexCommit); | ||||
if (ok) { | if (ok) { | ||||
resetIndex(revWalk | resetIndex(revWalk | ||||
} | } | ||||
if (untrackedCommit != null) { | 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, | boolean ok = untrackedMerger.merge(headCommit, | ||||
untrackedCommit); | untrackedCommit); | ||||
if (ok) { | if (ok) { | ||||
return this; | 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 | * Whether the command should restore untracked files | ||||
* | * |
/* | |||||
* 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 | |||||
} |
import java.util.Iterator; | import java.util.Iterator; | ||||
import java.util.List; | import java.util.List; | ||||
import org.eclipse.jgit.annotations.NonNull; | |||||
import org.eclipse.jgit.diff.DiffAlgorithm; | import org.eclipse.jgit.diff.DiffAlgorithm; | ||||
import org.eclipse.jgit.diff.Edit; | import org.eclipse.jgit.diff.Edit; | ||||
import org.eclipse.jgit.diff.EditList; | import org.eclipse.jgit.diff.EditList; | ||||
* diff algorithm. | * diff algorithm. | ||||
*/ | */ | ||||
public final class MergeAlgorithm { | public final class MergeAlgorithm { | ||||
private final DiffAlgorithm diffAlg; | private final DiffAlgorithm diffAlg; | ||||
@NonNull | |||||
private ContentMergeStrategy strategy = ContentMergeStrategy.CONFLICT; | |||||
/** | /** | ||||
* Creates a new MergeAlgorithm which uses | * Creates a new MergeAlgorithm which uses | ||||
* {@link org.eclipse.jgit.diff.HistogramDiff} as diff algorithm | * {@link org.eclipse.jgit.diff.HistogramDiff} as diff algorithm | ||||
this.diffAlg = diff; | 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 | // An special edit which acts as a sentinel value by marking the end the | ||||
// list of edits | // list of edits | ||||
private static final Edit END_EDIT = new Edit(Integer.MAX_VALUE, | private static final Edit END_EDIT = new Edit(Integer.MAX_VALUE, | ||||
if (theirs.size() != 0) { | if (theirs.size() != 0) { | ||||
EditList theirsEdits = diffAlg.diff(cmp, base, theirs); | EditList theirsEdits = diffAlg.diff(cmp, base, theirs); | ||||
if (!theirsEdits.isEmpty()) { | 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 | // we deleted, they didn't modify -> Let our deletion win | ||||
result.add(1, 0, 0, ConflictState.NO_CONFLICT); | result.add(1, 0, 0, ConflictState.NO_CONFLICT); | ||||
} else | |||||
} | |||||
} else { | |||||
// we and they deleted -> return a single chunk of nothing | // we and they deleted -> return a single chunk of nothing | ||||
result.add(1, 0, 0, ConflictState.NO_CONFLICT); | result.add(1, 0, 0, ConflictState.NO_CONFLICT); | ||||
} | |||||
return result; | return result; | ||||
} else if (theirs.size() == 0) { | } else if (theirs.size() == 0) { | ||||
EditList oursEdits = diffAlg.diff(cmp, base, ours); | EditList oursEdits = diffAlg.diff(cmp, base, ours); | ||||
if (!oursEdits.isEmpty()) { | 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 | // they deleted, we didn't modify -> Let their deletion win | ||||
result.add(2, 0, 0, ConflictState.NO_CONFLICT); | result.add(2, 0, 0, ConflictState.NO_CONFLICT); | ||||
} | |||||
return result; | return result; | ||||
} | } | ||||
// Add the conflict (Only if there is a conflict left to report) | // Add the conflict (Only if there is a conflict left to report) | ||||
if (minBSize > 0 || BSizeDelta != 0) { | 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 | // Add the common lines at end of conflict |
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | import java.util.Map; | ||||
import org.eclipse.jgit.annotations.NonNull; | |||||
import org.eclipse.jgit.attributes.Attributes; | import org.eclipse.jgit.attributes.Attributes; | ||||
import org.eclipse.jgit.diff.DiffAlgorithm; | import org.eclipse.jgit.diff.DiffAlgorithm; | ||||
import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm; | import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm; | ||||
*/ | */ | ||||
private int inCoreLimit; | 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 | * Keeps {@link CheckoutMetadata} for {@link #checkout()} and | ||||
* {@link #cleanUp()}. | * {@link #cleanUp()}. | ||||
dircache = DirCache.newInCore(); | 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} */ | /** {@inheritDoc} */ | ||||
@Override | @Override | ||||
protected boolean mergeImpl() throws IOException { | protected boolean mergeImpl() throws IOException { | ||||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); | add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); | ||||
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); | add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); | ||||
unmergedPaths.add(tw.getPathString()); | unmergedPaths.add(tw.getPathString()); | ||||
mergeResults.put(tw.getPathString(), new MergeResult<>(Collections.<RawText>emptyList())); | |||||
mergeResults.put(tw.getPathString(), | |||||
new MergeResult<>(Collections.emptyList())); | |||||
} | } | ||||
return true; | return true; | ||||
} | } | ||||
unmergedPaths.add(tw.getPathString()); | unmergedPaths.add(tw.getPathString()); | ||||
return true; | return true; | ||||
} else if (!attributes.canBeContentMerged()) { | } 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(), base, DirCacheEntry.STAGE_1, EPOCH, 0); | ||||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); | add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); | ||||
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); | add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); | ||||
return false; | 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) { | if (ignoreConflicts) { | ||||
result.setContainsConflicts(false); | result.setContainsConflicts(false); | ||||
} | } | ||||
mergeResults.put(tw.getPathString(), result); | mergeResults.put(tw.getPathString(), result); | ||||
unmergedPaths.add(tw.getPathString()); | unmergedPaths.add(tw.getPathString()); | ||||
} else { | } 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) { | if (ignoreConflicts) { | ||||
// In case a conflict is detected the working tree file | // In case a conflict is detected the working tree file | ||||
// is again filled with new content (containing conflict | // is again filled with new content (containing conflict | ||||
* @param ours | * @param ours | ||||
* @param theirs | * @param theirs | ||||
* @param attributes | * @param attributes | ||||
* @param strategy | |||||
* | * | ||||
* @return the result of the content merge | * @return the result of the content merge | ||||
* @throws BinaryBlobException | |||||
* if any of the blobs looks like a binary blob | |||||
* @throws IOException | * @throws IOException | ||||
*/ | */ | ||||
private MergeResult<RawText> contentMerge(CanonicalTreeParser base, | private MergeResult<RawText> contentMerge(CanonicalTreeParser base, | ||||
CanonicalTreeParser ours, CanonicalTreeParser theirs, | 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() { | private boolean isIndexDirty() { |