Skip to content

Commit 1b9911d

Browse files
author
Marija Savtchouk
committed
Allow dir/file conflicts in virtual base commit on recursive merge.
If RecursiveMerger finds multiple base commits, it tries to compute the virtual ancestor to use as a base for the three way merge. Currently, the content conflicts between ancestors are ignored (file staged with the conflict markers). If the path is a file in one ancestor and a dir in the other, it results in NoMergeBaseException (CONFLICTS_DURING_MERGE_BASE_CALCULATION). Allow these conflicts by ignoring this unmerged path in the virtual base. The merger will compute diff in the children instead and it can be further fixed manually if needed. Change-Id: Id59648ae1d6bdf300b26fff513c3204317b755ab Signed-off-by: Marija Savtchouk <[email protected]>
1 parent fe4b2a4 commit 1b9911d

File tree

2 files changed

+277
-10
lines changed

2 files changed

+277
-10
lines changed

org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,6 +1384,270 @@ public void checkMergeConflictInVirtualAncestor(
13841384
git.merge().include(commitB).call();
13851385
}
13861386

1387+
/**
1388+
* Merging two commits with a file/dir conflict in the virtual ancestor.
1389+
*
1390+
* <p>
1391+
* Those conflicts should be ignored, otherwise the found base can not be used by the
1392+
* RecursiveMerger.
1393+
* <pre>
1394+
* --------------
1395+
* | \
1396+
* | C1 - C4 --- ? master
1397+
* | / /
1398+
* | I - A1 - C2 - C3 second-branch
1399+
* | \ /
1400+
* \ \ /
1401+
* ----A2-------- branch-to-merge
1402+
* </pre>
1403+
* <p>
1404+
* <p>
1405+
* Path "a" is initially a file in I and A1. It is changed to a directory in A2
1406+
* ("branch-to-merge").
1407+
* <p>
1408+
* A2 is merged into "master" and "second-branch". The dir/file merge conflict is resolved
1409+
* manually, results in C4 and C3.
1410+
* <p>
1411+
* While merging C3 and C4, A1 and A2 are the base commits found by the recursive merge that
1412+
* have the dir/file conflict.
1413+
*/
1414+
@Theory
1415+
public void checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren(
1416+
MergeStrategy strategy)
1417+
throws Exception {
1418+
if (!strategy.equals(MergeStrategy.RECURSIVE)) {
1419+
return;
1420+
}
1421+
1422+
Git git = Git.wrap(db);
1423+
1424+
// master
1425+
writeTrashFile("a", "initial content");
1426+
git.add().addFilepattern("a").call();
1427+
RevCommit commitI = git.commit().setMessage("Initial commit").call();
1428+
1429+
writeTrashFile("a", "content in Ancestor 1");
1430+
git.add().addFilepattern("a").call();
1431+
RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
1432+
1433+
writeTrashFile("a", "content in Child 1 (commited on master)");
1434+
git.add().addFilepattern("a").call();
1435+
// commit C1M
1436+
git.commit().setMessage("Child 1 on master").call();
1437+
1438+
git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
1439+
// "a" becomes a directory in A2
1440+
git.rm().addFilepattern("a").call();
1441+
writeTrashFile("a/content", "content in Ancestor 2 (commited on branch-to-merge)");
1442+
git.add().addFilepattern("a/content").call();
1443+
RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
1444+
1445+
// second branch
1446+
git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
1447+
writeTrashFile("a", "content in Child 2 (commited on second-branch)");
1448+
git.add().addFilepattern("a").call();
1449+
// commit C2S
1450+
git.commit().setMessage("Child 2 on second-branch").call();
1451+
1452+
// Merge branch-to-merge into second-branch
1453+
MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1454+
assertEquals(mergeResult.getNewHead(), null);
1455+
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1456+
// Resolve the conflict manually, merge "a" as a file
1457+
git.rm().addFilepattern("a").call();
1458+
git.rm().addFilepattern("a/content").call();
1459+
writeTrashFile("a", "merge conflict resolution");
1460+
git.add().addFilepattern("a").call();
1461+
RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict")
1462+
.call();
1463+
1464+
// Merge branch-to-merge into master
1465+
git.checkout().setName("master").call();
1466+
mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1467+
assertEquals(mergeResult.getNewHead(), null);
1468+
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1469+
1470+
// Resolve the conflict manually - merge "a" as a file
1471+
git.rm().addFilepattern("a").call();
1472+
git.rm().addFilepattern("a/content").call();
1473+
writeTrashFile("a", "merge conflict resolution");
1474+
git.add().addFilepattern("a").call();
1475+
// commit C4M
1476+
git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
1477+
1478+
// Merge C4M (second-branch) into master (C3S)
1479+
// Conflict in virtual base should be here, but there are no conflicts in
1480+
// children
1481+
mergeResult = git.merge().include(commitC3S).call();
1482+
assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED);
1483+
1484+
}
1485+
1486+
@Theory
1487+
public void checkFileDirMergeConflictInVirtualAncestor_ConflictInChildren_FileDir(MergeStrategy strategy)
1488+
throws Exception {
1489+
if (!strategy.equals(MergeStrategy.RECURSIVE)) {
1490+
return;
1491+
}
1492+
1493+
Git git = Git.wrap(db);
1494+
1495+
// master
1496+
writeTrashFile("a", "initial content");
1497+
git.add().addFilepattern("a").call();
1498+
RevCommit commitI = git.commit().setMessage("Initial commit").call();
1499+
1500+
writeTrashFile("a", "content in Ancestor 1");
1501+
git.add().addFilepattern("a").call();
1502+
RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
1503+
1504+
writeTrashFile("a", "content in Child 1 (commited on master)");
1505+
git.add().addFilepattern("a").call();
1506+
// commit C1M
1507+
git.commit().setMessage("Child 1 on master").call();
1508+
1509+
git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
1510+
1511+
// "a" becomes a directory in A2
1512+
git.rm().addFilepattern("a").call();
1513+
writeTrashFile("a/content", "content in Ancestor 2 (commited on branch-to-merge)");
1514+
git.add().addFilepattern("a/content").call();
1515+
RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
1516+
1517+
// second branch
1518+
git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
1519+
writeTrashFile("a", "content in Child 2 (commited on second-branch)");
1520+
git.add().addFilepattern("a").call();
1521+
// commit C2S
1522+
git.commit().setMessage("Child 2 on second-branch").call();
1523+
1524+
// Merge branch-to-merge into second-branch
1525+
MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1526+
assertEquals(mergeResult.getNewHead(), null);
1527+
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1528+
// Resolve the conflict manually - write a file
1529+
git.rm().addFilepattern("a").call();
1530+
git.rm().addFilepattern("a/content").call();
1531+
writeTrashFile("a",
1532+
"content in Child 3 (commited on second-branch) - merge conflict resolution");
1533+
git.add().addFilepattern("a").call();
1534+
RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict")
1535+
.call();
1536+
1537+
// Merge branch-to-merge into master
1538+
git.checkout().setName("master").call();
1539+
mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1540+
assertEquals(mergeResult.getNewHead(), null);
1541+
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1542+
1543+
// Resolve the conflict manually - write a file
1544+
git.rm().addFilepattern("a").call();
1545+
git.rm().addFilepattern("a/content").call();
1546+
writeTrashFile("a", "content in Child 4 (commited on master) - merge conflict resolution");
1547+
git.add().addFilepattern("a").call();
1548+
// commit C4M
1549+
git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
1550+
1551+
// Merge C4M (second-branch) into master (C3S)
1552+
// Conflict in virtual base should be here
1553+
mergeResult = git.merge().include(commitC3S).call();
1554+
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1555+
String expected =
1556+
"<<<<<<< HEAD\n" + "content in Child 4 (commited on master) - merge conflict resolution\n"
1557+
+ "=======\n"
1558+
+ "content in Child 3 (commited on second-branch) - merge conflict resolution\n"
1559+
+ ">>>>>>> " + commitC3S.name() + "\n";
1560+
assertEquals(expected, read("a"));
1561+
// Nothing was populated from the ancestors.
1562+
assertEquals(
1563+
"[a, mode:100644, stage:2, content:content in Child 4 (commited on master) - merge conflict resolution][a, mode:100644, stage:3, content:content in Child 3 (commited on second-branch) - merge conflict resolution]",
1564+
indexState(CONTENT));
1565+
}
1566+
1567+
/**
1568+
* Same test as above, but "a" is a dir in A1 and a file in A2
1569+
*/
1570+
@Theory
1571+
public void checkFileDirMergeConflictInVirtualAncestor_ConflictInChildren_DirFile(MergeStrategy strategy)
1572+
throws Exception {
1573+
if (!strategy.equals(MergeStrategy.RECURSIVE)) {
1574+
return;
1575+
}
1576+
1577+
Git git = Git.wrap(db);
1578+
1579+
// master
1580+
writeTrashFile("a/content", "initial content");
1581+
git.add().addFilepattern("a/content").call();
1582+
RevCommit commitI = git.commit().setMessage("Initial commit").call();
1583+
1584+
writeTrashFile("a/content", "content in Ancestor 1");
1585+
git.add().addFilepattern("a/content").call();
1586+
RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
1587+
1588+
writeTrashFile("a/content", "content in Child 1 (commited on master)");
1589+
git.add().addFilepattern("a/content").call();
1590+
// commit C1M
1591+
git.commit().setMessage("Child 1 on master").call();
1592+
1593+
git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
1594+
1595+
// "a" becomes a file in A2
1596+
git.rm().addFilepattern("a/content").call();
1597+
writeTrashFile("a", "content in Ancestor 2 (commited on branch-to-merge)");
1598+
git.add().addFilepattern("a").call();
1599+
RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
1600+
1601+
// second branch
1602+
git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
1603+
writeTrashFile("a/content", "content in Child 2 (commited on second-branch)");
1604+
git.add().addFilepattern("a/content").call();
1605+
// commit C2S
1606+
git.commit().setMessage("Child 2 on second-branch").call();
1607+
1608+
// Merge branch-to-merge into second-branch
1609+
MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1610+
assertEquals(mergeResult.getNewHead(), null);
1611+
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1612+
// Resolve the conflict manually - write a file
1613+
git.rm().addFilepattern("a").call();
1614+
git.rm().addFilepattern("a/content").call();
1615+
deleteTrashFile("a/content");
1616+
deleteTrashFile("a");
1617+
writeTrashFile("a", "content in Child 3 (commited on second-branch) - merge conflict resolution");
1618+
git.add().addFilepattern("a").call();
1619+
RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict").call();
1620+
1621+
// Merge branch-to-merge into master
1622+
git.checkout().setName("master").call();
1623+
mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1624+
assertEquals(mergeResult.getNewHead(), null);
1625+
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1626+
1627+
// Resolve the conflict manually - write a file
1628+
git.rm().addFilepattern("a").call();
1629+
git.rm().addFilepattern("a/content").call();
1630+
deleteTrashFile("a/content");
1631+
deleteTrashFile("a");
1632+
writeTrashFile("a", "content in Child 4 (commited on master) - merge conflict resolution");
1633+
git.add().addFilepattern("a").call();
1634+
// commit C4M
1635+
git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
1636+
1637+
// Merge C4M (second-branch) into master (C3S)
1638+
// Conflict in virtual base should be here
1639+
mergeResult = git.merge().include(commitC3S).call();
1640+
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1641+
String expected = "<<<<<<< HEAD\n" + "content in Child 4 (commited on master) - merge conflict resolution\n"
1642+
+ "=======\n" + "content in Child 3 (commited on second-branch) - merge conflict resolution\n"
1643+
+ ">>>>>>> " + commitC3S.name() + "\n";
1644+
assertEquals(expected, read("a"));
1645+
// Nothing was populated from the ancestors.
1646+
assertEquals(
1647+
"[a, mode:100644, stage:2, content:content in Child 4 (commited on master) - merge conflict resolution][a, mode:100644, stage:3, content:content in Child 3 (commited on second-branch) - merge conflict resolution]",
1648+
indexState(CONTENT));
1649+
}
1650+
13871651
private void writeSubmodule(String path, ObjectId commit)
13881652
throws IOException, ConfigInvalidException {
13891653
addSubmoduleToIndex(path, commit);

org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -703,18 +703,21 @@ protected boolean processEntry(CanonicalTreeParser base,
703703
// conflict between ours and theirs. file/folder conflicts between
704704
// base/index/workingTree and something else are not relevant or
705705
// detected later
706-
if (nonTree(modeO) && !nonTree(modeT)) {
707-
if (nonTree(modeB))
708-
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
709-
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
710-
unmergedPaths.add(tw.getPathString());
711-
enterSubtree = false;
712-
return true;
713-
}
714-
if (nonTree(modeT) && !nonTree(modeO)) {
706+
if (nonTree(modeO) != nonTree(modeT)) {
707+
if (ignoreConflicts) {
708+
// In case of merge failures, ignore this path instead of reporting unmerged, so
709+
// a caller can use virtual commit. This will not result in files with conflict
710+
// markers in the index/working tree. The actual diff on the path will be
711+
// computed directly on children.
712+
enterSubtree = false;
713+
return true;
714+
}
715715
if (nonTree(modeB))
716716
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
717-
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
717+
if (nonTree(modeO))
718+
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
719+
if (nonTree(modeT))
720+
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
718721
unmergedPaths.add(tw.getPathString());
719722
enterSubtree = false;
720723
return true;

0 commit comments

Comments
 (0)