]> source.dussan.org Git - jgit.git/commitdiff
[merge] Fix merge conflicts with symlinks 19/195219/3
authorThomas Wolf <twolf@apache.org>
Mon, 15 Aug 2022 23:02:21 +0000 (01:02 +0200)
committerMatthias Sohn <matthias.sohn@sap.com>
Wed, 7 Sep 2022 13:02:02 +0000 (15:02 +0200)
Previous code would do a content merge on symlinks, and write the merge
result to the working tree as a file. C git doesn't do this; it leaves
a symlink in the working tree unchanged, or in a delete-modify conflict
it would check out "theirs".

Moreover, previous code would write the merge result to the link target,
not to the link. This would overwrite an existing link target, or fail
if the link pointed to a directory.

In link/file conflicts or file/link conflicts, C git always puts the
file into the working tree.

Change conflict handling accordingly. Add tests for all the conflict
cases.

Bug: 580347
Change-Id: I3cffcb4bcf8e336a85186031fff23f0c4b6ee19d
Signed-off-by: Thomas Wolf <twolf@apache.org>
org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SymlinkMergeTest.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java

index 54e4a09ee52d28c81647a55e180ee2103594a957..483b9a7c8173ec9ed4f1ac17bd0a5dcb84e6f66c 100644 (file)
@@ -275,6 +275,24 @@ public class TestRepository<R extends Repository> implements AutoCloseable {
                return e;
        }
 
