diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java index de11e2c00..cc84f197a 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java @@ -34,6 +34,7 @@ import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; @@ -512,6 +513,21 @@ protected DirCacheEntry createEntry(final String path, final FileMode mode, return entry; } + /** + * Create DirCacheEntry + * + * @param path + * @param objectId + * @return the DirCacheEntry + */ + protected DirCacheEntry createGitLink(String path, AnyObjectId objectId) { + final DirCacheEntry entry = new DirCacheEntry(path, + DirCacheEntry.STAGE_0); + entry.setFileMode(FileMode.GITLINK); + entry.setObjectId(objectId); + return entry; + } + /** * Assert files are equal * diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java new file mode 100644 index 000000000..c850b4d74 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java @@ -0,0 +1,368 @@ +/* + * Copyright (c) 2020, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +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.IOException; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.junit.Test; + +public class GitlinkMergeTest extends SampleDataRepositoryTestCase { + private static final String LINK_ID1 = "DEADBEEFDEADBEEFBABEDEADBEEFDEADBEEFBABE"; + private static final String LINK_ID2 = "DEADDEADDEADDEADDEADDEADDEADDEADDEADDEAD"; + private static final String LINK_ID3 = "BEEFBEEFBEEFBEEFBEEFBEEFBEEFBEEFBEEFBEEF"; + + private static final String SUBMODULE_PATH = "submodule.link"; + + @Test + public void testGitLinkMerging_AddNew() throws Exception { + assertGitLinkValue( + testGitLink(null, null, LINK_ID3, newResolveMerger(), true), + LINK_ID3); + } + + @Test + public void testGitLinkMerging_Delete() throws Exception { + assertGitLinkDoesntExist(testGitLink(LINK_ID1, LINK_ID1, null, + newResolveMerger(), true)); + } + + @Test + public void testGitLinkMerging_UpdateDelete() throws Exception { + testGitLink(LINK_ID1, LINK_ID2, null, newResolveMerger(), false); + } + + @Test + public void testGitLinkMerging_DeleteUpdate() throws Exception { + testGitLink(LINK_ID1, null, LINK_ID3, newResolveMerger(), false); + } + + @Test + public void testGitLinkMerging_UpdateUpdate() throws Exception { + testGitLink(LINK_ID1, LINK_ID2, LINK_ID3, newResolveMerger(), false); + } + + @Test + public void testGitLinkMerging_bothAddedSameLink() throws Exception { + assertGitLinkValue( + testGitLink(null, LINK_ID2, LINK_ID2, newResolveMerger(), true), + LINK_ID2); + } + + @Test + public void testGitLinkMerging_bothAddedDifferentLink() throws Exception { + testGitLink(null, LINK_ID2, LINK_ID3, newResolveMerger(), false); + } + + @Test + public void testGitLinkMerging_AddNew_ignoreConflicts() throws Exception { + assertGitLinkValue( + testGitLink(null, null, LINK_ID3, newIgnoreConflictMerger(), + true), + LINK_ID3); + } + + @Test + public void testGitLinkMerging_Delete_ignoreConflicts() throws Exception { + assertGitLinkDoesntExist(testGitLink(LINK_ID1, LINK_ID1, null, + newIgnoreConflictMerger(), true)); + } + + @Test + public void testGitLinkMerging_UpdateDelete_ignoreConflicts() + throws Exception { + assertGitLinkValue(testGitLink(LINK_ID1, LINK_ID2, null, + newIgnoreConflictMerger(), true), LINK_ID2); + } + + @Test + public void testGitLinkMerging_DeleteUpdate_ignoreConflicts() + throws Exception { + assertGitLinkDoesntExist(testGitLink(LINK_ID1, null, LINK_ID3, + newIgnoreConflictMerger(), true)); + } + + @Test + public void testGitLinkMerging_UpdateUpdate_ignoreConflicts() + throws Exception { + assertGitLinkValue(testGitLink(LINK_ID1, LINK_ID2, LINK_ID3, + newIgnoreConflictMerger(), true), LINK_ID2); + } + + @Test + public void testGitLinkMerging_bothAddedSameLink_ignoreConflicts() + throws Exception { + assertGitLinkValue(testGitLink(null, LINK_ID2, LINK_ID2, + newIgnoreConflictMerger(), true), LINK_ID2); + } + + @Test + public void testGitLinkMerging_bothAddedDifferentLink_ignoreConflicts() + throws Exception { + assertGitLinkValue(testGitLink(null, LINK_ID2, LINK_ID3, + newIgnoreConflictMerger(), true), LINK_ID2); + } + + protected Merger testGitLink(@Nullable String baseLink, + @Nullable String oursLink, @Nullable String theirsLink, + Merger merger, boolean shouldMerge) + throws Exception { + DirCache treeB = db.readDirCache(); + DirCache treeO = db.readDirCache(); + DirCache treeT = db.readDirCache(); + + DirCacheBuilder bTreeBuilder = treeB.builder(); + DirCacheBuilder oTreeBuilder = treeO.builder(); + DirCacheBuilder tTreeBuilder = treeT.builder(); + + maybeAddLink(bTreeBuilder, baseLink); + maybeAddLink(oTreeBuilder, oursLink); + maybeAddLink(tTreeBuilder, theirsLink); + + bTreeBuilder.finish(); + oTreeBuilder.finish(); + tTreeBuilder.finish(); + + ObjectInserter ow = db.newObjectInserter(); + ObjectId b = commit(ow, treeB, new ObjectId[] {}); + ObjectId o = commit(ow, treeO, new ObjectId[] { b }); + ObjectId t = commit(ow, treeT, new ObjectId[] { b }); + + boolean merge = merger.merge(new ObjectId[] { o, t }); + assertEquals(shouldMerge, merge); + + return merger; + } + + private Merger newResolveMerger() { + return MergeStrategy.RESOLVE.newMerger(db, true); + } + + private Merger newIgnoreConflictMerger() { + return new ResolveMerger(db, true) { + @Override + protected boolean mergeImpl() throws IOException { + // emulate call with ignore conflicts. + return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1], + true); + } + }; + } + + @Test + public void testGitLinkMerging_blobWithLink() throws Exception { + DirCache treeB = db.readDirCache(); + DirCache treeO = db.readDirCache(); + DirCache treeT = db.readDirCache(); + + DirCacheBuilder bTreeBuilder = treeB.builder(); + DirCacheBuilder oTreeBuilder = treeO.builder(); + DirCacheBuilder tTreeBuilder = treeT.builder(); + + bTreeBuilder.add( + createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob")); + oTreeBuilder.add( + createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob 2")); + + maybeAddLink(tTreeBuilder, LINK_ID3); + + bTreeBuilder.finish(); + oTreeBuilder.finish(); + tTreeBuilder.finish(); + + ObjectInserter ow = db.newObjectInserter(); + ObjectId b = commit(ow, treeB, new ObjectId[] {}); + ObjectId o = commit(ow, treeO, new ObjectId[] { b }); + ObjectId t = commit(ow, treeT, new ObjectId[] { b }); + + Merger resolveMerger = MergeStrategy.RESOLVE.newMerger(db); + boolean merge = resolveMerger.merge(new ObjectId[] { o, t }); + assertFalse(merge); + } + + @Test + public void testGitLinkMerging_linkWithBlob() throws Exception { + DirCache treeB = db.readDirCache(); + DirCache treeO = db.readDirCache(); + DirCache treeT = db.readDirCache(); + + DirCacheBuilder bTreeBuilder = treeB.builder(); + DirCacheBuilder oTreeBuilder = treeO.builder(); + DirCacheBuilder tTreeBuilder = treeT.builder(); + + maybeAddLink(bTreeBuilder, LINK_ID1); + maybeAddLink(oTreeBuilder, LINK_ID2); + tTreeBuilder.add( + createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob 3")); + + bTreeBuilder.finish(); + oTreeBuilder.finish(); + tTreeBuilder.finish(); + + ObjectInserter ow = db.newObjectInserter(); + ObjectId b = commit(ow, treeB, new ObjectId[] {}); + ObjectId o = commit(ow, treeO, new ObjectId[] { b }); + ObjectId t = commit(ow, treeT, new ObjectId[] { b }); + + Merger resolveMerger = MergeStrategy.RESOLVE.newMerger(db); + boolean merge = resolveMerger.merge(new ObjectId[] { o, t }); + assertFalse(merge); + } + + @Test + public void testGitLinkMerging_linkWithLink() throws Exception { + DirCache treeB = db.readDirCache(); + DirCache treeO = db.readDirCache(); + DirCache treeT = db.readDirCache(); + + DirCacheBuilder bTreeBuilder = treeB.builder(); + DirCacheBuilder oTreeBuilder = treeO.builder(); + DirCacheBuilder tTreeBuilder = treeT.builder(); + + bTreeBuilder.add( + createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob")); + maybeAddLink(oTreeBuilder, LINK_ID2); + maybeAddLink(tTreeBuilder, LINK_ID3); + + bTreeBuilder.finish(); + oTreeBuilder.finish(); + tTreeBuilder.finish(); + + ObjectInserter ow = db.newObjectInserter(); + ObjectId b = commit(ow, treeB, new ObjectId[] {}); + ObjectId o = commit(ow, treeO, new ObjectId[] { b }); + ObjectId t = commit(ow, treeT, new ObjectId[] { b }); + + Merger resolveMerger = MergeStrategy.RESOLVE.newMerger(db); + boolean merge = resolveMerger.merge(new ObjectId[] { o, t }); + assertFalse(merge); + } + + @Test + public void testGitLinkMerging_blobWithBlobFromLink() throws Exception { + DirCache treeB = db.readDirCache(); + DirCache treeO = db.readDirCache(); + DirCache treeT = db.readDirCache(); + + DirCacheBuilder bTreeBuilder = treeB.builder(); + DirCacheBuilder oTreeBuilder = treeO.builder(); + DirCacheBuilder tTreeBuilder = treeT.builder(); + + maybeAddLink(bTreeBuilder, LINK_ID1); + oTreeBuilder.add( + createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob 2")); + tTreeBuilder.add( + createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob 3")); + + bTreeBuilder.finish(); + oTreeBuilder.finish(); + tTreeBuilder.finish(); + + ObjectInserter ow = db.newObjectInserter(); + ObjectId b = commit(ow, treeB, new ObjectId[] {}); + ObjectId o = commit(ow, treeO, new ObjectId[] { b }); + ObjectId t = commit(ow, treeT, new ObjectId[] { b }); + + Merger resolveMerger = MergeStrategy.RESOLVE.newMerger(db); + boolean merge = resolveMerger.merge(new ObjectId[] { o, t }); + assertFalse(merge); + } + + @Test + public void testGitLinkMerging_linkBlobDeleted() throws Exception { + // We changed a link to a blob, others has deleted this link. + DirCache treeB = db.readDirCache(); + DirCache treeO = db.readDirCache(); + DirCache treeT = db.readDirCache(); + + DirCacheBuilder bTreeBuilder = treeB.builder(); + DirCacheBuilder oTreeBuilder = treeO.builder(); + DirCacheBuilder tTreeBuilder = treeT.builder(); + + maybeAddLink(bTreeBuilder, LINK_ID1); + oTreeBuilder.add( + createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob 2")); + + bTreeBuilder.finish(); + oTreeBuilder.finish(); + tTreeBuilder.finish(); + + ObjectInserter ow = db.newObjectInserter(); + ObjectId b = commit(ow, treeB, new ObjectId[] {}); + ObjectId o = commit(ow, treeO, new ObjectId[] { b }); + ObjectId t = commit(ow, treeT, new ObjectId[] { b }); + + Merger resolveMerger = MergeStrategy.RESOLVE.newMerger(db); + boolean merge = resolveMerger.merge(new ObjectId[] { o, t }); + assertFalse(merge); + } + + private void maybeAddLink(DirCacheBuilder builder, + @Nullable String linkId) { + if (linkId == null) { + return; + } + DirCacheEntry newLink = createGitLink(SUBMODULE_PATH, + ObjectId.fromString(linkId)); + builder.add(newLink); + } + + private void assertGitLinkValue(Merger resolveMerger, String expectedValue) + throws Exception { + try (TreeWalk tw = new TreeWalk(db)) { + tw.setRecursive(true); + tw.reset(resolveMerger.getResultTreeId()); + + assertTrue(tw.next()); + assertEquals(SUBMODULE_PATH, tw.getPathString()); + assertEquals(ObjectId.fromString(expectedValue), tw.getObjectId(0)); + + assertFalse(tw.next()); + } + } + + private void assertGitLinkDoesntExist(Merger resolveMerger) + throws Exception { + try (TreeWalk tw = new TreeWalk(db)) { + tw.setRecursive(true); + tw.reset(resolveMerger.getResultTreeId()); + + assertFalse(tw.next()); + } + } + + private static ObjectId commit(ObjectInserter odi, DirCache treeB, + ObjectId[] parentIds) throws Exception { + CommitBuilder c = new CommitBuilder(); + c.setTreeId(treeB.writeTree(odi)); + c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0)); + c.setCommitter(c.getAuthor()); + c.setParentIds(parentIds); + c.setMessage("Tree " + c.getTreeId().name()); + ObjectId id = odi.insert(c); + odi.flush(); + return id; + } +} 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 506d33312..6c217fdf2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -588,7 +588,8 @@ protected boolean processEntry(CanonicalTreeParser base, final int modeO = tw.getRawMode(T_OURS); final int modeT = tw.getRawMode(T_THEIRS); final int modeB = tw.getRawMode(T_BASE); - + boolean gitLinkMerging = isGitLink(modeO) || isGitLink(modeT) + || isGitLink(modeB); if (modeO == 0 && modeT == 0 && modeB == 0) // File is either untracked or new, staged but uncommitted return true; @@ -737,31 +738,28 @@ protected boolean processEntry(CanonicalTreeParser base, return false; } - boolean gitlinkConflict = isGitLink(modeO) || isGitLink(modeT); - // Don't attempt to resolve submodule link conflicts - if (gitlinkConflict || !attributes.canBeContentMerged()) { + if (gitLinkMerging && ignoreConflicts) { + // Always select 'ours' in case of GITLINK merge failures so + // a caller can use virtual commit. + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0, EPOCH, 0); + return true; + } else if (gitLinkMerging) { + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); + add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); + MergeResult result = createGitLinksMergeResult( + base, ours, theirs); + result.setContainsConflicts(true); + mergeResults.put(tw.getPathString(), result); + unmergedPaths.add(tw.getPathString()); + return true; + } else if (!attributes.canBeContentMerged()) { add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); - if (gitlinkConflict) { - MergeResult result = new MergeResult<>( - Arrays.asList( - new SubmoduleConflict(base == null ? null - : base.getEntryObjectId()), - new SubmoduleConflict(ours == null ? null - : ours.getEntryObjectId()), - new SubmoduleConflict(theirs == null ? null - : theirs.getEntryObjectId()))); - result.setContainsConflicts(true); - mergeResults.put(tw.getPathString(), result); - if (!ignoreConflicts) { - unmergedPaths.add(tw.getPathString()); - } - } else { - // attribute merge issues are conflicts but not failures - unmergedPaths.add(tw.getPathString()); - } + // attribute merge issues are conflicts but not failures + unmergedPaths.add(tw.getPathString()); return true; } @@ -786,45 +784,73 @@ protected boolean processEntry(CanonicalTreeParser base, // OURS or THEIRS has been deleted if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw .idEqual(T_BASE, T_THEIRS)))) { - MergeResult result = contentMerge(base, ours, theirs, - attributes); - - if (ignoreConflicts) { - // In case a conflict is detected the working tree file is - // again filled with new content (containing conflict - // markers). But also stage 0 of the index is filled with - // that content. - result.setContainsConflicts(false); - updateIndex(base, ours, theirs, result, attributes); - } else { + if (gitLinkMerging && ignoreConflicts) { + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0, EPOCH, 0); + } else if (gitLinkMerging) { add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); - DirCacheEntry e = add(tw.getRawPath(), theirs, - DirCacheEntry.STAGE_3, EPOCH, 0); + add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); + MergeResult result = createGitLinksMergeResult( + base, ours, theirs); + result.setContainsConflicts(true); + mergeResults.put(tw.getPathString(), result); + unmergedPaths.add(tw.getPathString()); + } else { + MergeResult result = contentMerge(base, ours, + theirs, attributes); - // OURS was deleted checkout THEIRS - if (modeO == 0) { - // Check worktree before checking out THEIRS - if (isWorktreeDirty(work, ourDce)) { - return false; - } - if (nonTree(modeT)) { - if (e != null) { - addToCheckout(tw.getPathString(), e, attributes); + if (ignoreConflicts) { + // In case a conflict is detected the working tree file + // is again filled with new content (containing conflict + // markers). But also stage 0 of the index is filled + // with that content. + result.setContainsConflicts(false); + updateIndex(base, ours, theirs, result, attributes); + } else { + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, + 0); + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, + 0); + DirCacheEntry e = add(tw.getRawPath(), theirs, + DirCacheEntry.STAGE_3, EPOCH, 0); + + // OURS was deleted checkout THEIRS + if (modeO == 0) { + // Check worktree before checking out THEIRS + if (isWorktreeDirty(work, ourDce)) { + return false; + } + if (nonTree(modeT)) { + if (e != null) { + addToCheckout(tw.getPathString(), e, + attributes); + } } } + + unmergedPaths.add(tw.getPathString()); + + // generate a MergeResult for the deleted file + mergeResults.put(tw.getPathString(), result); } - - unmergedPaths.add(tw.getPathString()); - - // generate a MergeResult for the deleted file - mergeResults.put(tw.getPathString(), result); } } } return true; } + private static MergeResult createGitLinksMergeResult( + CanonicalTreeParser base, CanonicalTreeParser ours, + CanonicalTreeParser theirs) { + return new MergeResult<>(Arrays.asList( + new SubmoduleConflict( + base == null ? null : base.getEntryObjectId()), + new SubmoduleConflict( + ours == null ? null : ours.getEntryObjectId()), + new SubmoduleConflict( + theirs == null ? null : theirs.getEntryObjectId()))); + } + /** * Does the content merge. The three texts base, ours and theirs are * specified with {@link CanonicalTreeParser}. If any of the parsers is