Skip to content

Commit 567315a

Browse files
kumarp149msohn
authored andcommitted
ResolveMerger: Fix the issue with binary modify-modify conflicts
1) If the file was marked as binary by git attributes, we should add the path to conflicts if content differs in OURS and THEIRS 2) If the path is a file in OURS, THEIRS and BASE and if it is a binary in any one of them, no content merge should be attempted and the file content is kept as is in the work tree Bug: jgit-14 Change-Id: I9201bdc53a55f8f40adade4b6a36ee8ae25f4db8
1 parent e97623d commit 567315a

File tree

3 files changed

+230
-38
lines changed

3 files changed

+230
-38
lines changed

org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
package org.eclipse.jgit.attributes.merge;
1111

1212
import static org.junit.Assert.assertEquals;
13-
import static org.junit.Assert.assertFalse;
1413
import static org.junit.Assert.assertNotNull;
1514
import static org.junit.Assert.assertNull;
1615
import static org.junit.Assert.assertTrue;
@@ -42,7 +41,6 @@
4241
import org.eclipse.jgit.treewalk.FileTreeIterator;
4342
import org.eclipse.jgit.treewalk.TreeWalk;
4443
import org.eclipse.jgit.treewalk.filter.PathFilter;
45-
import org.junit.Ignore;
4644
import org.junit.Test;
4745

4846
public class MergeGitAttributeTest extends RepositoryTestCase {
@@ -268,12 +266,7 @@ public void mergeTextualFile_SetBinaryMerge_Conflict()
268266
}
269267
}
270268