+       /**
+        * Construct a symlink mode tree entry.
+        *
+        * @param path
+        *            path of the symlink.
+        * @param blob
+        *            a blob, previously constructed in the repository.
+        * @return the entry.
+        * @throws Exception
+        * @since 6.3
+        */
+       public DirCacheEntry link(String path, RevBlob blob) throws Exception {
+               DirCacheEntry e = new DirCacheEntry(path);
+               e.setFileMode(FileMode.SYMLINK);
+               e.setObjectId(blob);
+               return e;
+       }
+
        /**
         * Construct a tree from a specific listing of file entries.
         *
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SymlinkMergeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SymlinkMergeTest.java
new file mode 100644 (file)
index 0000000..3cdc8da
--- /dev/null
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2022 Thomas Wolf <twolf@apache.org> 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
+ * https://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 static org.junit.Assume.assumeTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.MergeResult;
+import org.eclipse.jgit.api.MergeResult.MergeStatus;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests for merges involving symlinks.
+ */
+@RunWith(Parameterized.class)
+public class SymlinkMergeTest extends RepositoryTestCase {
+
+       @Parameters(name = "target={0}, core.symlinks={1}")
+       public static Object[][] parameters() {
+               return new Object[][] {
+                       { Target.NONE, Boolean.TRUE },
+                       { Target.FILE, Boolean.TRUE },
+                       { Target.DIRECTORY, Boolean.TRUE },
+                       { Target.NONE, Boolean.FALSE },
+                       { Target.FILE, Boolean.FALSE },
+                       { Target.DIRECTORY, Boolean.FALSE },
+               };
+       }
+
+       public enum Target {
+               NONE, FILE, DIRECTORY
+       }
+
+       @Parameter(0)
+       public Target target;
+
+       @Parameter(1)
+       public boolean useSymLinks;
+
+       private void setTargets() throws IOException {
+               switch (target) {
+               case DIRECTORY:
+                       assertTrue(new File(trash, "target").mkdir());
+                       assertTrue(new File(trash, "target1").mkdir());
+                       assertTrue(new File(trash, "target2").mkdir());
+                       break;
+               case FILE:
+                       writeTrashFile("target", "t");
+                       writeTrashFile("target1", "t1");
+                       writeTrashFile("target2", "t2");
+                       break;
+               default:
+                       break;
+               }
+       }
+
+       private void checkTargets() throws IOException {
+               File t = new File(trash, "target");
+               File t1 = new File(trash, "target1");
+               File t2 = new File(trash, "target2");
+               switch (target) {
+               case DIRECTORY:
+                       assertTrue(t.isDirectory());
+                       assertTrue(t1.isDirectory());
+                       assertTrue(t2.isDirectory());
+                       break;
+               case FILE:
+                       checkFile(t, "t");
+                       checkFile(t1, "t1");
+                       checkFile(t2, "t2");
+                       break;
+               default:
+                       assertFalse(Files.exists(t.toPath(), LinkOption.NOFOLLOW_LINKS));
+                       assertFalse(Files.exists(t1.toPath(), LinkOption.NOFOLLOW_LINKS));
+                       assertFalse(Files.exists(t2.toPath(), LinkOption.NOFOLLOW_LINKS));
+                       break;
+               }
+       }
+
+       private void assertSymLink(File link, String content) throws Exception {
+               if (useSymLinks) {
+                       assertTrue(Files.isSymbolicLink(link.toPath()));
+                       assertEquals(content, db.getFS().readSymLink(link));
+               } else {
+                       assertFalse(Files.isSymbolicLink(link.toPath()));
+                       assertTrue(link.isFile());
+                       checkFile(link, content);
+               }
+       }
+
+       // Link/link conflict: C git records the conflict but leaves the link in the
+       // working tree unchanged.
+
+       @Test
+       public void mergeWithSymlinkConflict() throws Exception {
+               assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
+               StoredConfig config = db.getConfig();
+               config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+                               ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
+               config.save();
+               try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+                       db.incrementOpen();
+                       // Create the links directly in the git repo, then use a hard reset
+                       // to get them into the workspace. This enables us to run these
+                       // tests also with core.symLinks = false.
+                       RevCommit base = repo
+                                       .commit(repo.tree(repo.link("link", repo.blob("target"))));
+                       RevCommit side = repo.commit(
+                                       repo.tree(repo.link("link", repo.blob("target1"))), base);
+                       RevCommit head = repo.commit(
+                                       repo.tree(repo.link("link", repo.blob("target2"))), base);
+                       try (Git git = new Git(db)) {
+                               setTargets();
+                               git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
+                               File link = new File(trash, "link");
+                               assertSymLink(link, "target2");
+                               MergeResult result = git.merge().include(side)
+                                               .setMessage("merged").call();
+                               assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+                               // Link should be unmodified
+                               assertSymLink(link, "target2");
+                               checkTargets();
+                               assertEquals("[link, mode:120000, stage:1, content:target]"
+                                               + "[link, mode:120000, stage:2, content:target2]"
+                                               + "[link, mode:120000, stage:3, content:target1]",
+                                               indexState(CONTENT));
+                       }
+               }
+       }
+
+       // In file/link conflicts, C git never does a content merge. It records the
+       // stages in the index, and always puts the file into the workspace.
+
+       @Test
+       public void mergeWithFileSymlinkConflict() throws Exception {
+               assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
+               StoredConfig config = db.getConfig();
+               config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+                               ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
+               config.save();
+               try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+                       db.incrementOpen();
+                       RevCommit base = repo.commit(repo.tree());
+                       RevCommit side = repo.commit(
+                                       repo.tree(repo.link("link", repo.blob("target1"))), base);
+                       RevCommit head = repo.commit(
+                                       repo.tree(repo.file("link", repo.blob("not a link"))),
+                                       base);
+                       try (Git git = new Git(db)) {
+                               setTargets();
+                               git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
+                               File link = new File(trash, "link");
+                               assertFalse(Files.isSymbolicLink(link.toPath()));
+                               checkFile(link, "not a link");
+                               MergeResult result = git.merge().include(side)
+                                               .setMessage("merged").call();
+                               assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+                               // File should be unmodified
+                               assertFalse(Files.isSymbolicLink(link.toPath()));
+                               checkFile(link, "not a link");
+                               checkTargets();
+                               assertEquals("[link, mode:100644, stage:2, content:not a link]"
+                                               + "[link, mode:120000, stage:3, content:target1]",
+                                               indexState(CONTENT));
+                       }
+               }
+       }
+
+       @Test
+       public void mergeWithSymlinkFileConflict() throws Exception {
+               assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
+               StoredConfig config = db.getConfig();
+               config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+                               ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
+               config.save();
+               try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+                       db.incrementOpen();
+                       RevCommit base = repo.commit(repo.tree());
+                       RevCommit side = repo.commit(
+                                       repo.tree(repo.file("link", repo.blob("not a link"))),
+                                       base);
+                       RevCommit head = repo.commit(
+                                       repo.tree(repo.link("link", repo.blob("target2"))), base);
+                       try (Git git = new Git(db)) {
+                               setTargets();
+                               git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
+                               File link = new File(trash, "link");
+                               assertSymLink(link, "target2");
+                               MergeResult result = git.merge().include(side)
+                                               .setMessage("merged").call();
+                               assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+                               // Should now be a file!
+                               assertFalse(Files.isSymbolicLink(link.toPath()));
+                               checkFile(link, "not a link");
+                               checkTargets();
+                               assertEquals("[link, mode:120000, stage:2, content:target2]"
+                                               + "[link, mode:100644, stage:3, content:not a link]",
+                                               indexState(CONTENT));
+                       }
+               }
+       }
+
+       // In Delete/modify conflicts with the non-deleted side a link, C git puts
+       // the link into the working tree.
+
+       @Test
+       public void mergeWithSymlinkDeleteModify() throws Exception {
+               assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
+               StoredConfig config = db.getConfig();
+               config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+                               ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
+               config.save();
+               try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+                       db.incrementOpen();
+                       RevCommit base = repo
+                                       .commit(repo.tree(repo.link("link", repo.blob("target"))));
+                       RevCommit side = repo.commit(
+                                       repo.tree(repo.link("link", repo.blob("target1"))), base);
+                       RevCommit head = repo.commit(repo.tree(), base);
+                       try (Git git = new Git(db)) {
+                               setTargets();
+                               git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
+                               File link = new File(trash, "link");
+                               assertFalse(
+                                               Files.exists(link.toPath(), LinkOption.NOFOLLOW_LINKS));
+                               MergeResult result = git.merge().include(side)
+                                               .setMessage("merged").call();
+                               assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+                               // Link should have the content from side
+                               assertSymLink(link, "target1");
+                               checkTargets();
+                               assertEquals("[link, mode:120000, stage:1, content:target]"
+                                               + "[link, mode:120000, stage:3, content:target1]",
+                                               indexState(CONTENT));
+                       }
+               }
+       }
+
+       @Test
+       public void mergeWithSymlinkModifyDelete() throws Exception {
+               assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
+               StoredConfig config = db.getConfig();
+               config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+                               ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
+               config.save();
+               try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+                       db.incrementOpen();
+                       RevCommit base = repo
+                                       .commit(repo.tree(repo.link("link", repo.blob("target"))));
+                       RevCommit side = repo.commit(repo.tree(), base);
+                       RevCommit head = repo.commit(
+                                       repo.tree(repo.link("link", repo.blob("target2"))), base);
+                       try (Git git = new Git(db)) {
+                               setTargets();
+                               git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
+                               File link = new File(trash, "link");
+                               assertSymLink(link, "target2");
+                               MergeResult result = git.merge().include(side)
+                                               .setMessage("merged").call();
+                               assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+                               // Link should be unmodified
+                               assertSymLink(link, "target2");
+                               checkTargets();
+                               assertEquals("[link, mode:120000, stage:1, content:target]"
+                                               + "[link, mode:120000, stage:2, content:target2]",
+                                               indexState(CONTENT));
+                       }
+               }
+       }
+}
index 23f8e4a5d57c1ce0b2995f9966f7a07d67a5f1ab..8b9b569c389ed668d3b327edc0ad18110386c1bd 100644 (file)
@@ -3,7 +3,7 @@
  * Copyright (C) 2010-2012, Matthias Sohn <matthias.sohn@sap.com>
  * Copyright (C) 2012, Research In Motion Limited
  * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr)
- * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <twolf@apache.org> 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
@@ -320,6 +320,25 @@ public class ResolveMerger extends ThreeWayMerger {
                return null;
        }
 
+       /**
+        * Adds the conflict stages for the current path of {@link #tw} to the index
+        * builder and returns the "theirs" stage; if present.
+        *
+        * @param base
+        *            of the conflict
+        * @param ours
+        *            of the conflict
+        * @param theirs
+        *            of the conflict
+        * @return the {@link DirCacheEntry} for the "theirs" stage, or {@code null}
+        */
+       private DirCacheEntry addConflict(CanonicalTreeParser base,
+                       CanonicalTreeParser ours, CanonicalTreeParser theirs) {
+               add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
+               add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
+               return add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
+       }
+
        /**
         * adds a entry to the index builder which is a copy of the specified
         * DirCacheEntry
@@ -501,9 +520,7 @@ public class ResolveMerger extends ThreeWayMerger {
                                // length.
                                // This path can be skipped on ignoreConflicts, so the caller
                                // could use virtual commit.
-                               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);
+                               addConflict(base, ours, theirs);
                                unmergedPaths.add(tw.getPathString());
                                mergeResults.put(tw.getPathString(),
                                                new MergeResult<>(Collections.emptyList()));
@@ -608,9 +625,7 @@ public class ResolveMerger extends ThreeWayMerger {
                                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);
+                               addConflict(base, ours, theirs);
                                MergeResult<SubmoduleConflict> result = createGitLinksMergeResult(
                                                base, ours, theirs);
                                result.setContainsConflicts(true);
@@ -631,9 +646,7 @@ public class ResolveMerger extends ThreeWayMerger {
                                        default:
                                                break;
                                }
-                               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);
+                               addConflict(base, ours, theirs);
 
                                // attribute merge issues are conflicts but not failures
                                unmergedPaths.add(tw.getPathString());
@@ -646,30 +659,61 @@ public class ResolveMerger extends ThreeWayMerger {
                        }
 
                        MergeResult<RawText> result = null;
-                       try {
-                               result = contentMerge(base, ours, theirs, attributes,
-                                               getContentMergeStrategy());
-                       } catch (BinaryBlobException e) {
+                       boolean hasSymlink = FileMode.SYMLINK.equals(modeO)
+                                       || FileMode.SYMLINK.equals(modeT);
+                       if (!hasSymlink) {
+                               try {
+                                       result = contentMerge(base, ours, theirs, attributes,
+                                                       getContentMergeStrategy());
+                               } catch (BinaryBlobException e) {
+                                       // result == null
+                               }
+                       }
+                       if (result == null) {
                                switch (getContentMergeStrategy()) {
-                                       case OURS:
-                                               keep(ourDce);
-                                               return true;
-                                       case THEIRS:
-                                               DirCacheEntry theirEntry = add(tw.getRawPath(), theirs,
-                                                               DirCacheEntry.STAGE_0, EPOCH, 0);
-                                               addToCheckout(tw.getPathString(), theirEntry, attributes);
-                                               return true;
-                                       default:
-                                               result = new MergeResult<>(Collections.emptyList());
-                                               result.setContainsConflicts(true);
-                                               break;
+                               case OURS:
+                                       keep(ourDce);
+                                       return true;
+                               case THEIRS:
+                                       DirCacheEntry e = add(tw.getRawPath(), theirs,
+                                                       DirCacheEntry.STAGE_0, EPOCH, 0);
+                                       if (e != null) {
+                                               addToCheckout(tw.getPathString(), e, attributes);
+                                       }
+                                       return true;
+                               default:
+                                       result = new MergeResult<>(Collections.emptyList());
+                                       result.setContainsConflicts(true);
+                                       break;
                                }
                        }
                        if (ignoreConflicts) {
                                result.setContainsConflicts(false);
                        }
-                       updateIndex(base, ours, theirs, result, attributes[T_OURS]);
                        String currentPath = tw.getPathString();
+                       if (hasSymlink) {
+                               if (ignoreConflicts) {
+                                       if (((modeT & FileMode.TYPE_MASK) == FileMode.TYPE_FILE)) {
+                                               DirCacheEntry e = add(tw.getRawPath(), theirs,
+                                                               DirCacheEntry.STAGE_0, EPOCH, 0);
+                                               addToCheckout(currentPath, e, attributes);
+                                       } else {
+                                               keep(ourDce);
+                                       }
+                               } else {
+                                       // Record the conflict
+                                       DirCacheEntry e = addConflict(base, ours, theirs);
+                                       mergeResults.put(currentPath, result);
+                                       // If theirs is a file, check it out. In link/file
+                                       // conflicts, C git prefers the file.
+                                       if (((modeT & FileMode.TYPE_MASK) == FileMode.TYPE_FILE)
+                                                       && e != null) {
+                                               addToCheckout(currentPath, e, attributes);
+                                       }
+                               }
+                       } else {
+                               updateIndex(base, ours, theirs, result, attributes[T_OURS]);
+                       }
                        if (result.containsConflicts() && !ignoreConflicts) {
                                unmergedPaths.add(currentPath);
                        }
@@ -683,40 +727,58 @@ public class ResolveMerger extends ThreeWayMerger {
                                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);
-                                       add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
+                                       addConflict(base, ours, theirs);
                                        MergeResult<SubmoduleConflict> result = createGitLinksMergeResult(
                                                        base, ours, theirs);
                                        result.setContainsConflicts(true);
                                        mergeResults.put(tw.getPathString(), result);
                                        unmergedPaths.add(tw.getPathString());
                                } else {
+                                       boolean isSymLink = ((modeO | modeT)
+                                                       & FileMode.TYPE_MASK) == FileMode.TYPE_SYMLINK;
                                        // Content merge strategy does not apply to delete-modify
                                        // conflicts!
                                        MergeResult<RawText> result;
-                                       try {
-                                               result = contentMerge(base, ours, theirs, attributes,
-                                                               ContentMergeStrategy.CONFLICT);
-                                       } catch (BinaryBlobException e) {
+                                       if (isSymLink) {
+                                               // No need to do a content merge
                                                result = new MergeResult<>(Collections.emptyList());
                                                result.setContainsConflicts(true);
+                                       } else {
+                                               try {
+                                                       result = contentMerge(base, ours, theirs,
+                                                                       attributes, ContentMergeStrategy.CONFLICT);
+                                               } catch (BinaryBlobException e) {
+                                                       result = new MergeResult<>(Collections.emptyList());
+                                                       result.setContainsConflicts(true);
+                                               }
                                        }
                                        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[T_OURS]);
+                                               if (isSymLink) {
+                                                       if (modeO != 0) {
+                                                               keep(ourDce);
+                                                       } else {
+                                                               // Check out theirs
+                                                               if (isWorktreeDirty(work, ourDce)) {
+                                                                       return false;
+                                                               }
+                                                               DirCacheEntry e = add(tw.getRawPath(), theirs,
+                                                                               DirCacheEntry.STAGE_0, EPOCH, 0);
+                                                               if (e != null) {
+                                                                       addToCheckout(tw.getPathString(), e,
+                                                                                       attributes);
+                                                               }
+                                                       }
+                                               } else {
+                                                       // 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.
+                                                       updateIndex(base, ours, theirs, result,
+                                                                       attributes[T_OURS]);
+                                               }
                                        } 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);
+                                               DirCacheEntry e = addConflict(base, ours, theirs);
 
                                                // OURS was deleted checkout THEIRS
                                                if (modeO == 0) {
@@ -862,25 +924,25 @@ public class ResolveMerger extends ThreeWayMerger {
                                // A conflict occurred, the file will contain conflict markers
                                // the index will be populated with the three stages and the
                                // workdir (if used) contains the halfway merged content.
-                               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);
+                               addConflict(base, ours, theirs);
                                mergeResults.put(tw.getPathString(), result);
                                return;
                        }
 
                        // No conflict occurred, the file will contain fully merged content.
                        // The index will be populated with the new merged version.
-                       Instant lastModified =
-                                       mergedFile == null ? null : nonNullRepo().getFS().lastModifiedInstant(mergedFile);
+                       Instant lastModified = mergedFile == null ? null
+                                       : nonNullRepo().getFS().lastModifiedInstant(mergedFile);
                        // Set the mode for the new content. Fall back to REGULAR_FILE if
                        // we can't merge modes of OURS and THEIRS.
                        int newMode = mergeFileModes(tw.getRawMode(0), tw.getRawMode(1),
                                        tw.getRawMode(2));
                        FileMode mode = newMode == FileMode.MISSING.getBits()
                                        ? FileMode.REGULAR_FILE : FileMode.fromBits(newMode);
-                       workTreeUpdater.insertToIndex(rawMerged.openInputStream(), tw.getPathString().getBytes(UTF_8), mode,
-                                       DirCacheEntry.STAGE_0, lastModified, (int) rawMerged.length(),
+                       workTreeUpdater.insertToIndex(rawMerged.openInputStream(),
+                                       tw.getPathString().getBytes(UTF_8), mode,
+                                       DirCacheEntry.STAGE_0, lastModified,
+                                       (int) rawMerged.length(),
                                        attributes.get(Constants.ATTR_MERGE));
                } finally {
                        if (rawMerged != null) {