From ab99b78ca08a6b52e9ae8b49afa04dd16496f2ac Mon Sep 17 00:00:00 2001 From: "George C. Young" Date: Thu, 21 Feb 2013 13:44:40 -0500 Subject: [PATCH] Implement recursive merge strategy Extend ResolveMerger with RecursiveMerger to merge two tips that have up to 200 bases. Bug: 380314 CQ: 6854 Change-Id: I6292bb7bda55c0242a448a94956f2d6a94fddbaa Also-by: Christian Halstrick Signed-off-by: Chris Aniszczyk Signed-off-by: Matthias Sohn --- README.md | 2 - .../jgit/merge/RecursiveMergerTest.java | 578 ++++++++++++++++++ .../eclipse/jgit/merge/ResolveMergerTest.java | 66 ++ .../eclipse/jgit/internal/JGitText.properties | 3 + .../jgit/errors/NoMergeBaseException.java | 124 ++++ .../org/eclipse/jgit/internal/JGitText.java | 4 + .../org/eclipse/jgit/merge/MergeStrategy.java | 18 +- .../src/org/eclipse/jgit/merge/Merger.java | 44 +- .../eclipse/jgit/merge/RecursiveMerger.java | 274 +++++++++ .../org/eclipse/jgit/merge/ResolveMerger.java | 212 ++++--- .../eclipse/jgit/merge/StrategyRecursive.java | 67 ++ .../eclipse/jgit/merge/ThreeWayMerger.java | 3 +- 12 files changed, 1301 insertions(+), 94 deletions(-) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/RecursiveMergerTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/errors/NoMergeBaseException.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyRecursive.java diff --git a/README.md b/README.md index 8d9c13781..6e6c0c7bf 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,6 @@ There are some missing features: - gitattributes support -- Recursive merge strategy - Support ------- diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/RecursiveMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/RecursiveMergerTest.java new file mode 100644 index 000000000..9860d304f --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/RecursiveMergerTest.java @@ -0,0 +1,578 @@ +/* + * Copyright (C) 2012, Christian Halstrick + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheEditor; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.NoMergeBaseException; +import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.junit.TestRepository.BranchBuilder; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevBlob; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.file.FileRepository; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.junit.Before; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; + +@RunWith(Theories.class) +public class RecursiveMergerTest extends RepositoryTestCase { + static int counter = 0; + + @DataPoints + public static MergeStrategy[] strategiesUnderTest = new MergeStrategy[] { + MergeStrategy.RECURSIVE, MergeStrategy.RESOLVE }; + + public enum IndexState { + Bare, Missing, SameAsHead, SameAsOther, SameAsWorkTree, DifferentFromHeadAndOtherAndWorktree + } + + @DataPoints + public static IndexState[] indexStates = IndexState.values(); + + public enum WorktreeState { + Bare, Missing, SameAsHead, DifferentFromHeadAndOther, SameAsOther; + } + + @DataPoints + public static WorktreeState[] worktreeStates = WorktreeState.values(); + + private TestRepository db_t; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + db_t = new TestRepository(db); + } + + @Theory + /** + * Merging m2,s2 from the following topology. In master and side different + * files are touched. No need to do a real content merge. + * + *
+	 * m0--m1--m2
+	 *   \   \/
+	 *    \  /\
+	 *     s1--s2
+	 * 
+ */ + public void crissCrossMerge(MergeStrategy strategy, IndexState indexState, + WorktreeState worktreeState) throws Exception { + if (!validateStates(indexState, worktreeState)) + return; + // fill the repo + BranchBuilder master = db_t.branch("master"); + RevCommit m0 = master.commit().add("m", ",m0").message("m0").create(); + RevCommit m1 = master.commit().add("m", "m1").message("m1").create(); + db_t.getRevWalk().parseCommit(m1); + + BranchBuilder side = db_t.branch("side"); + RevCommit s1 = side.commit().parent(m0).add("s", "s1").message("s1") + .create(); + RevCommit s2 = side.commit().parent(m1).add("m", "m1") + .message("s2(merge)").create(); + RevCommit m2 = master.commit().parent(s1).add("s", "s1") + .message("m2(merge)").create(); + + Git git = Git.wrap(db); + git.checkout().setName("master").call(); + modifyWorktree(worktreeState, "m", "side"); + modifyWorktree(worktreeState, "s", "side"); + modifyIndex(indexState, "m", "side"); + modifyIndex(indexState, "s", "side"); + + ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, + worktreeState == WorktreeState.Bare); + if (worktreeState != WorktreeState.Bare) + merger.setWorkingTreeIterator(new FileTreeIterator(db)); + try { + boolean expectSuccess = true; + if (!(indexState == IndexState.Bare + || indexState == IndexState.Missing + || indexState == IndexState.SameAsHead || indexState == IndexState.SameAsOther)) + // index is dirty + expectSuccess = false; + + assertEquals(Boolean.valueOf(expectSuccess), + Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 }))); + assertEquals(MergeStrategy.RECURSIVE, strategy); + assertEquals("m1", + contentAsString(db, merger.getResultTreeId(), "m")); + assertEquals("s1", + contentAsString(db, merger.getResultTreeId(), "s")); + } catch (NoMergeBaseException e) { + assertEquals(MergeStrategy.RESOLVE, strategy); + assertEquals(e.getReason(), + MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED); + } + } + + @Theory + /** + * Merging m2,s2 from the following topology. The same file is modified + * in both branches. The modifications should be mergeable. m2 and s2 + * contain branch specific conflict resolutions. Therefore m2 and don't contain the same content. + * + *
+	 * m0--m1--m2
+	 *   \   \/
+	 *    \  /\
+	 *     s1--s2
+	 * 
+ */ + public void crissCrossMerge_mergeable(MergeStrategy strategy, + IndexState indexState, WorktreeState worktreeState) + throws Exception { + if (!validateStates(indexState, worktreeState)) + return; + + BranchBuilder master = db_t.branch("master"); + RevCommit m0 = master.commit().add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n") + .message("m0").create(); + RevCommit m1 = master.commit() + .add("f", "1-master\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m1") + .create(); + db_t.getRevWalk().parseCommit(m1); + + BranchBuilder side = db_t.branch("side"); + RevCommit s1 = side.commit().parent(m0) + .add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9-side\n").message("s1") + .create(); + RevCommit s2 = side.commit().parent(m1) + .add("f", "1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n") + .message("s2(merge)").create(); + RevCommit m2 = master + .commit() + .parent(s1) + .add("f", "1-master\n2\n3-res(master)\n4\n5\n6\n7\n8\n9-side\n") + .message("m2(merge)").create(); + + Git git = Git.wrap(db); + git.checkout().setName("master").call(); + modifyWorktree(worktreeState, "f", "side"); + modifyIndex(indexState, "f", "side"); + + ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, + worktreeState == WorktreeState.Bare); + if (worktreeState != WorktreeState.Bare) + merger.setWorkingTreeIterator(new FileTreeIterator(db)); + try { + boolean expectSuccess = true; + if (!(indexState == IndexState.Bare + || indexState == IndexState.Missing || indexState == IndexState.SameAsHead)) + // index is dirty + expectSuccess = false; + else if (worktreeState == WorktreeState.DifferentFromHeadAndOther + || worktreeState == WorktreeState.SameAsOther) + expectSuccess = false; + assertEquals(Boolean.valueOf(expectSuccess), + Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 }))); + assertEquals(MergeStrategy.RECURSIVE, strategy); + if (!expectSuccess) + // if the merge was not successful skip testing the state of index and workingtree + return; + assertEquals( + "1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side", + contentAsString(db, merger.getResultTreeId(), "f")); + if (indexState != IndexState.Bare) + assertEquals( + "[f, mode:100644, content:1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side\n]", + indexState(RepositoryTestCase.CONTENT)); + if (worktreeState != WorktreeState.Bare + && worktreeState != WorktreeState.Missing) + assertEquals( + "1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side\n", + read("f")); + } catch (NoMergeBaseException e) { + assertEquals(MergeStrategy.RESOLVE, strategy); + assertEquals(e.getReason(), + MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED); + } + } + + @Theory + /** + * Merging m2,s2 from the following topology. The same file is modified + * in both branches. The modifications are not automatically + * mergeable. m2 and s2 contain branch specific conflict resolutions. + * Therefore m2 and s2 don't contain the same content. + * + *
+	 * m0--m1--m2
+	 *   \   \/
+	 *    \  /\
+	 *     s1--s2
+	 * 
+ */ + public void crissCrossMerge_nonmergeable(MergeStrategy strategy, + IndexState indexState, WorktreeState worktreeState) + throws Exception { + if (!validateStates(indexState, worktreeState)) + return; + + BranchBuilder master = db_t.branch("master"); + RevCommit m0 = master.commit().add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n") + .message("m0").create(); + RevCommit m1 = master.commit() + .add("f", "1-master\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m1") + .create(); + db_t.getRevWalk().parseCommit(m1); + + BranchBuilder side = db_t.branch("side"); + RevCommit s1 = side.commit().parent(m0) + .add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9-side\n").message("s1") + .create(); + RevCommit s2 = side.commit().parent(m1) + .add("f", "1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n") + .message("s2(merge)").create(); + RevCommit m2 = master.commit().parent(s1) + .add("f", "1-master\n2\n3\n4\n5\n6\n7-conflict\n8\n9-side\n") + .message("m2(merge)").create(); + + Git git = Git.wrap(db); + git.checkout().setName("master").call(); + modifyWorktree(worktreeState, "f", "side"); + modifyIndex(indexState, "f", "side"); + + ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, + worktreeState == WorktreeState.Bare); + if (worktreeState != WorktreeState.Bare) + merger.setWorkingTreeIterator(new FileTreeIterator(db)); + try { + assertFalse(merger.merge(new RevCommit[] { m2, s2 })); + assertEquals(MergeStrategy.RECURSIVE, strategy); + if (indexState == IndexState.SameAsHead + && worktreeState == WorktreeState.SameAsHead) { + assertEquals( + "[f, mode:100644, stage:1, content:1-master\n2\n3\n4\n5\n6\n7\n8\n9-side\n]" + + "[f, mode:100644, stage:2, content:1-master\n2\n3\n4\n5\n6\n7-conflict\n8\n9-side\n]" + + "[f, mode:100644, stage:3, content:1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n]", + indexState(RepositoryTestCase.CONTENT)); + assertEquals( + "1-master\n2\n3\n4\n5\n6\n<<<<<<< OURS\n7-conflict\n=======\n7-res(side)\n>>>>>>> THEIRS\n8\n9-side\n", + read("f")); + } + } catch (NoMergeBaseException e) { + assertEquals(MergeStrategy.RESOLVE, strategy); + assertEquals(e.getReason(), + MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED); + } + } + + @Theory + /** + * Merging m2,s2 which have three common predecessors.The same file is modified + * in all branches. The modifications should be mergeable. m2 and s2 + * contain branch specific conflict resolutions. Therefore m2 and s2 + * don't contain the same content. + * + *
+	 *     m1-----m2
+	 *    /  \/  /
+	 *   /   /\ /
+	 * m0--o1  x
+	 *   \   \/ \
+	 *    \  /\  \
+	 *     s1-----s2
+	 * 
+ */ + public void crissCrossMerge_ThreeCommonPredecessors(MergeStrategy strategy, + IndexState indexState, WorktreeState worktreeState) + throws Exception { + if (!validateStates(indexState, worktreeState)) + return; + + BranchBuilder master = db_t.branch("master"); + RevCommit m0 = master.commit().add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n") + .message("m0").create(); + RevCommit m1 = master.commit() + .add("f", "1-master\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m1") + .create(); + BranchBuilder side = db_t.branch("side"); + RevCommit s1 = side.commit().parent(m0) + .add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9-side\n").message("s1") + .create(); + BranchBuilder other = db_t.branch("other"); + RevCommit o1 = other.commit().parent(m0) + .add("f", "1\n2\n3\n4\n5-other\n6\n7\n8\n9\n").message("o1") + .create(); + + RevCommit m2 = master + .commit() + .parent(s1) + .parent(o1) + .add("f", + "1-master\n2\n3-res(master)\n4\n5-other\n6\n7\n8\n9-side\n") + .message("m2(merge)").create(); + + RevCommit s2 = side + .commit() + .parent(m1) + .parent(o1) + .add("f", + "1-master\n2\n3\n4\n5-other\n6\n7-res(side)\n8\n9-side\n") + .message("s2(merge)").create(); + + Git git = Git.wrap(db); + git.checkout().setName("master").call(); + modifyWorktree(worktreeState, "f", "side"); + modifyIndex(indexState, "f", "side"); + + ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, + worktreeState == WorktreeState.Bare); + if (worktreeState != WorktreeState.Bare) + merger.setWorkingTreeIterator(new FileTreeIterator(db)); + try { + boolean expectSuccess = true; + if (!(indexState == IndexState.Bare + || indexState == IndexState.Missing || indexState == IndexState.SameAsHead)) + // index is dirty + expectSuccess = false; + else if (worktreeState == WorktreeState.DifferentFromHeadAndOther + || worktreeState == WorktreeState.SameAsOther) + // workingtree is dirty + expectSuccess = false; + + assertEquals(Boolean.valueOf(expectSuccess), + Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 }))); + assertEquals(MergeStrategy.RECURSIVE, strategy); + if (!expectSuccess) + // if the merge was not successful skip testing the state of index and workingtree + return; + assertEquals( + "1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side", + contentAsString(db, merger.getResultTreeId(), "f")); + if (indexState != IndexState.Bare) + assertEquals( + "[f, mode:100644, content:1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side\n]", + indexState(RepositoryTestCase.CONTENT)); + if (worktreeState != WorktreeState.Bare + && worktreeState != WorktreeState.Missing) + assertEquals( + "1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side\n", + read("f")); + } catch (NoMergeBaseException e) { + assertEquals(MergeStrategy.RESOLVE, strategy); + assertEquals(e.getReason(), + MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED); + } + } + + void modifyIndex(IndexState indexState, String path, String other) + throws Exception { + RevBlob blob; + switch (indexState) { + case Missing: + setIndex(null, path); + break; + case SameAsHead: + setIndex(contentId(Constants.HEAD, path), path); + break; + case SameAsOther: + setIndex(contentId(other, path), path); + break; + case SameAsWorkTree: + blob = db_t.blob(read(path)); + setIndex(blob, path); + break; + case DifferentFromHeadAndOtherAndWorktree: + blob = db_t.blob(Integer.toString(counter++)); + setIndex(blob, path); + break; + case Bare: + File file = new File(db.getDirectory(), "index"); + if (!file.exists()) + return; + db.close(); + file.delete(); + db = new FileRepository(db.getDirectory()); + db_t = new TestRepository(db); + break; + } + } + + private void setIndex(final ObjectId id, String path) + throws MissingObjectException, IOException { + DirCache lockedDircache; + DirCacheEditor dcedit; + + lockedDircache = db.lockDirCache(); + dcedit = lockedDircache.editor(); + try { + if (id != null) { + final ObjectLoader contLoader = db.newObjectReader().open(id); + dcedit.add(new DirCacheEditor.PathEdit(path) { + @Override + public void apply(DirCacheEntry ent) { + ent.setFileMode(FileMode.REGULAR_FILE); + ent.setLength(contLoader.getSize()); + ent.setObjectId(id); + } + }); + } else + dcedit.add(new DirCacheEditor.DeletePath(path)); + } finally { + dcedit.commit(); + } + } + + private ObjectId contentId(String revName, String path) throws Exception { + RevCommit headCommit = db_t.getRevWalk().parseCommit( + db.resolve(revName)); + db_t.parseBody(headCommit); + return db_t.get(headCommit.getTree(), path).getId(); + } + + void modifyWorktree(WorktreeState worktreeState, String path, String other) + throws Exception { + FileOutputStream fos = null; + ObjectId bloblId; + + try { + switch (worktreeState) { + case Missing: + new File(db.getWorkTree(), path).delete(); + break; + case DifferentFromHeadAndOther: + write(new File(db.getWorkTree(), path), + Integer.toString(counter++)); + break; + case SameAsHead: + bloblId = contentId(Constants.HEAD, path); + fos = new FileOutputStream(new File(db.getWorkTree(), path)); + db.newObjectReader().open(bloblId).copyTo(fos); + break; + case SameAsOther: + bloblId = contentId(other, path); + fos = new FileOutputStream(new File(db.getWorkTree(), path)); + db.newObjectReader().open(bloblId).copyTo(fos); + break; + case Bare: + if (db.isBare()) + return; + File workTreeFile = db.getWorkTree(); + db.getConfig().setBoolean("core", null, "bare", true); + db.getDirectory().renameTo(new File(workTreeFile, "test.git")); + db = new FileRepository(new File(workTreeFile, "test.git")); + db_t = new TestRepository(db); + } + } finally { + if (fos != null) + fos.close(); + } + } + + private boolean validateStates(IndexState indexState, + WorktreeState worktreeState) { + if (worktreeState == WorktreeState.Bare + && indexState != IndexState.Bare) + return false; + if (worktreeState != WorktreeState.Bare + && indexState == IndexState.Bare) + return false; + if (worktreeState != WorktreeState.DifferentFromHeadAndOther + && indexState == IndexState.SameAsWorkTree) + // would be a duplicate: the combination WorktreeState.X and + // IndexState.X already covered this + return false; + return true; + } + + private String contentAsString(Repository r, ObjectId treeId, String path) + throws MissingObjectException, IOException { + TreeWalk tw = new TreeWalk(r); + tw.addTree(treeId); + tw.setFilter(PathFilter.create(path)); + tw.setRecursive(true); + if (!tw.next()) + return null; + AnyObjectId blobId = tw.getObjectId(0); + + StringBuilder result = new StringBuilder(); + BufferedReader br = null; + ObjectReader or = r.newObjectReader(); + try { + br = new BufferedReader(new InputStreamReader(or.open(blobId) + .openStream())); + String line; + boolean first = true; + while ((line = br.readLine()) != null) { + if (!first) + result.append('\n'); + result.append(line); + first = false; + } + return result.toString(); + } finally { + if (br != null) + br.close(); + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/ResolveMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/ResolveMergerTest.java index 8c10c731c..d8ef2dd6b 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/ResolveMergerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/ResolveMergerTest.java @@ -54,7 +54,10 @@ import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.api.MergeResult.MergeStatus; import org.eclipse.jgit.api.errors.CheckoutConflictException; +import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.errors.NoMergeBaseException; +import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; import org.eclipse.jgit.revwalk.RevCommit; @@ -72,6 +75,9 @@ public class ResolveMergerTest extends RepositoryTestCase { @DataPoint public static MergeStrategy resolve = MergeStrategy.RESOLVE; + @DataPoint + public static MergeStrategy recursive = MergeStrategy.RECURSIVE; + @Theory public void failingPathsShouldNotResultInOKReturnValue( MergeStrategy strategy) throws Exception { @@ -396,6 +402,66 @@ public void checkMergeMergeableFilesWithTreeInIndex(MergeStrategy strategy) } } + /** + * Merging after criss-cross merges. In this case we merge together two + * commits which have two equally good common ancestors + * + * @param strategy + * @throws Exception + */ + @Theory + public void checkMergeCrissCross(MergeStrategy strategy) throws Exception { + Git git = Git.wrap(db); + + writeTrashFile("1", "1\n2\n3"); + git.add().addFilepattern("1").call(); + RevCommit first = git.commit().setMessage("added 1").call(); + + writeTrashFile("1", "1master\n2\n3"); + RevCommit masterCommit = git.commit().setAll(true) + .setMessage("modified 1 on master").call(); + + writeTrashFile("1", "1master2\n2\n3"); + git.commit().setAll(true) + .setMessage("modified 1 on master again").call(); + + git.checkout().setCreateBranch(true).setStartPoint(first) + .setName("side").call(); + writeTrashFile("1", "1\n2\na\nb\nc\n3side"); + RevCommit sideCommit = git.commit().setAll(true) + .setMessage("modified 1 on side").call(); + + writeTrashFile("1", "1\n2\n3side2"); + git.commit().setAll(true) + .setMessage("modified 1 on side again").call(); + + MergeResult result = git.merge().setStrategy(strategy) + .include(masterCommit).call(); + assertEquals(MergeStatus.MERGED, result.getMergeStatus()); + result.getNewHead(); + git.checkout().setName("master").call(); + result = git.merge().setStrategy(strategy).include(sideCommit).call(); + assertEquals(MergeStatus.MERGED, result.getMergeStatus()); + + // we have two branches which are criss-cross merged. Try to merge the + // tips. This should succeed with RecursiveMerge and fail with + // ResolveMerge + try { + MergeResult mergeResult = git.merge().setStrategy(strategy) + .include(git.getRepository().getRef("refs/heads/side")) + .call(); + assertEquals(MergeStrategy.RECURSIVE, strategy); + assertEquals(MergeResult.MergeStatus.MERGED, + mergeResult.getMergeStatus()); + assertEquals("1master2\n2\n3side2\n", read("1")); + } catch (JGitInternalException e) { + assertEquals(MergeStrategy.RESOLVE, strategy); + assertTrue(e.getCause() instanceof NoMergeBaseException); + assertEquals(((NoMergeBaseException) e.getCause()).getReason(), + MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED); + } + } + @Theory public void checkLockedFilesToBeDeleted(MergeStrategy strategy) throws Exception { diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index b3ef62ad0..cc0b76623 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -288,6 +288,8 @@ mergeConflictOnNotes=Merge conflict on note {0}. base = {1}, ours = {2}, theirs mergeStrategyAlreadyExistsAsDefault=Merge strategy "{0}" already exists as a default strategy mergeStrategyDoesNotSupportHeads=merge strategy {0} does not support {1} heads to be merged into HEAD mergeUsingStrategyResultedInDescription=Merge of revisions {0} with base {1} using strategy {2} resulted in: {3}. {4} +mergeRecursiveReturnedNoCommit=Merge returned no commit:\n Depth {0}\n Head one {1}\n Head two {2} +mergeRecursiveTooManyMergeBasesFor = "More than {0} merge bases for:\n a {1}\n b {2} found:\n count {3}" minutesAgo={0} minutes ago missingAccesskey=Missing accesskey. missingConfigurationForKey=No value for key {0} found in configuration @@ -313,6 +315,7 @@ noApplyInDelete=No apply in delete noClosingBracket=No closing {0} found for {1} at index {2}. noHEADExistsAndNoExplicitStartingRevisionWasSpecified=No HEAD exists and no explicit starting revision was specified noHMACsupport=No {0} support: {1} +noMergeBase=No merge base could be determined. Reason={0}. {1} noMergeHeadSpecified=No merge head specified noSuchRef=no such ref notABoolean=Not a boolean: {0} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoMergeBaseException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoMergeBaseException.java new file mode 100644 index 000000000..df90e6900 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoMergeBaseException.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2013, Christian Halstrick + * 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.errors; + +import java.io.IOException; +import java.text.MessageFormat; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.merge.RecursiveMerger; + +/** + * Exception thrown if a merge fails because no merge base could be determined. + */ +public class NoMergeBaseException extends IOException { + private static final long serialVersionUID = 1L; + + private MergeBaseFailureReason reason; + + /** + * An enum listing the different reason why no merge base could be + * determined. + */ + public static enum MergeBaseFailureReason { + /** + * Multiple merge bases have been found (e.g. the commits to be merged + * have multiple common predecessors) but the merge strategy doesn't + * support this (e.g. ResolveMerge) + */ + MULTIPLE_MERGE_BASES_NOT_SUPPORTED, + + /** + * The number of merge bases exceeds {@link RecursiveMerger#MAX_BASES} + */ + TOO_MANY_MERGE_BASES, + + /** + * In order to find a single merge base it may required to merge + * together multiple common predecessors. If during these merges + * conflicts occur the merge fails with this reason + */ + CONFLICTS_DURING_MERGE_BASE_CALCULATION + } + + + /** + * Construct a NoMergeBase exception + * + * @param reason + * the reason why no merge base could be found + * @param message + * a text describing the problem + */ + public NoMergeBaseException(MergeBaseFailureReason reason, String message) { + super(MessageFormat.format(JGitText.get().noMergeBase, + reason.toString(), message)); + this.reason = reason; + } + + /** + * Construct a NoMergeBase exception + * + * @param reason + * the reason why no merge base could be found + * @param message + * a text describing the problem + * @param why + * an exception causing this error + */ + public NoMergeBaseException(MergeBaseFailureReason reason, String message, + Throwable why) { + super(MessageFormat.format(JGitText.get().noMergeBase, + reason.toString(), message)); + this.reason = reason; + initCause(why); + } + + /** + * @return the reason why no merge base could be found + */ + public MergeBaseFailureReason getReason() { + return reason; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index d032bbc88..f8ea5ff93 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2010, 2013 Sasa Zivkov + * Copyright (C) 2012, Research In Motion Limited * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -349,6 +350,8 @@ public static JGitText get() { /***/ public String mergeStrategyAlreadyExistsAsDefault; /***/ public String mergeStrategyDoesNotSupportHeads; /***/ public String mergeUsingStrategyResultedInDescription; + /***/ public String mergeRecursiveReturnedNoCommit; + /***/ public String mergeRecursiveTooManyMergeBasesFor; /***/ public String minutesAgo; /***/ public String missingAccesskey; /***/ public String missingConfigurationForKey; @@ -374,6 +377,7 @@ public static JGitText get() { /***/ public String noClosingBracket; /***/ public String noHEADExistsAndNoExplicitStartingRevisionWasSpecified; /***/ public String noHMACsupport; + /***/ public String noMergeBase; /***/ public String noMergeHeadSpecified; /***/ public String noSuchRef; /***/ public String notABoolean; 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 2e6fc4137..3b64ddd36 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeStrategy.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeStrategy.java @@ -1,6 +1,7 @@ /* * Copyright (C) 2008-2009, Google Inc. * Copyright (C) 2009, Matthias Sohn + * Copyright (C) 2012, Research In Motion Limited * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -66,9 +67,18 @@ 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 */ + /** + * Simple strategy to merge paths. It tries to merge also contents. Multiple + * merge bases are not supported + */ public static final ThreeWayMergeStrategy RESOLVE = new StrategyResolve(); + /** + * Recursive strategy to merge paths. It tries to merge also contents. + * Multiple merge bases are supported + */ + public static final ThreeWayMergeStrategy RECURSIVE = new StrategyRecursive(); + private static final HashMap STRATEGIES = new HashMap(); static { @@ -76,6 +86,7 @@ public abstract class MergeStrategy { register(THEIRS); register(SIMPLE_TWO_WAY_IN_CORE); register(RESOLVE); + register(RECURSIVE); } /** @@ -103,7 +114,8 @@ public static void register(final MergeStrategy imp) { public static synchronized void register(final String name, final MergeStrategy imp) { if (STRATEGIES.containsKey(name)) - throw new IllegalArgumentException(MessageFormat.format(JGitText.get().mergeStrategyAlreadyExistsAsDefault, name)); + throw new IllegalArgumentException(MessageFormat.format( + JGitText.get().mergeStrategyAlreadyExistsAsDefault, name)); STRATEGIES.put(name, imp); } @@ -146,7 +158,7 @@ public static synchronized MergeStrategy[] get() { /** * Create a new merge instance. - * + * * @param db * repository database the merger will read from, and eventually * write results back to. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java index fd94cfb23..04c29e6e8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java @@ -47,6 +47,8 @@ import java.text.MessageFormat; import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.NoMergeBaseException; +import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; @@ -186,19 +188,19 @@ public boolean merge(final AnyObjectId... tips) throws IOException { /** * Create an iterator to walk the merge base of two commits. * - * @param aIdx - * index of the first commit in {@link #sourceObjects}. - * @param bIdx - * index of the second commit in {@link #sourceObjects}. + * @param a + * the first commit in {@link #sourceObjects}. + * @param b + * the second commit in {@link #sourceObjects}. * @return the new iterator * @throws IncorrectObjectTypeException * one of the input objects is not a commit. * @throws IOException * objects are missing or multiple merge bases were found. */ - protected AbstractTreeIterator mergeBase(final int aIdx, final int bIdx) + protected AbstractTreeIterator mergeBase(RevCommit a, RevCommit b) throws IOException { - RevCommit base = getBaseCommit(aIdx, bIdx); + RevCommit base = getBaseCommit(a, b); return (base == null) ? new EmptyTreeIterator() : openTree(base.getTree()); } @@ -224,18 +226,38 @@ public RevCommit getBaseCommit(final int aIdx, final int bIdx) if (sourceCommits[bIdx] == null) throw new IncorrectObjectTypeException(sourceObjects[bIdx], Constants.TYPE_COMMIT); + return getBaseCommit(sourceCommits[aIdx], sourceCommits[bIdx]); + } + + /** + * Return the merge base of two commits. + * + * @param a + * the first commit in {@link #sourceObjects}. + * @param b + * the second commit in {@link #sourceObjects}. + * @return the merge base of two commits + * @throws IncorrectObjectTypeException + * one of the input objects is not a commit. + * @throws IOException + * objects are missing or multiple merge bases were found. + */ + protected RevCommit getBaseCommit(RevCommit a, RevCommit b) + throws IncorrectObjectTypeException, IOException { walk.reset(); walk.setRevFilter(RevFilter.MERGE_BASE); - walk.markStart(sourceCommits[aIdx]); - walk.markStart(sourceCommits[bIdx]); + walk.markStart(a); + walk.markStart(b); final RevCommit base = walk.next(); if (base == null) return null; final RevCommit base2 = walk.next(); if (base2 != null) { - throw new IOException(MessageFormat.format(JGitText.get().multipleMergeBasesFor - , sourceCommits[aIdx].name(), sourceCommits[bIdx].name() - , base.name(), base2.name())); + throw new NoMergeBaseException( + MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED, + MessageFormat.format( + JGitText.get().multipleMergeBasesFor, a.name(), b.name(), + base.name(), base2.name())); } return base; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java new file mode 100644 index 000000000..d68be3540 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2012, Research In Motion Limited + * Copyright (C) 2012, Christian Halstrick + * 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. + */ + +/* + * Contributors: + * George Young - initial API and implementation + * Christian Halstrick - initial API and implementation + */ +package org.eclipse.jgit.merge; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.NoMergeBaseException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.filter.RevFilter; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.WorkingTreeIterator; + +/** + * A three-way merger performing a content-merge if necessary across multiple + * bases using recursion + * + * This merger extends the resolve merger and does several things differently: + * + * - allow more than one merge base, up to a maximum + * + * - uses "Lists" instead of Arrays for chained types + * + * - recursively merges the merge bases together to compute a usable base + * + */ + +public class RecursiveMerger extends ResolveMerger { + static Logger log = Logger.getLogger(RecursiveMerger.class.toString()); + + /** + * The maximum number of merge bases. This merge will stop when the number + * of merge bases exceeds this value + */ + public final int MAX_BASES = 200; + + private PersonIdent ident = new PersonIdent(db); + + /** + * Normal recursive merge when you want a choice of DirCache placement + * inCore + * + * @param local + * @param inCore + */ + protected RecursiveMerger(Repository local, boolean inCore) { + super(local, inCore); + } + + /** + * Normal recursive merge, implies not inCore + * + * @param local + */ + protected RecursiveMerger(Repository local) { + this(local, false); + } + + /** + * Get a single base commit for two given commits. If the two source commits + * have more than one base commit recursively merge the base commits + * together until you end up with a single base commit. + * + * @throws IOException + * @throws IncorrectObjectTypeException + */ + @Override + protected RevCommit getBaseCommit(RevCommit a, RevCommit b) + throws IncorrectObjectTypeException, IOException { + return getBaseCommit(a, b, 0); + } + + /** + * Get a single base commit for two given commits. If the two source commits + * have more than one base commit recursively merge the base commits + * together until a virtual common base commit has been found. + * + * @param a + * the first commit to be merged + * @param b + * the second commit to be merged + * @param callDepth + * the callDepth when this method is called recursively + * @return the merge base of two commits + * @throws IOException + * @throws IncorrectObjectTypeException + * one of the input objects is not a commit. + * @throws NoMergeBaseException + * too many merge bases are found or the computation of a common + * merge base failed (e.g. because of a conflict). + */ + protected RevCommit getBaseCommit(RevCommit a, RevCommit b, int callDepth) + throws IOException { + ArrayList baseCommits = new ArrayList(); + walk.reset(); + walk.setRevFilter(RevFilter.MERGE_BASE); + walk.markStart(a); + walk.markStart(b); + RevCommit c; + while ((c = walk.next()) != null) + baseCommits.add(c); + + if (baseCommits.isEmpty()) + return null; + if (baseCommits.size() == 1) + return baseCommits.get(0); + if (baseCommits.size() >= MAX_BASES) + throw new NoMergeBaseException(NoMergeBaseException.MergeBaseFailureReason.TOO_MANY_MERGE_BASES, MessageFormat.format( + JGitText.get().mergeRecursiveTooManyMergeBasesFor, + Integer.valueOf(MAX_BASES), a.name(), b.name(), + Integer.valueOf(baseCommits.size()))); + + // We know we have more than one base commit. We have to do merges now + // to determine a single base commit. We don't want to spoil the current + // dircache and working tree with the results of this intermediate + // merges. Therefore set the dircache to a new in-memory dircache and + // disable that we update the working-tree. We set this back to the + // original values once a single base commit is created. + RevCommit currentBase = baseCommits.get(0); + DirCache oldDircache = dircache; + boolean oldIncore = inCore; + WorkingTreeIterator oldWTreeIt = workingTreeIterator; + workingTreeIterator = null; + try { + dircache = dircacheFromTree(currentBase.getTree()); + inCore = true; + + List parents = new ArrayList(); + parents.add(currentBase); + for (int commitIdx = 1; commitIdx < baseCommits.size(); commitIdx++) { + RevCommit nextBase = baseCommits.get(commitIdx); + if (commitIdx >= MAX_BASES) + throw new NoMergeBaseException( + NoMergeBaseException.MergeBaseFailureReason.TOO_MANY_MERGE_BASES, + MessageFormat.format( + JGitText.get().mergeRecursiveTooManyMergeBasesFor, + Integer.valueOf(MAX_BASES), a.name(), b.name(), + Integer.valueOf(baseCommits.size()))); + parents.add(nextBase); + if (mergeTrees( + openTree(getBaseCommit(currentBase, nextBase, + callDepth + 1).getTree()), + currentBase.getTree(), + nextBase.getTree())) + currentBase = createCommitForTree(resultTree, parents); + else + throw new NoMergeBaseException( + NoMergeBaseException.MergeBaseFailureReason.CONFLICTS_DURING_MERGE_BASE_CALCULATION, + MessageFormat.format( + JGitText.get().mergeRecursiveTooManyMergeBasesFor, + Integer.valueOf(MAX_BASES), a.name(), + b.name(), + Integer.valueOf(baseCommits.size()))); + } + } finally { + inCore = oldIncore; + dircache = oldDircache; + workingTreeIterator = oldWTreeIt; + } + return currentBase; + } + + /** + * Create a new commit by explicitly specifying the content tree and the + * parents. The commit message is not set and author/committer are set to + * the current user. + * + * @param tree + * the tree this commit should capture + * @param parents + * the list of parent commits + * @return a new (persisted) commit + * @throws IOException + */ + private RevCommit createCommitForTree(ObjectId tree, List parents) + throws IOException { + CommitBuilder c = new CommitBuilder(); + c.setParentIds(parents); + c.setTreeId(tree); + c.setAuthor(ident); + c.setCommitter(ident); + ObjectInserter odi = db.newObjectInserter(); + ObjectId newCommitId = odi.insert(c); + odi.flush(); + RevCommit ret = walk.lookupCommit(newCommitId); + walk.parseHeaders(ret); + return ret; + } + + /** + * Create a new in memory dircache which has the same content as a given + * tree. + * + * @param treeId + * the tree which should be used to fill the dircache + * @return a new in memory dircache + * @throws IOException + */ + private DirCache dircacheFromTree(ObjectId treeId) throws IOException { + DirCache ret = DirCache.newInCore(); + DirCacheBuilder builder = ret.builder(); + TreeWalk tw = new TreeWalk(db); + tw.addTree(treeId); + tw.setRecursive(true); + while (tw.next()) { + DirCacheEntry e = new DirCacheEntry(tw.getRawPath()); + e.setFileMode(tw.getFileMode(0)); + e.setObjectId(tw.getObjectId(0)); + builder.add(e); + } + builder.finish(); + return ret; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java index 433458a27..bea211933 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -1,6 +1,7 @@ /* * Copyright (C) 2010, Christian Halstrick , * Copyright (C) 2010-2012, Matthias Sohn + * Copyright (C) 2012, Research In Motion Limited * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -80,6 +81,8 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.NameConflictTreeWalk; import org.eclipse.jgit.treewalk.WorkingTreeIterator; @@ -104,7 +107,10 @@ public enum MergeFailureReason { private NameConflictTreeWalk tw; - private String commitNames[]; + /** + * string versions of a list of commit SHA1s + */ + protected String commitNames[]; private static final int T_BASE = 0; @@ -118,7 +124,10 @@ public enum MergeFailureReason { private DirCacheBuilder builder; - private ObjectId resultTree; + /** + * merge result as tree + */ + protected ObjectId resultTree; private List unmergedPaths = new ArrayList(); @@ -134,13 +143,38 @@ public enum MergeFailureReason { private boolean enterSubtree; - private boolean inCore; + /** + * Set to true if this merge should work in-memory. The repos dircache and + * workingtree are not touched by this method. Eventually needed files are + * created as temporary files and a new empty, in-memory dircache will be + * used instead the repo's one. Often used for bare repos where the repo + * doesn't even have a workingtree and dircache. + */ + protected boolean inCore; - private DirCache dircache; + /** + * Set to true if this merger should use the default dircache of the + * repository and should handle locking and unlocking of the dircache. If + * this merger should work in-core or if an explicit dircache was specified + * during construction then this field is set to false. + */ + protected boolean implicitDirCache; - private WorkingTreeIterator workingTreeIterator; + /** + * Directory cache + */ + protected DirCache dircache; - private MergeAlgorithm mergeAlgorithm; + /** + * The iterator to access the working tree. If set to null this + * merger will not touch the working tree. + */ + protected WorkingTreeIterator workingTreeIterator; + + /** + * our merge algorithm + */ + protected MergeAlgorithm mergeAlgorithm; /** * @param local @@ -153,11 +187,14 @@ protected ResolveMerger(Repository local, boolean inCore) { ConfigConstants.CONFIG_KEY_ALGORITHM, SupportedAlgorithm.HISTOGRAM); mergeAlgorithm = new MergeAlgorithm(DiffAlgorithm.getAlgorithm(diffAlg)); - commitNames = new String[] { "BASE", "OURS", "THEIRS" }; + commitNames = new String[] { "BASE", "OURS", "THEIRS" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ this.inCore = inCore; if (inCore) { + implicitDirCache = false; dircache = DirCache.newInCore(); + } else { + implicitDirCache = true; } } @@ -170,67 +207,11 @@ protected ResolveMerger(Repository local) { @Override protected boolean mergeImpl() throws IOException { - boolean implicitDirCache = false; - - if (dircache == null) { + if (implicitDirCache) dircache = getRepository().lockDirCache(); - implicitDirCache = true; - } try { - builder = dircache.builder(); - DirCacheBuildIterator buildIt = new DirCacheBuildIterator(builder); - - tw = new NameConflictTreeWalk(db); - tw.addTree(mergeBase()); - tw.addTree(sourceTrees[0]); - tw.addTree(sourceTrees[1]); - tw.addTree(buildIt); - if (workingTreeIterator != null) - tw.addTree(workingTreeIterator); - - 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), - (workingTreeIterator == null) ? null : tw.getTree(T_FILE, WorkingTreeIterator.class))) { - cleanUp(); - return false; - } - if (tw.isSubtree() && enterSubtree) - tw.enterSubtree(); - } - - if (!inCore) { - // 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(); - - // 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; - - } else { - builder.finish(); - builder = null; - } - - if (getUnmergedPaths().isEmpty() && !failed()) { - resultTree = dircache.writeTree(getObjectInserter()); - return true; - } else { - resultTree = null; - return false; - } + return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1]); } finally { if (implicitDirCache) dircache.unlock(); @@ -279,14 +260,15 @@ private void createDir(File f) throws IOException { /** * 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. In case if inCore operation just clear - * the history of modified files. + * contained only stage 0. In case if inCore operation just clear the + * history of modified files. * * @throws IOException * @throws CorruptObjectException * @throws NoWorkTreeException */ - private void cleanUp() throws NoWorkTreeException, CorruptObjectException, IOException { + private void cleanUp() throws NoWorkTreeException, CorruptObjectException, + IOException { if (inCore) { modifiedFiles.clear(); return; @@ -298,7 +280,10 @@ private void cleanUp() throws NoWorkTreeException, CorruptObjectException, IOExc while(mpathsIt.hasNext()) { String mpath=mpathsIt.next(); DirCacheEntry entry = dc.getEntry(mpath); - FileOutputStream fos = new FileOutputStream(new File(db.getWorkTree(), mpath)); + if (entry == null) + continue; + FileOutputStream fos = new FileOutputStream(new File( + db.getWorkTree(), mpath)); try { or.open(entry.getObjectId()).copyTo(fos); } finally { @@ -610,6 +595,9 @@ private MergeResult contentMerge(CanonicalTreeParser base, } private boolean isIndexDirty() { + if (inCore) + return false; + final int modeI = tw.getRawMode(T_INDEX); final int modeO = tw.getRawMode(T_OURS); @@ -623,7 +611,7 @@ private boolean isIndexDirty() { } private boolean isWorktreeDirty(WorkingTreeIterator work) { - if (inCore || work == null) + if (work == null) return false; final int modeF = tw.getRawMode(T_FILE); @@ -862,19 +850,20 @@ public boolean failed() { /** * 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. + * not set explicitly and if this merger doesn't work in-core, 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; + implicitDirCache = false; } /** @@ -891,4 +880,73 @@ public void setDirCache(DirCache dc) { public void setWorkingTreeIterator(WorkingTreeIterator workingTreeIterator) { this.workingTreeIterator = workingTreeIterator; } + + + /** + * The resolve conflict way of three way merging + * + * @param baseTree + * @param headTree + * @param mergeTree + * @return whether the trees merged cleanly + * @throws IOException + */ + protected boolean mergeTrees(AbstractTreeIterator baseTree, + RevTree headTree, RevTree mergeTree) throws IOException { + + builder = dircache.builder(); + DirCacheBuildIterator buildIt = new DirCacheBuildIterator(builder); + + tw = new NameConflictTreeWalk(db); + tw.addTree(baseTree); + tw.addTree(headTree); + tw.addTree(mergeTree); + tw.addTree(buildIt); + if (workingTreeIterator != null) + tw.addTree(workingTreeIterator); + + 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), + (workingTreeIterator == null) ? null : tw.getTree(T_FILE, + WorkingTreeIterator.class))) { + cleanUp(); + return false; + } + if (tw.isSubtree() && enterSubtree) + tw.enterSubtree(); + } + + if (!inCore) { + // 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(); + + // 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; + + } else { + builder.finish(); + builder = null; + } + + if (getUnmergedPaths().isEmpty() && !failed()) { + resultTree = dircache.writeTree(getObjectInserter()); + return true; + } else { + resultTree = null; + return false; + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyRecursive.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyRecursive.java new file mode 100644 index 000000000..11c8bca5d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyRecursive.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2012, Research In Motion Limited + * 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 StrategyRecursive extends StrategyResolve { + + @Override + public ThreeWayMerger newMerger(Repository db) { + return new RecursiveMerger(db, false); + } + + @Override + public ThreeWayMerger newMerger(Repository db, boolean inCore) { + return new RecursiveMerger(db, inCore); + } + + @Override + public String getName() { + return "recursive"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java index 521f8963a..1ad791bb7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2009, Google Inc. + * Copyright (C) 2012, Research In Motion Limited * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -118,6 +119,6 @@ public boolean merge(final AnyObjectId... tips) throws IOException { protected AbstractTreeIterator mergeBase() throws IOException { if (baseTree != null) return openTree(baseTree); - return mergeBase(0, 1); + return mergeBase(sourceCommits[0], sourceCommits[1]); } }