271-
/*
272-
* This test is commented because JGit add conflict markers in binary files.
273-
* cf. https://www.eclipse.org/forums/index.php/t/1086511/
274-
*/
275269
@Test
276-
@Ignore
277270
public void mergeBinaryFile_NoAttr_Conflict() throws IllegalStateException,
278271
IOException, NoHeadException, ConcurrentRefUpdateException,
279272
CheckoutConflictException, InvalidMergeHeadsException,
@@ -433,7 +426,7 @@ public void mergeBinaryFile_SetMerge_Conflict()
433426
try (FileInputStream mergeResultFile = new FileInputStream(
434427
db.getWorkTree().toPath().resolve(ENABLED_CHECKED_GIF)
435428
.toFile())) {
436-
assertFalse(contentEquals(
429+
assertTrue(contentEquals(
437430
getClass().getResourceAsStream(ENABLED_CHECKED_GIF),
438431
mergeResultFile));
439432
}

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

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222
import java.io.File;
2323
import java.io.FileInputStream;
2424
import java.io.IOException;
25+
import java.nio.file.Files;
2526
import java.time.Instant;
2627
import java.util.Arrays;
28+
import java.util.HashSet;
2729
import java.util.Map;
30+
import java.util.Set;
2831

2932
import org.eclipse.jgit.api.Git;
3033
import org.eclipse.jgit.api.MergeResult;
@@ -51,6 +54,7 @@
5154
import org.eclipse.jgit.lib.ObjectLoader;
5255
import org.eclipse.jgit.lib.ObjectReader;
5356
import org.eclipse.jgit.lib.ObjectStream;
57+
import org.eclipse.jgit.lib.Repository;
5458
import org.eclipse.jgit.lib.StoredConfig;
5559
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
5660
import org.eclipse.jgit.revwalk.RevCommit;
@@ -1789,6 +1793,188 @@ public void checkModeMergeConflictInVirtualAncestor(MergeStrategy strategy) thro
17891793

17901794
}
17911795

1796+
/**
1797+
* File is binary in ours, theirs and base with different content in each of
1798+
* them. Content of the file should not change after the merge conflict as
1799+
* no conflict markers are added to the binary files
1800+
*/
1801+
@Theory
1802+
public void oursBinaryTheirsBinaryBaseBinary(MergeStrategy strategy)
1803+
throws Exception {
1804+
Git git = Git.wrap(db);
1805+
String binaryFile = "file";
1806+
1807+
writeTrashFile(binaryFile, "\u0000\u0001");
1808+
git.add().addFilepattern(binaryFile).call();
1809+
RevCommit parent = git.commit().setMessage("BASE COMMIT").call();
1810+
String fileHashInBase = getFileHashInWorkTree(git, binaryFile);
1811+
1812+
writeTrashFile(binaryFile, "\u0001\u0002");
1813+
git.add().addFilepattern(binaryFile).call();
1814+
RevCommit child1 = git.commit().setMessage("THEIRS COMMIT").call();
1815+
String fileHashInChild1 = getFileHashInWorkTree(git, binaryFile);
1816+
1817+
git.checkout().setCreateBranch(true).setStartPoint(parent)
1818+
.setName("side").call();
1819+
1820+
writeTrashFile(binaryFile, "\u0002\u0000");
1821+
git.add().addFilepattern(binaryFile).call();
1822+
git.commit().setMessage("OURS COMMIT").call();
1823+
String fileHashInChild2 = getFileHashInWorkTree(git, binaryFile);
1824+
1825+
MergeResult mergeResult = git.merge().setStrategy(strategy)
1826+
.include(child1).call();
1827+
1828+
// check if the merge caused a conflict
1829+
assertTrue(mergeResult.getConflicts() != null
1830+
&& !mergeResult.getConflicts().isEmpty());
1831+
String fileHashInChild2AfterMerge = getFileHashInWorkTree(git,
1832+
binaryFile);
1833+
1834+
// check if the file content changed during a conflicting merge
1835+
assertEquals(fileHashInChild2AfterMerge, fileHashInChild2);
1836+
1837+
Set<String> hashesInIndexFile = new HashSet<>();
1838+
DirCache indexContent = git.getRepository().readDirCache();
1839+
for (int i = 0; i < indexContent.getEntryCount(); ++i) {
1840+
DirCacheEntry indexEntry = indexContent.getEntry(i);
1841+
if (binaryFile.equals(indexEntry.getPathString())) {
1842+
hashesInIndexFile.add(indexEntry.getObjectId().name());
1843+
}
1844+
}
1845+
1846+
// check if all the three stages are added to index file
1847+
assertTrue(hashesInIndexFile.contains(fileHashInBase));
1848+
assertTrue(hashesInIndexFile.contains(fileHashInChild1));
1849+
assertTrue(hashesInIndexFile.contains(fileHashInChild2));
1850+
}
1851+
1852+
/**
1853+
* File is text in ours and theirs with different content but binary in
1854+
* base. Even in this case, file will be treated as a binary and no conflict
1855+
* markers are added to it
1856+
*/
1857+
@Theory
1858+
public void oursAndTheirsDifferentTextBaseBinary(MergeStrategy strategy)
1859+
throws Exception {
1860+
Git git = Git.wrap(db);
1861+
String binaryFile = "file";
1862+
1863+
writeTrashFile(binaryFile, "\u0000\u0001");
1864+
git.add().addFilepattern(binaryFile).call();
1865+
RevCommit parent = git.commit().setMessage("BASE COMMIT").call();
1866+
String fileHashInBase = getFileHashInWorkTree(git, binaryFile);
1867+
1868+
writeTrashFile(binaryFile, "TEXT1");
1869+
git.add().addFilepattern(binaryFile).call();
1870+
RevCommit child1 = git.commit().setMessage("THEIRS COMMIT").call();
1871+
String fileHashInChild1 = getFileHashInWorkTree(git, binaryFile);
1872+
1873+
git.checkout().setCreateBranch(true).setStartPoint(parent)
1874+
.setName("side").call();
1875+
1876+
writeTrashFile(binaryFile, "TEXT2");
1877+
git.add().addFilepattern(binaryFile).call();
1878+
git.commit().setMessage("OURS COMMIT").call();
1879+
String fileHashInChild2 = getFileHashInWorkTree(git, binaryFile);
1880+
1881+
MergeResult mergeResult = git.merge().setStrategy(strategy)
1882+
.include(child1).call();
1883+
1884+
assertTrue(mergeResult.getConflicts() != null
1885+
&& !mergeResult.getConflicts().isEmpty());
1886+
String fileHashInChild2AfterMerge = getFileHashInWorkTree(git,
1887+
binaryFile);
1888+
1889+
assertEquals(fileHashInChild2AfterMerge, fileHashInChild2);
1890+
1891+
Set<String> hashesInIndexFile = new HashSet<>();
1892+
DirCache indexContent = git.getRepository().readDirCache();
1893+
for (int i = 0; i < indexContent.getEntryCount(); ++i) {
1894+
DirCacheEntry indexEntry = indexContent.getEntry(i);
1895+
if (binaryFile.equals(indexEntry.getPathString())) {
1896+
hashesInIndexFile.add(indexEntry.getObjectId().name());
1897+
}
1898+
}
1899+
1900+
assertTrue(hashesInIndexFile.contains(fileHashInBase));
1901+
assertTrue(hashesInIndexFile.contains(fileHashInChild1));
1902+
assertTrue(hashesInIndexFile.contains(fileHashInChild2));
1903+
}
1904+
1905+
/**
1906+
* Tests the scenario where a file is expected to be treated as binary
1907+
* according to Git attributes
1908+
*/
1909+
@Theory
1910+
public void fileInBinaryInAttribute(MergeStrategy strategy)
1911+
throws Exception {
1912+
Git git = Git.wrap(db);
1913+
String binaryFile = "file.bin";
1914+
1915+
writeTrashFile(".gitattributes", binaryFile + " binary");
1916+
git.add().addFilepattern(".gitattributes").call();
1917+
git.commit().setMessage("ADDING GITATTRIBUTES").call();
1918+
1919+
writeTrashFile(binaryFile, "\u0000\u0001");
1920+
git.add().addFilepattern(binaryFile).call();
1921+
RevCommit parent = git.commit().setMessage("BASE COMMIT").call();
1922+
String fileHashInBase = getFileHashInWorkTree(git, binaryFile);
1923+
1924+
writeTrashFile(binaryFile, "\u0001\u0002");
1925+
git.add().addFilepattern(binaryFile).call();
1926+
RevCommit child1 = git.commit().setMessage("THEIRS COMMIT").call();
1927+
String fileHashInChild1 = getFileHashInWorkTree(git, binaryFile);
1928+
1929+
git.checkout().setCreateBranch(true).setStartPoint(parent)
1930+
.setName("side").call();
1931+
1932+
writeTrashFile(binaryFile, "\u0002\u0000");
1933+
git.add().addFilepattern(binaryFile).call();
1934+
git.commit().setMessage("OURS COMMIT").call();
1935+
String fileHashInChild2 = getFileHashInWorkTree(git, binaryFile);
1936+
1937+
MergeResult mergeResult = git.merge().setStrategy(strategy)
1938+
.include(child1).call();
1939+
1940+
// check if the merge caused a conflict
1941+
assertTrue(mergeResult.getConflicts() != null
1942+
&& !mergeResult.getConflicts().isEmpty());
1943+
String fileHashInChild2AfterMerge = getFileHashInWorkTree(git,
1944+
binaryFile);
1945+
1946+
// check if the file content changed during a conflicting merge
1947+
assertEquals(fileHashInChild2AfterMerge, fileHashInChild2);
1948+
1949+
Set<String> hashesInIndexFile = new HashSet<>();
1950+
DirCache indexContent = git.getRepository().readDirCache();
1951+
for (int i = 0; i < indexContent.getEntryCount(); ++i) {
1952+
DirCacheEntry indexEntry = indexContent.getEntry(i);
1953+
if (binaryFile.equals(indexEntry.getPathString())) {
1954+
hashesInIndexFile.add(indexEntry.getObjectId().name());
1955+
}
1956+
}
1957+
1958+
// check if all the three stages are added to index file
1959+
assertTrue(hashesInIndexFile.contains(fileHashInBase));
1960+
assertTrue(hashesInIndexFile.contains(fileHashInChild1));
1961+
assertTrue(hashesInIndexFile.contains(fileHashInChild2));
1962+
}
1963+
1964+
private String getFileHashInWorkTree(Git git, String filePath)
1965+
throws IOException {
1966+
Repository repository = git.getRepository();
1967+
ObjectInserter objectInserter = repository.newObjectInserter();
1968+
1969+
File conflictingFile = new File(repository.getWorkTree(), filePath);
1970+
byte[] fileContent = Files.readAllBytes(conflictingFile.toPath());
1971+
ObjectId blobId = objectInserter.insert(Constants.OBJ_BLOB,
1972+
fileContent);
1973+
objectInserter.flush();
1974+
1975+
return blobId.name();
1976+
}
1977+
17921978
private void writeSubmodule(String path, ObjectId commit)
17931979
throws IOException, ConfigInvalidException {
17941980
addSubmoduleToIndex(path, commit);

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

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,10 +1274,15 @@ protected boolean processEntry(CanonicalTreeParser base,
12741274
default:
12751275
break;
12761276
}
1277+
// add the conflicting path to merge result
1278+
String currentPath = tw.getPathString();
1279+
MergeResult<RawText> result = new MergeResult<>(
1280+
Collections.emptyList());
1281+
result.setContainsConflicts(true);
1282+
mergeResults.put(currentPath, result);
12771283
addConflict(base, ours, theirs);
1278-
12791284
// attribute merge issues are conflicts but not failures
1280-
unmergedPaths.add(tw.getPathString());
1285+
unmergedPaths.add(currentPath);
12811286
return true;
12821287
}
12831288

@@ -1289,38 +1294,48 @@ protected boolean processEntry(CanonicalTreeParser base,
12891294
MergeResult<RawText> result = null;
12901295
boolean hasSymlink = FileMode.SYMLINK.equals(modeO)
12911296
|| FileMode.SYMLINK.equals(modeT);
1297+
1298+
String currentPath = tw.getPathString();
1299+
// if the path is not a symlink in ours and theirs
12921300
if (!hasSymlink) {
12931301
try {
12941302
result = contentMerge(base, ours, theirs, attributes,
12951303
getContentMergeStrategy());
1296-
} catch (BinaryBlobException e) {
1297-
// result == null
1298-
}
1299-
}
1300-
if (result == null) {
1301-
switch (getContentMergeStrategy()) {
1302-
case OURS:
1303-
keep(ourDce);
1304-
return true;
1305-
case THEIRS:
1306-
DirCacheEntry e = add(tw.getRawPath(), theirs,
1307-
DirCacheEntry.STAGE_0, EPOCH, 0);
1308-
if (e != null) {
1309-
addToCheckout(tw.getPathString(), e, attributes);
1304+
if (result.containsConflicts() && !ignoreConflicts) {
1305+
result.setContainsConflicts(true);
1306+
unmergedPaths.add(currentPath);
1307+
} else if (ignoreConflicts) {
1308+
result.setContainsConflicts(false);
13101309
}
1310+
updateIndex(base, ours, theirs, result, attributes[T_OURS]);
1311+
workTreeUpdater.markAsModified(currentPath);
1312+
// Entry is null - only add the metadata
1313+
addToCheckout(currentPath, null, attributes);
13111314
return true;
1312-
default:
1313-
result = new MergeResult<>(Collections.emptyList());
1314-
result.setContainsConflicts(true);
1315-
break;
1315+
} catch (BinaryBlobException e) {
1316+
// if the file is binary in either OURS, THEIRS or BASE
1317+
// here, we don't have an option to ignore conflicts
13161318
}
13171319
}
1318-
if (ignoreConflicts) {
1319-
result.setContainsConflicts(false);
1320+
switch (getContentMergeStrategy()) {
1321+
case OURS:
1322+
keep(ourDce);
1323+
return true;
1324+
case THEIRS:
1325+
DirCacheEntry e = add(tw.getRawPath(), theirs,
1326+
DirCacheEntry.STAGE_0, EPOCH, 0);
1327+
if (e != null) {
1328+
addToCheckout(currentPath, e, attributes);
1329+
}
1330+
return true;
1331+
default:
1332+
result = new MergeResult<>(Collections.emptyList());
1333+
result.setContainsConflicts(true);
1334+
break;
13201335
}
1321-
String currentPath = tw.getPathString();
13221336
if (hasSymlink) {
13231337
if (ignoreConflicts) {
1338+
result.setContainsConflicts(false);
13241339
if (((modeT & FileMode.TYPE_MASK) == FileMode.TYPE_FILE)) {
13251340
DirCacheEntry e = add(tw.getRawPath(), theirs,
13261341
DirCacheEntry.STAGE_0, EPOCH, 0);
@@ -1329,9 +1344,9 @@ protected boolean processEntry(CanonicalTreeParser base,
13291344
keep(ourDce);
13301345
}
13311346
} else {
1332-
// Record the conflict
13331347
DirCacheEntry e = addConflict(base, ours, theirs);
13341348
mergeResults.put(currentPath, result);
1349+
unmergedPaths.add(currentPath);
13351350
// If theirs is a file, check it out. In link/file
13361351
// conflicts, C git prefers the file.
13371352
if (((modeT & FileMode.TYPE_MASK) == FileMode.TYPE_FILE)
@@ -1340,14 +1355,12 @@ protected boolean processEntry(CanonicalTreeParser base,
13401355
}
13411356
}
13421357
} else {
1343-
updateIndex(base, ours, theirs, result, attributes[T_OURS]);
1344-
}
1345-
if (result.containsConflicts() && !ignoreConflicts) {
1358+
result.setContainsConflicts(true);
1359+
addConflict(base, ours, theirs);
13461360
unmergedPaths.add(currentPath);
1361+
mergeResults.put(currentPath, result);
13471362
}
1348-
workTreeUpdater.markAsModified(currentPath);
1349-
// Entry is null - only adds the metadata.
1350-
addToCheckout(currentPath, null, attributes);
1363+
return true;
13511364
} else if (modeO != modeT) {
13521365
// OURS or THEIRS has been deleted
13531366
if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw

0 commit comments

Comments
 (0)