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 ccb1672dd..2def86cf1 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 @@ -45,20 +45,20 @@ package org.eclipse.jgit.api; import java.io.File; import java.io.IOException; +import java.util.Iterator; -import org.eclipse.jgit.errors.CheckoutConflictException; -import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.api.MergeResult.MergeStatus; +import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.GitIndex; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.lib.RepositoryTestCase; -import org.eclipse.jgit.lib.WorkDirCheckout; -import org.eclipse.jgit.lib.GitIndex.Entry; +import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; public class MergeCommandTest extends RepositoryTestCase { - public void testMergeInItself() throws Exception { Git git = new Git(db); git.commit().setMessage("initial commit").call(); @@ -97,13 +97,15 @@ public class MergeCommandTest extends RepositoryTestCase { public void testFastForwardWithFiles() throws Exception { Git git = new Git(db); - addNewFileToIndex("file1"); + writeTrashFile("file1", "file1"); + git.add().addFilepattern("file1").call(); RevCommit first = git.commit().setMessage("initial commit").call(); assertTrue(new File(db.getWorkTree(), "file1").exists()); createBranch(first, "refs/heads/branch1"); - addNewFileToIndex("file2"); + writeTrashFile("file2", "file2"); + git.add().addFilepattern("file2").call(); RevCommit second = git.commit().setMessage("second commit").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); @@ -121,14 +123,17 @@ public class MergeCommandTest extends RepositoryTestCase { public void testMultipleHeads() throws Exception { Git git = new Git(db); - addNewFileToIndex("file1"); + writeTrashFile("file1", "file1"); + git.add().addFilepattern("file1").call(); RevCommit first = git.commit().setMessage("initial commit").call(); createBranch(first, "refs/heads/branch1"); - addNewFileToIndex("file2"); + writeTrashFile("file2", "file2"); + git.add().addFilepattern("file2").call(); RevCommit second = git.commit().setMessage("second commit").call(); - addNewFileToIndex("file3"); + writeTrashFile("file3", "file3"); + git.add().addFilepattern("file3").call(); git.commit().setMessage("third commit").call(); checkoutBranch("refs/heads/branch1"); @@ -142,42 +147,265 @@ public class MergeCommandTest extends RepositoryTestCase { merge.call(); fail("Expected exception not thrown when merging multiple heads"); } catch (InvalidMergeHeadsException e) { + // expected this exception } } + + public void testContentMerge() throws Exception { + 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\n"); + writeTrashFile("b", "1\nb(side)\n3\n"); + git.add().addFilepattern("a").addFilepattern("b").call(); + RevCommit secondCommit = git.commit().setMessage("side").call(); + + assertEquals("1\nb(side)\n3\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).call(); + assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus()); + + assertEquals( + "1\n<<<<<<< HEAD\na(main)\n=======\na(side)\n>>>>>>> 86503e7e397465588cc267b65d778538bffccb83\n3\n", + read(new File(db.getWorkTree(), "a"))); + assertEquals("1\nb(side)\n3\n", read(new File(db.getWorkTree(), "b"))); + assertEquals("1\nc(main)\n3\n", + read(new File(db.getWorkTree(), "c/c/c"))); + + assertEquals(1, result.getConflicts().size()); + assertEquals(3, result.getConflicts().get("a")[0].length); + + assertEquals(RepositoryState.MERGING, db.getRepositoryState()); + } + + public void testSuccessfulContentMerge() throws Exception { + 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(side)\na\n3\n"); + writeTrashFile("b", "1\nb(side)\n3\n"); + git.add().addFilepattern("a").addFilepattern("b").call(); + RevCommit secondCommit = git.commit().setMessage("side").call(); + + assertEquals("1\nb(side)\n3\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\n3(main)\n"); + writeTrashFile("c/c/c", "1\nc(main)\n3\n"); + git.add().addFilepattern("a").addFilepattern("c/c/c").call(); + RevCommit thirdCommit = git.commit().setMessage("main").call(); + + MergeResult result = git.merge().include(secondCommit.getId()) + .setStrategy(MergeStrategy.RESOLVE).call(); + assertEquals(MergeStatus.MERGED, result.getMergeStatus()); + + assertEquals("1(side)\na\n3(main)\n", read(new File(db.getWorkTree(), + "a"))); + assertEquals("1\nb(side)\n3\n", read(new File(db.getWorkTree(), "b"))); + assertEquals("1\nc(main)\n3\n", read(new File(db.getWorkTree(), + "c/c/c"))); + + assertEquals(null, result.getConflicts()); + + assertTrue(2 == result.getMergedCommits().length); + assertEquals(thirdCommit, result.getMergedCommits()[0]); + assertEquals(secondCommit, result.getMergedCommits()[1]); + + Iterator it = git.log().call().iterator(); + RevCommit newHead = it.next(); + assertEquals(newHead, result.getNewHead()); + assertEquals(2, newHead.getParentCount()); + assertEquals(thirdCommit, newHead.getParent(0)); + assertEquals(secondCommit, newHead.getParent(1)); + assertEquals( + "merging 3fa334456d236a92db020289fe0bf481d91777b4 into HEAD", + newHead.getFullMessage()); + // @TODO fix me + assertEquals(RepositoryState.SAFE, db.getRepositoryState()); + // test index state + } + + public void testSuccessfulContentMergeAndDirtyworkingTree() + throws Exception { + Git git = new Git(db); + + writeTrashFile("a", "1\na\n3\n"); + writeTrashFile("b", "1\nb\n3\n"); + writeTrashFile("d", "1\nd\n3\n"); + writeTrashFile("c/c/c", "1\nc\n3\n"); + git.add().addFilepattern("a").addFilepattern("b") + .addFilepattern("c/c/c").addFilepattern("d").call(); + RevCommit initialCommit = git.commit().setMessage("initial").call(); + + createBranch(initialCommit, "refs/heads/side"); + checkoutBranch("refs/heads/side"); + + writeTrashFile("a", "1(side)\na\n3\n"); + writeTrashFile("b", "1\nb(side)\n3\n"); + git.add().addFilepattern("a").addFilepattern("b").call(); + RevCommit secondCommit = git.commit().setMessage("side").call(); + + assertEquals("1\nb(side)\n3\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\n3(main)\n"); + writeTrashFile("c/c/c", "1\nc(main)\n3\n"); + git.add().addFilepattern("a").addFilepattern("c/c/c").call(); + RevCommit thirdCommit = git.commit().setMessage("main").call(); + + writeTrashFile("d", "--- dirty ---"); + MergeResult result = git.merge().include(secondCommit.getId()) + .setStrategy(MergeStrategy.RESOLVE).call(); + assertEquals(MergeStatus.MERGED, result.getMergeStatus()); + + assertEquals("1(side)\na\n3(main)\n", read(new File(db.getWorkTree(), + "a"))); + assertEquals("1\nb(side)\n3\n", read(new File(db.getWorkTree(), "b"))); + assertEquals("1\nc(main)\n3\n", read(new File(db.getWorkTree(), + "c/c/c"))); + + assertEquals(null, result.getConflicts()); + + assertTrue(2 == result.getMergedCommits().length); + assertEquals(thirdCommit, result.getMergedCommits()[0]); + assertEquals(secondCommit, result.getMergedCommits()[1]); + + Iterator it = git.log().call().iterator(); + RevCommit newHead = it.next(); + assertEquals(newHead, result.getNewHead()); + assertEquals(2, newHead.getParentCount()); + assertEquals(thirdCommit, newHead.getParent(0)); + assertEquals(secondCommit, newHead.getParent(1)); + assertEquals( + "merging 064d54d98a4cdb0fed1802a21c656bfda67fe879 into HEAD", + newHead.getFullMessage()); + + assertEquals(RepositoryState.SAFE, db.getRepositoryState()); + } + + public void testMergeFailingWithDirtyWorkingTree() throws Exception { + 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(); + + createBranch(initialCommit, "refs/heads/side"); + checkoutBranch("refs/heads/side"); + + writeTrashFile("a", "1(side)\na\n3\n"); + writeTrashFile("b", "1\nb(side)\n3\n"); + git.add().addFilepattern("a").addFilepattern("b").call(); + RevCommit secondCommit = git.commit().setMessage("side").call(); + + assertEquals("1\nb(side)\n3\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\n3(main)\n"); + git.add().addFilepattern("a").call(); + git.commit().setMessage("main").call(); + + writeTrashFile("a", "--- dirty ---"); + MergeResult result = git.merge().include(secondCommit.getId()) + .setStrategy(MergeStrategy.RESOLVE).call(); + + assertEquals(MergeStatus.FAILED, result.getMergeStatus()); + + assertEquals("--- dirty ---", read(new File(db.getWorkTree(), "a"))); + assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b"))); + + assertEquals(null, result.getConflicts()); + + assertEquals(RepositoryState.SAFE, db.getRepositoryState()); + } + + public void testMergeConflictFileFolder() throws Exception { + 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(); + + createBranch(initialCommit, "refs/heads/side"); + checkoutBranch("refs/heads/side"); + + writeTrashFile("c/c/c", "1\nc(side)\n3\n"); + writeTrashFile("d", "1\nd(side)\n3\n"); + git.add().addFilepattern("c/c/c").addFilepattern("d").call(); + RevCommit secondCommit = git.commit().setMessage("side").call(); + + checkoutBranch("refs/heads/master"); + + writeTrashFile("c", "1\nc(main)\n3\n"); + writeTrashFile("d/d/d", "1\nd(main)\n3\n"); + git.add().addFilepattern("c").addFilepattern("d/d/d").call(); + git.commit().setMessage("main").call(); + + MergeResult result = git.merge().include(secondCommit.getId()) + .setStrategy(MergeStrategy.RESOLVE).call(); + + assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus()); + + assertEquals("1\na\n3\n", read(new File(db.getWorkTree(), "a"))); + assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b"))); + assertEquals("1\nc(main)\n3\n", read(new File(db.getWorkTree(), "c"))); + assertEquals("1\nd(main)\n3\n", read(new File(db.getWorkTree(), "d/d/d"))); + + assertEquals(null, result.getConflicts()); + + assertEquals(RepositoryState.MERGING, db.getRepositoryState()); + } + private void createBranch(ObjectId objectId, String branchName) throws IOException { RefUpdate updateRef = db.updateRef(branchName); updateRef.setNewObjectId(objectId); updateRef.update(); } - private void checkoutBranch(String branchName) throws Exception { - File workDir = db.getWorkTree(); - if (workDir != null) { - WorkDirCheckout workDirCheckout = new WorkDirCheckout(db, - workDir, db.mapTree(Constants.HEAD), - db.getIndex(), db.mapTree(branchName)); - workDirCheckout.setFailOnConflict(true); - try { - workDirCheckout.checkout(); - } catch (CheckoutConflictException e) { - throw new JGitInternalException( - "Couldn't check out because of conflicts", e); - } - } - + private void checkoutBranch(String branchName) throws IllegalStateException, IOException { + RevWalk walk = new RevWalk(db); + RevCommit head = walk.parseCommit(db.resolve(Constants.HEAD)); + RevCommit branch = walk.parseCommit(db.resolve(branchName)); + DirCacheCheckout dco = new DirCacheCheckout(db, + head.getTree().getId(), db.lockDirCache(), + branch.getTree().getId()); + dco.setFailOnConflict(true); + dco.checkout(); + walk.release(); // update the HEAD RefUpdate refUpdate = db.updateRef(Constants.HEAD); refUpdate.link(branchName); } - - private void addNewFileToIndex(String filename) throws IOException, - CorruptObjectException { - File writeTrashFile = writeTrashFile(filename, filename); - - GitIndex index = db.getIndex(); - Entry entry = index.add(db.getWorkTree(), writeTrashFile); - entry.update(writeTrashFile); - index.write(); - } } 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 f94f32fef..9f73fd1fc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java @@ -43,28 +43,32 @@ */ package org.eclipse.jgit.api; -import java.io.File; import java.io.IOException; import java.text.MessageFormat; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import java.util.Map; import org.eclipse.jgit.JGitText; import org.eclipse.jgit.api.MergeResult.MergeStatus; +import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.GitIndex; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.lib.WorkDirCheckout; import org.eclipse.jgit.lib.Ref.Storage; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.merge.MergeStrategy; +import org.eclipse.jgit.merge.ResolveMerger; +import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; +import org.eclipse.jgit.merge.ThreeWayMerger; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.FileTreeIterator; /** * A class used to execute a {@code Merge} command. It has setters for all @@ -101,7 +105,7 @@ public class MergeCommand extends GitCommand { */ public MergeResult call() throws NoHeadException, ConcurrentRefUpdateException, CheckoutConflictException, - InvalidMergeHeadsException { + InvalidMergeHeadsException, WrongRepositoryStateException, NoMessageException { checkCallable(); if (commits.size() != 1) @@ -109,8 +113,10 @@ public class MergeCommand extends GitCommand { commits.isEmpty() ? JGitText.get().noMergeHeadSpecified : MessageFormat.format( JGitText.get().mergeStrategyDoesNotSupportHeads, - mergeStrategy.getName(), commits.size())); + mergeStrategy.getName(), + Integer.valueOf(commits.size()))); + RevWalk revWalk = null; try { Ref head = repo.getRef(Constants.HEAD); if (head == null) @@ -119,74 +125,99 @@ public class MergeCommand extends GitCommand { StringBuilder refLogMessage = new StringBuilder("merge "); // Check for FAST_FORWARD, ALREADY_UP_TO_DATE - RevWalk revWalk = new RevWalk(repo); - try { - RevCommit headCommit = revWalk.lookupCommit(head.getObjectId()); + revWalk = new RevWalk(repo); + RevCommit headCommit = revWalk.lookupCommit(head.getObjectId()); - Ref ref = commits.get(0); + // we know for know there is only one commit + Ref ref = commits.get(0); - refLogMessage.append(ref.getName()); + refLogMessage.append(ref.getName()); - // handle annotated tags - ObjectId objectId = ref.getPeeledObjectId(); - if (objectId == null) - objectId = ref.getObjectId(); + // handle annotated tags + ObjectId objectId = ref.getPeeledObjectId(); + if (objectId == null) + objectId = ref.getObjectId(); - RevCommit srcCommit = revWalk.lookupCommit(objectId); - if (revWalk.isMergedInto(srcCommit, headCommit)) { - setCallable(false); - return new MergeResult(headCommit, srcCommit, - new ObjectId[] { srcCommit, headCommit }, - MergeStatus.ALREADY_UP_TO_DATE, mergeStrategy); - } else if (revWalk.isMergedInto(headCommit, srcCommit)) { - // FAST_FORWARD detected: skip doing a real merge but only - // update HEAD - refLogMessage.append(": " + MergeStatus.FAST_FORWARD); - checkoutNewHead(revWalk, headCommit, srcCommit); - updateHead(refLogMessage, srcCommit, head.getObjectId()); - setCallable(false); - return new MergeResult(srcCommit, headCommit, - new ObjectId[] { srcCommit, headCommit }, - MergeStatus.FAST_FORWARD, mergeStrategy); + RevCommit srcCommit = revWalk.lookupCommit(objectId); + if (revWalk.isMergedInto(srcCommit, headCommit)) { + setCallable(false); + return new MergeResult(headCommit, srcCommit, new ObjectId[] { + headCommit, srcCommit }, + MergeStatus.ALREADY_UP_TO_DATE, mergeStrategy, null, null); + } else if (revWalk.isMergedInto(headCommit, srcCommit)) { + // FAST_FORWARD detected: skip doing a real merge but only + // update HEAD + refLogMessage.append(": " + MergeStatus.FAST_FORWARD); + DirCacheCheckout dco = new DirCacheCheckout(repo, + headCommit.getTree(), repo.lockDirCache(), + srcCommit.getTree()); + dco.setFailOnConflict(true); + dco.checkout(); + + updateHead(refLogMessage, srcCommit, head.getObjectId()); + setCallable(false); + return new MergeResult(srcCommit, srcCommit, new ObjectId[] { + headCommit, srcCommit }, MergeStatus.FAST_FORWARD, + mergeStrategy, null, null); + } else { + repo.writeMergeCommitMsg("merging " + ref.getName() + " into " + + head.getName()); + repo.writeMergeHeads(Arrays.asList(ref.getObjectId())); + ThreeWayMerger merger = (ThreeWayMerger) mergeStrategy + .newMerger(repo); + boolean noProblems; + Map lowLevelResults = null; + Map failingPaths = null; + if (merger instanceof ResolveMerger) { + ResolveMerger resolveMerger = (ResolveMerger) merger; + resolveMerger.setCommitNames(new String[] { + "BASE", "HEAD", ref.getName() }); + resolveMerger.setWorkingTreeIt(new FileTreeIterator(repo)); + noProblems = merger.merge(headCommit, srcCommit); + lowLevelResults = resolveMerger + .getMergeResults(); + failingPaths = resolveMerger.getFailingPathes(); + } else + noProblems = merger.merge(headCommit, srcCommit); + + if (noProblems) { + DirCacheCheckout dco = new DirCacheCheckout(repo, + headCommit.getTree(), repo.lockDirCache(), + merger.getResultTreeId()); + dco.setFailOnConflict(true); + dco.checkout(); + RevCommit newHead = new Git(getRepository()).commit().call(); + return new MergeResult(newHead.getId(), + null, new ObjectId[] { + headCommit.getId(), srcCommit.getId() }, + MergeStatus.MERGED, mergeStrategy, null, null); } else { - return new MergeResult( - headCommit, - null, - new ObjectId[] { srcCommit, headCommit }, - MergeResult.MergeStatus.NOT_SUPPORTED, - mergeStrategy, - JGitText.get().onlyAlreadyUpToDateAndFastForwardMergesAreAvailable); + if (failingPaths != null) { + repo.writeMergeCommitMsg(null); + repo.writeMergeHeads(null); + return new MergeResult(null, + merger.getBaseCommit(0, 1), + new ObjectId[] { + headCommit.getId(), srcCommit.getId() }, + MergeStatus.FAILED, mergeStrategy, + lowLevelResults, null); + } else + return new MergeResult(null, + merger.getBaseCommit(0, 1), + new ObjectId[] { headCommit.getId(), + srcCommit.getId() }, + MergeStatus.CONFLICTING, mergeStrategy, + lowLevelResults, null); } - } finally { - revWalk.release(); } } catch (IOException e) { throw new JGitInternalException( MessageFormat.format( JGitText.get().exceptionCaughtDuringExecutionOfMergeCommand, e), e); - } - } - - private void checkoutNewHead(RevWalk revWalk, RevCommit headCommit, - RevCommit newHeadCommit) throws IOException, - CheckoutConflictException { - GitIndex index = repo.getIndex(); - - File workDir = repo.getWorkTree(); - if (workDir != null) { - WorkDirCheckout workDirCheckout = new WorkDirCheckout(repo, - workDir, repo.mapTree(headCommit.getTree()), index, - repo.mapTree(newHeadCommit.getTree())); - workDirCheckout.setFailOnConflict(true); - try { - workDirCheckout.checkout(); - } catch (org.eclipse.jgit.errors.CheckoutConflictException e) { - throw new CheckoutConflictException( - JGitText.get().couldNotCheckOutBecauseOfConflicts, - workDirCheckout.getConflicts(), e); - } - index.write(); + } finally { + if (revWalk != null) + revWalk.release(); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java index 6fcf2ee6d..ddb14a03f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java @@ -44,11 +44,15 @@ package org.eclipse.jgit.api; import java.text.MessageFormat; +import java.util.HashMap; import java.util.Map; import org.eclipse.jgit.JGitText; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.merge.MergeChunk; +import org.eclipse.jgit.merge.MergeChunk.ConflictState; import org.eclipse.jgit.merge.MergeStrategy; +import org.eclipse.jgit.merge.ResolveMerger; /** * Encapsulates the result of a {@link MergeCommand}. @@ -125,11 +129,14 @@ public class MergeResult { * the status the merge resulted in * @param mergeStrategy * the used {@link MergeStrategy} + * @param lowLevelResults + * merge results as returned by {@link ResolveMerger#getMergeResults()} */ public MergeResult(ObjectId newHead, ObjectId base, ObjectId[] mergedCommits, MergeStatus mergeStatus, + Map lowLevelResults, MergeStrategy mergeStrategy) { - this(newHead, base, mergedCommits, mergeStatus, mergeStrategy, null); + this(newHead, base, mergedCommits, mergeStatus, mergeStrategy, lowLevelResults, null); } /** @@ -145,18 +152,25 @@ public class MergeResult { * the status the merge resulted in * @param mergeStrategy * the used {@link MergeStrategy} + * @param lowLevelResults + * merge results as returned by {@link ResolveMerger#getMergeResults()} * @param description * a user friendly description of the merge result */ public MergeResult(ObjectId newHead, ObjectId base, ObjectId[] mergedCommits, MergeStatus mergeStatus, - MergeStrategy mergeStrategy, String description) { + MergeStrategy mergeStrategy, + Map lowLevelResults, + String description) { this.newHead = newHead; this.mergedCommits = mergedCommits; this.base = base; this.mergeStatus = mergeStatus; this.mergeStrategy = mergeStrategy; this.description = description; + if (lowLevelResults != null) + for (String path : lowLevelResults.keySet()) + addConflict(path, lowLevelResults.get(path)); } /** @@ -214,6 +228,55 @@ public class MergeResult { this.conflicts = conflicts; } + /** + * @param path + * @param conflictingRanges + * the conflicts to set + */ + public void addConflict(String path, int[][] conflictingRanges) { + if (conflicts == null) + conflicts = new HashMap(); + conflicts.put(path, conflictingRanges); + } + + /** + * @param path + * @param lowLevelResult + */ + public void addConflict(String path, org.eclipse.jgit.merge.MergeResult lowLevelResult) { + if (conflicts == null) + conflicts = new HashMap(); + int nrOfConflicts = 0; + // just counting + for (MergeChunk mergeChunk : lowLevelResult) { + if (mergeChunk.getConflictState().equals(ConflictState.FIRST_CONFLICTING_RANGE)) { + nrOfConflicts++; + } + } + int currentConflict = -1; + int[][] ret=new int[nrOfConflicts][mergedCommits.length+1]; + for (MergeChunk mergeChunk : lowLevelResult) { + // to store the end of this chunk (end of the last conflicting range) + int endOfChunk = 0; + if (mergeChunk.getConflictState().equals(ConflictState.FIRST_CONFLICTING_RANGE)) { + if (currentConflict > -1) { + // there was a previous conflicting range for which the end + // is not set yet - set it! + ret[currentConflict][mergedCommits.length] = endOfChunk; + } + currentConflict++; + endOfChunk = mergeChunk.getEnd(); + ret[currentConflict][mergeChunk.getSequenceIndex()] = mergeChunk.getBegin(); + } + if (mergeChunk.getConflictState().equals(ConflictState.NEXT_CONFLICTING_RANGE)) { + if (mergeChunk.getEnd() > endOfChunk) + endOfChunk = mergeChunk.getEnd(); + ret[currentConflict][mergeChunk.getSequenceIndex()] = mergeChunk.getBegin(); + } + } + conflicts.put(path, ret); + } + /** * Returns information about the conflicts which occurred during a * {@link MergeCommand}. The returned value maps the path of a conflicting diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java index 450411a50..13a253e0a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -187,7 +187,8 @@ public class DirCacheCheckout { public DirCacheCheckout(Repository repo, ObjectId headCommitTree, DirCache dc, ObjectId mergeCommitTree) throws IOException { this(repo, headCommitTree, dc, mergeCommitTree, new FileTreeIterator( - repo.getWorkTree(), repo.getFS(), WorkingTreeOptions.createDefaultInstance())); + repo.getWorkTree(), repo.getFS(), + WorkingTreeOptions.createDefaultInstance())); } /** @@ -341,7 +342,7 @@ public class DirCacheCheckout { file.getParentFile().mkdirs(); file.createNewFile(); DirCacheEntry entry = dc.getEntry(path); - checkoutEntry(file, entry, config_filemode()); + checkoutEntry(repo, file, entry, config_filemode()); } @@ -743,7 +744,8 @@ public class DirCacheCheckout { NameConflictTreeWalk tw = new NameConflictTreeWalk(repo); tw.reset(); tw.addTree(new DirCacheIterator(dc)); - tw.addTree(new FileTreeIterator(repo.getWorkTree(), repo.getFS(), WorkingTreeOptions.createDefaultInstance())); + tw.addTree(new FileTreeIterator(repo.getWorkTree(), repo.getFS(), + WorkingTreeOptions.createDefaultInstance())); tw.setRecursive(true); tw.setFilter(PathFilter.create(path)); DirCacheIterator dcIt; @@ -769,7 +771,7 @@ public class DirCacheCheckout { * TODO: this method works directly on File IO, we may need another * abstraction (like WorkingTreeIterator). This way we could tell e.g. * Eclipse that Files in the workspace got changed - * + * @param repo * @param f * the file to be modified. The parent directory for this file * has to exist already @@ -779,7 +781,7 @@ public class DirCacheCheckout { * whether the mode bits should be handled at all. * @throws IOException */ - public void checkoutEntry(File f, DirCacheEntry entry, + public static void checkoutEntry(final Repository repo, File f, DirCacheEntry entry, boolean config_filemode) throws IOException { ObjectLoader ol = repo.open(entry.getObjectId()); if (ol == null) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeStrategy.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeStrategy.java index d678f7c0f..28347e869 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeStrategy.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeStrategy.java @@ -66,12 +66,16 @@ public abstract class MergeStrategy { /** Simple strategy to merge paths, without simultaneous edits. */ public static final ThreeWayMergeStrategy SIMPLE_TWO_WAY_IN_CORE = new StrategySimpleTwoWayInCore(); + /** Simple strategy to merge paths. It tries to merge also contents. Multiple merge bases are not supported */ + public static final ThreeWayMergeStrategy RESOLVE = new StrategyResolve(); + private static final HashMap STRATEGIES = new HashMap(); static { register(OURS); register(THEIRS); register(SIMPLE_TWO_WAY_IN_CORE); + register(RESOLVE); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java new file mode 100644 index 000000000..a69396a57 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -0,0 +1,571 @@ +/* + * Copyright (C) 2010, Christian Halstrick , + * Copyright (C) 2010, Matthias Sohn + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.merge; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.JGitText; +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuildIterator; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.IndexWriteException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.NameConflictTreeWalk; +import org.eclipse.jgit.treewalk.WorkingTreeIterator; + +/** + * A three-way merger performing a content-merge if necessary + */ +public class ResolveMerger extends ThreeWayMerger { + /** + * If the merge fails abnormally (means: not because of unresolved + * conflicts) this enum is used to explain why it failed + */ + public enum MergeFailureReason { + /** the merge failed because of a dirty index */ + DIRTY_INDEX, + /** the merge failed because of a dirty workingtree */ + DIRTY_WORKTREE + } + + private NameConflictTreeWalk tw; + + private String commitNames[]; + + private static final int T_BASE = 0; + + private static final int T_OURS = 1; + + private static final int T_THEIRS = 2; + + private static final int T_INDEX = 3; + + private static final int T_FILE = 4; + + private DirCacheBuilder builder; + + private ObjectId resultTree; + + private List unmergedPathes = new ArrayList(); + + private List modifiedFiles = new LinkedList(); + + private Map toBeCheckedOut = new HashMap(); + + private Map mergeResults = new HashMap(); + + private Map failingPathes = new HashMap(); + + private ObjectInserter oi; + + private boolean enterSubtree; + + private DirCache dircache; + + private WorkingTreeIterator workingTreeIt; + + + /** + * @param local + */ + protected ResolveMerger(Repository local) { + super(local); + commitNames = new String[] { "BASE", "OURS", "THEIRS" }; + oi = getObjectInserter(); + } + + @Override + protected boolean mergeImpl() throws IOException { + boolean implicitDirCache = false; + + if (dircache == null) { + dircache = getRepository().lockDirCache(); + implicitDirCache = true; + } + + try { + builder = dircache.builder(); + DirCacheBuildIterator buildIt = new DirCacheBuildIterator(builder); + + tw = new NameConflictTreeWalk(db); + tw.reset(); + tw.addTree(mergeBase()); + tw.addTree(sourceTrees[0]); + tw.addTree(sourceTrees[1]); + tw.addTree(buildIt); + if (workingTreeIt != null) + tw.addTree(workingTreeIt); + + while (tw.next()) { + if (!processEntry( + tw.getTree(T_BASE, CanonicalTreeParser.class), + tw.getTree(T_OURS, CanonicalTreeParser.class), + tw.getTree(T_THEIRS, CanonicalTreeParser.class), + tw.getTree(T_INDEX, DirCacheBuildIterator.class), + (workingTreeIt == null) ? null : tw.getTree(T_FILE, WorkingTreeIterator.class))) { + cleanUp(); + return false; + } + if (tw.isSubtree() && enterSubtree) + tw.enterSubtree(); + } + + // All content-merges are successfully done. If we can now write the + // new + // index we are on quite safe ground. Even if the checkout of files + // coming from "theirs" fails the user can work around such failures + // by + // checking out the index again. + if (!builder.commit()) { + cleanUp(); + throw new IndexWriteException(); + } + builder = null; + + // No problem found. The only thing left to be done is to checkout + // all files from "theirs" which have been selected to go into the + // new index. + checkout(); + if (getUnmergedPathes().isEmpty()) { + resultTree = dircache.writeTree(oi); + return true; + } else { + resultTree = null; + return false; + } + } finally { + if (implicitDirCache) + dircache.unlock(); + } + } + + private void checkout() throws NoWorkTreeException, IOException { + for (Map.Entry entry : toBeCheckedOut.entrySet()) { + File f = new File(db.getWorkTree(), entry.getKey()); + createDir(f.getParentFile()); + DirCacheCheckout.checkoutEntry(db, + f, + entry.getValue(), true); + modifiedFiles.add(entry.getKey()); + } + } + + private void createDir(File f) throws IOException { + if (!f.isDirectory() && !f.mkdirs()) { + File p = f; + while (p != null && !p.exists()) + p = p.getParentFile(); + if (p == null || p.isDirectory()) + throw new IOException(JGitText.get().cannotCreateDirectory); + p.delete(); + if (!f.mkdirs()) + throw new IOException(JGitText.get().cannotCreateDirectory); + } + } + + /** + * Reverts the worktree after an unsuccessful merge. We know that for all + * modified files the old content was in the old index and the index + * contained only stage 0 + * + * @throws IOException + * @throws CorruptObjectException + * @throws NoWorkTreeException + */ + private void cleanUp() throws NoWorkTreeException, CorruptObjectException, IOException { + DirCache dc = db.readDirCache(); + ObjectReader or = db.getObjectDatabase().newReader(); + Iterator mpathsIt=modifiedFiles.iterator(); + while(mpathsIt.hasNext()) { + String mpath=mpathsIt.next(); + DirCacheEntry entry = dc.getEntry(mpath); + FileOutputStream fos = new FileOutputStream(new File(db.getWorkTree(), mpath)); + try { + or.open(entry.getObjectId()).copyTo(fos); + } finally { + fos.close(); + } + mpathsIt.remove(); + } + } + + /** + * adds a new path with the specified stage to the index builder + * + * @param path + * @param p + * @param stage + * @return the entry which was added to the index + */ + private DirCacheEntry add(byte[] path, CanonicalTreeParser p, int stage) { + if (p != null && !p.getEntryFileMode().equals(FileMode.TREE)) { + DirCacheEntry e = new DirCacheEntry(path, stage); + e.setFileMode(p.getEntryFileMode()); + e.setObjectId(p.getEntryObjectId()); + builder.add(e); + return e; + } + return null; + } + + /** + * Processes one path and tries to merge. This method will do all do all + * trivial (not content) merges and will also detect if a merge will fail. + * The merge will fail when one of the following is true + *
    + *
  • the index entry does not match the entry in ours. When merging one + * branch into the current HEAD, ours will point to HEAD and theirs will + * point to the other branch. It is assumed that the index matches the HEAD + * because it will only not match HEAD if it was populated before the merge + * operation. But the merge commit should not accidentally contain + * modifications done before the merge. Check the git read-tree documentation for further explanations.
  • + *
  • A conflict was detected and the working-tree file is dirty. When a + * conflict is detected the content-merge algorithm will try to write a + * merged version into the working-tree. If the file is dirty we would + * override unsaved data.
  • + * + * @param base + * the common base for ours and theirs + * @param ours + * the ours side of the merge. When merging a branch into the + * HEAD ours will point to HEAD + * @param theirs + * the theirs side of the merge. When merging a branch into the + * current HEAD theirs will point to the branch which is merged + * into HEAD. + * @param index + * the index entry + * @param work + * the file in the working tree + * @return false if the merge will fail because the index entry + * didn't match ours or the working-dir file was dirty and a + * conflict occured + * @throws MissingObjectException + * @throws IncorrectObjectTypeException + * @throws CorruptObjectException + * @throws IOException + */ + private boolean processEntry(CanonicalTreeParser base, + CanonicalTreeParser ours, CanonicalTreeParser theirs, + DirCacheBuildIterator index, WorkingTreeIterator work) + throws MissingObjectException, IncorrectObjectTypeException, + CorruptObjectException, IOException { + enterSubtree = true; + final int modeO = tw.getRawMode(T_OURS); + final int modeI = tw.getRawMode(T_INDEX); + + // Each index entry has to match ours, means: it has to be clean + if (nonTree(modeI) + && !(tw.idEqual(T_INDEX, T_OURS) && modeO == modeI)) { + failingPathes.put(tw.getPathString(), MergeFailureReason.DIRTY_INDEX); + return false; + } + + final int modeT = tw.getRawMode(T_THEIRS); + if (nonTree(modeO) && modeO == modeT && tw.idEqual(T_OURS, T_THEIRS)) { + // ours and theirs are equal: it doesn'nt matter + // which one we choose. OURS is choosen here. + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0); + // no checkout needed! + return true; + } + + final int modeB = tw.getRawMode(T_BASE); + if (nonTree(modeO) && modeB == modeT && tw.idEqual(T_BASE, T_THEIRS)) { + // THEIRS was not changed compared to base. All changes must be in + // OURS. Choose OURS. + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0); + return true; + } + + if (nonTree(modeT) && modeB == modeO && tw.idEqual(T_BASE, T_OURS)) { + // OURS was not changed compared to base. All changes must be in + // THEIRS. Choose THEIRS. + DirCacheEntry e=add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_0); + if (e!=null) + toBeCheckedOut.put(tw.getPathString(), e); + return true; + } + + if (tw.isSubtree()) { + // file/folder conflicts: here I want to detect only file/folder + // conflict between ours and theirs. file/folder conflicts between + // base/index/workingTree and something else are not relevant or + // detected later + if (nonTree(modeO) && !nonTree(modeT)) { + if (nonTree(modeB)) + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1); + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2); + unmergedPathes.add(tw.getPathString()); + enterSubtree = false; + return true; + } + if (nonTree(modeT) && !nonTree(modeO)) { + if (nonTree(modeB)) + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1); + add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3); + unmergedPathes.add(tw.getPathString()); + enterSubtree = false; + return true; + } + + // ours and theirs are both folders or both files (and treewalk + // tells us we are in a subtree because of index or working-dir). + // If they are both folders no content-merge is required - we can + // return here. + if (!nonTree(modeO)) + return true; + + // ours and theirs are both files, just fall out of the if block + // and do the content merge + } + + if (nonTree(modeO) && nonTree(modeT)) { + // We are going to update the worktree. Make sure the worktree is + // not modified + if (work != null + && (!nonTree(work.getEntryRawMode()) || work.isModified( + index.getDirCacheEntry(), true, true, db.getFS()))) { + failingPathes.put(tw.getPathString(), + MergeFailureReason.DIRTY_WORKTREE); + return false; + } + + if (!contentMerge(base, ours, theirs)) { + unmergedPathes.add(tw.getPathString()); + } + modifiedFiles.add(tw.getPathString()); + } + return true; + } + + private boolean contentMerge(CanonicalTreeParser base, + CanonicalTreeParser ours, CanonicalTreeParser theirs) + throws FileNotFoundException, IllegalStateException, IOException { + MergeFormatter fmt = new MergeFormatter(); + + // do the merge + MergeResult result = MergeAlgorithm.merge( + getRawText(base.getEntryObjectId(), db), + getRawText(ours.getEntryObjectId(), db), + getRawText(theirs.getEntryObjectId(), db)); + + File workTree = db.getWorkTree(); + if (workTree == null) + // TODO: This should be handled by WorkingTreeIterators which + // support write operations + throw new UnsupportedOperationException(); + + File of = new File(workTree, tw.getPathString()); + FileOutputStream fos = new FileOutputStream(of); + try { + fmt.formatMerge(fos, result, Arrays.asList(commitNames), + Constants.CHARACTER_ENCODING); + } finally { + fos.close(); + } + if (result.containsConflicts()) { + // a conflict occured, the file will contain conflict markers + // the index will be populated with the three stages and only the + // workdir contains the halfways merged content + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1); + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2); + add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3); + mergeResults.put(tw.getPathString(), result); + return false; + } else { + // no conflict occured, the file will contain fully merged content. + // the index will be populated with the new merged version + DirCacheEntry dce = new DirCacheEntry(tw.getPathString()); + dce.setFileMode(tw.getFileMode(0)); + dce.setLastModified(of.lastModified()); + dce.setLength((int) of.length()); + InputStream is = new FileInputStream(of); + try { + dce.setObjectId(oi.insert(Constants.OBJ_BLOB, of.length(), + is)); + } finally { + is.close(); + } + builder.add(dce); + return true; + } + } + + private static RawText getRawText(ObjectId id, Repository db) + throws IOException { + if (id.equals(ObjectId.zeroId())) + return new RawText(new byte[] {}); + return new RawText(db.open(id, Constants.OBJ_BLOB).getCachedBytes()); + } + + private static boolean nonTree(final int mode) { + return mode != 0 && !FileMode.TREE.equals(mode); + } + + @Override + public ObjectId getResultTreeId() { + return (resultTree == null) ? null : resultTree.toObjectId(); + } + + /** + * @param commitNames + * the names of the commits as they would appear in conflict + * markers + */ + public void setCommitNames(String[] commitNames) { + this.commitNames = commitNames; + } + + /** + * @return the names of the commits as they would appear in conflict + * markers. + */ + public String[] getCommitNames() { + return commitNames; + } + + /** + * @return the paths with conflicts. This is a subset of the files listed + * by {@link #getModifiedFiles()} + */ + public List getUnmergedPathes() { + return unmergedPathes; + } + + /** + * @return the paths of files which have been modified by this merge. A + * file will be modified if a content-merge works on this path or if + * the merge algorithm decides to take the theirs-version. This is a + * superset of the files listed by {@link #getUnmergedPathes()}. + */ + public List getModifiedFiles() { + return modifiedFiles; + } + + /** + * @return a map which maps the paths of files which have to be checked out + * because the merge created new fully-merged content for this file + * into the index. This means: the merge wrote a new stage 0 entry + * for this path. + */ + public Map getToBeCheckedOut() { + return toBeCheckedOut; + } + + /** + * @return the mergeResults + */ + public Map getMergeResults() { + return mergeResults; + } + + /** + * @return lists paths causing this merge to fail abnormally (not because of + * a conflict). null is returned if this merge didn't + * fail abnormally. + */ + public Map getFailingPathes() { + return (failingPathes.size() == 0) ? null : failingPathes; + } + + /** + * Sets the DirCache which shall be used by this merger. If the DirCache is + * not set explicitly this merger will implicitly get and lock a default + * DirCache. If the DirCache is explicitly set the caller is responsible to + * lock it in advance. Finally the merger will call + * {@link DirCache#commit()} which requires that the DirCache is locked. If + * the {@link #mergeImpl()} returns without throwing an exception the lock + * will be released. In case of exceptions the caller is responsible to + * release the lock. + * + * @param dc + * the DirCache to set + */ + public void setDirCache(DirCache dc) { + this.dircache = dc; + } + + /** + * Sets the WorkingTreeIterator to be used by this merger. If no + * WorkingTreeIterator is set this merger will ignore the working tree and + * fail if a content merge is necessary. + *

    + * TODO: enhance WorkingTreeIterator to support write operations. Then this + * merger will be able to merge with a different working tree abstraction. + * + * @param workingTreeIt + * the workingTreeIt to set + */ + public void setWorkingTreeIt(WorkingTreeIterator workingTreeIt) { + this.workingTreeIt = workingTreeIt; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java new file mode 100644 index 000000000..2885625bf --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010, Christian Halstrick , + * Copyright (C) 2010, Matthias Sohn + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.merge; + +import org.eclipse.jgit.lib.Repository; + +/** + * A three-way merge strategy performing a content-merge if necessary + */ +public class StrategyResolve extends ThreeWayMergeStrategy { + @Override + public ThreeWayMerger newMerger(Repository db) { + return new ResolveMerger(db); + } + + @Override + public String getName() { + return "resolve"; + } +} \ No newline at end of file