blob: 039a6e8cfc5877e6ca6a7528d1e2428a32c52dbe [file] [log] [blame]
/*
* Copyright (C) 2012, Christian Halstrick <christian.halstrick@sap.com>
* 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 static org.junit.Assert.assertTrue;
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.internal.storage.file.FileRepository;
import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
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.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<FileRepository> 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.
*
* <pre>
* m0--m1--m2
* \ \/
* \ /\
* s1--s2
* </pre>
*/
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. m1 and s1 are the two root
* commits of the repo. In master and side different files are touched.
* No need to do a real content merge.
*
* <pre>
* m1--m2
* \/
* /\
* s1--s2
* </pre>
*/
public void crissCrossMerge_twoRoots(MergeStrategy strategy,
IndexState indexState, WorktreeState worktreeState)
throws Exception {
if (!validateStates(indexState, worktreeState))
return;
// fill the repo
BranchBuilder master = db_t.branch("master");
BranchBuilder side = db_t.branch("side");
RevCommit m1 = master.commit().add("m", "m1").message("m1").create();
db_t.getRevWalk().parseCommit(m1);
RevCommit s1 = side.commit().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 s2 don't contain the same content.
*
* <pre>
* m0--m1--m2
* \ \/
* \ /\
* s1--s2
* </pre>
*/
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(LocalDiskRepositoryTestCase.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 should be mergeable but only if the automerge of m1 and s1
* is choosen as parent. Choosing m0 as parent would not be sufficient (in contrast to the merge in
* crissCrossMerge_mergeable). m2 and s2 contain branch specific conflict resolutions. Therefore m2
* and s2 don't contain the same content.
*
* <pre>
* m0--m1--m2
* \ \/
* \ /\
* s1--s2
* </pre>
*/
public void crissCrossMerge_mergeable2(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\n")
.message("m0")
.create();
RevCommit m1 = master.commit().add("f", "1-master\n2\n3\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-side\n")
.message("s1").create();
RevCommit s2 = side.commit().parent(m1)
.add("f", "1-master\n2\n3-side-r\n")
.message("s2(merge)")
.create();
RevCommit m2 = master.commit().parent(s1)
.add("f", "1-master-r\n2\n3-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-r\n2\n3-side-r",
contentAsString(db, merger.getResultTreeId(), "f"));
if (indexState != IndexState.Bare)
assertEquals(
"[f, mode:100644, content:1-master-r\n2\n3-side-r\n]",
indexState(LocalDiskRepositoryTestCase.CONTENT));
if (worktreeState != WorktreeState.Bare
&& worktreeState != WorktreeState.Missing)
assertEquals(
"1-master-r\n2\n3-side-r\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. m1 and s1 are not mergeable
* without conflicts. The same file is modified in both branches. The
* modifications should be mergeable but only if the merge result of
* merging m1 and s1 is choosen as parent (including the conflict markers).
*
* <pre>
* m0--m1--m2
* \ \/
* \ /\
* s1--s2
* </pre>
*/
public void crissCrossMerge_ParentsNotMergeable(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\n").message("m0")
.create();
RevCommit m1 = master.commit().add("f", "1\nx(master)\n2\n3\n")
.message("m1").create();
db_t.getRevWalk().parseCommit(m1);
BranchBuilder side = db_t.branch("side");
RevCommit s1 = side.commit().parent(m0)
.add("f", "1\nx(side)\n2\n3\ny(side)\n")
.message("s1").create();
RevCommit s2 = side.commit().parent(m1)
.add("f", "1\nx(side)\n2\n3\ny(side-again)\n")
.message("s2(merge)")
.create();
RevCommit m2 = master.commit().parent(s1)
.add("f", "1\nx(side)\n2\n3\ny(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("Merge didn't return as expected: strategy:"
+ strategy.getName() + ", indexState:" + indexState
+ ", worktreeState:" + worktreeState + " . ",
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\nx(side)\n2\n3\ny(side-again)",
contentAsString(db, merger.getResultTreeId(), "f"));
if (indexState != IndexState.Bare)
assertEquals(
"[f, mode:100644, content:1\nx(side)\n2\n3\ny(side-again)\n]",
indexState(LocalDiskRepositoryTestCase.CONTENT));
if (worktreeState != WorktreeState.Bare
&& worktreeState != WorktreeState.Missing)
assertEquals("1\nx(side)\n2\n3\ny(side-again)\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 should be mergeable but only if the automerge of m1 and s1
* is choosen as parent. On both branches delete and modify files untouched on the other branch.
* On both branches create new files. Make sure these files are correctly merged and
* exist in the workingtree.
*
* <pre>
* m0--m1--m2
* \ \/
* \ /\
* s1--s2
* </pre>
*/
public void crissCrossMerge_checkOtherFiles(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\n").add("m.m", "0")
.add("m.d", "0").add("s.m", "0").add("s.d", "0").message("m0")
.create();
RevCommit m1 = master.commit().add("f", "1-master\n2\n3\n")
.add("m.c", "0").add("m.m", "1").rm("m.d").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-side\n")
.add("s.c", "0").add("s.m", "1").rm("s.d").message("s1")
.create();
RevCommit s2 = side.commit().parent(m1)
.add("f", "1-master\n2\n3-side-r\n").add("m.m", "1")
.add("m.c", "0").rm("m.d").message("s2(merge)").create();
RevCommit m2 = master.commit().parent(s1)
.add("f", "1-master-r\n2\n3-side\n").add("s.m", "1")
.add("s.c", "0").rm("s.d").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-r\n2\n3-side-r",
contentAsString(db, merger.getResultTreeId(), "f"));
if (indexState != IndexState.Bare)
assertEquals(
"[f, mode:100644, content:1-master-r\n2\n3-side-r\n][m.c, mode:100644, content:0][m.m, mode:100644, content:1][s.c, mode:100644, content:0][s.m, mode:100644, content:1]",
indexState(LocalDiskRepositoryTestCase.CONTENT));
if (worktreeState != WorktreeState.Bare
&& worktreeState != WorktreeState.Missing) {
assertEquals(
"1-master-r\n2\n3-side-r\n",
read("f"));
assertTrue(check("s.c"));
assertFalse(check("s.d"));
assertTrue(check("s.m"));
assertTrue(check("m.c"));
assertFalse(check("m.d"));
assertTrue(check("m.m"));
}
} 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.
*
* <pre>
* m0--m1--m2
* \ \/
* \ /\
* s1--s2
* </pre>
*/
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(LocalDiskRepositoryTestCase.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.
*
* <pre>
* m1-----m2
* / \/ /
* / /\ /
* m0--o1 x
* \ \/ \
* \ /\ \
* s1-----s2
* </pre>
*/
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(LocalDiskRepositoryTestCase.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 {
AnyObjectId blobId;
try (TreeWalk tw = new TreeWalk(r)) {
tw.addTree(treeId);
tw.setFilter(PathFilter.create(path));
tw.setRecursive(true);
if (!tw.next()) {
return null;
}
blobId = tw.getObjectId(0);
}
StringBuilder result = new StringBuilder();
ObjectReader or = r.newObjectReader();
try (BufferedReader 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();
}
}
}