From 45e79a526c7ffebaf8e4758a6cb6b7af05716707 Mon Sep 17 00:00:00 2001 From: Christian Halstrick Date: Thu, 12 Aug 2010 15:19:18 +0200 Subject: [PATCH] Added merge strategy RESOLVE This adds the first merge strategy to JGit which does real content-merges if necessary. The new merge strategy "resolve" takes as input three commits: a common base, ours and theirs. It will simply takeover changes on files which are only touched in ours or theirs. For files touched in ours and theirs it will try to merge the two contents knowing taking into account the specified common base. Rename detection has not been introduced for now. Change-Id: I49a5ebcdcf4f540f606092c0f1dc66c965dc66ba Signed-off-by: Christian Halstrick Signed-off-by: Stefan Lay --- .../eclipse/jgit/api/MergeCommandTest.java | 300 +++++++-- .../org/eclipse/jgit/api/MergeCommand.java | 153 +++-- .../src/org/eclipse/jgit/api/MergeResult.java | 67 +- .../jgit/dircache/DirCacheCheckout.java | 12 +- .../org/eclipse/jgit/merge/MergeStrategy.java | 4 + .../org/eclipse/jgit/merge/ResolveMerger.java | 571 ++++++++++++++++++ .../eclipse/jgit/merge/StrategyResolve.java | 61 ++ 7 files changed, 1064 insertions(+), 104 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java 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 ccb1672dd7..2def86cf1c 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 f94f32fef9..9f73fd1fca 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 6fcf2ee6d0..ddb14a03fc 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 450411a50a..13a253e0a2 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 d678f7c0fc..28347e8692 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 0000000000..a69396a572 --- /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 0000000000..2885625bf0 --- /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 -- 2.39.5