Allow to resolve a conflict by checking out a file

DirCacheEditor unconditionally applied a PathEdit to all stages in the
index. This gives wrong results if one wants to check out a file from
some commit to resolve a conflict: JGit would update the working tree
file multiple times (once per stage), and set all stages to point to
the checked-out blob.

C git replaces the stages by the entry for the checked-out file.

To support this, add a DirCacheEntry.setStage() method so that
CheckoutCommand can force the stage to zero. In DirCacheEditor, keep
only the zero stage if the PathEdit re-set the stage.

Bug: 568038
Change-Id: Ic7c635bb5aaa06ffaaeed50bc5e45702c56fc6d1
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java
index e0a1c1d..f52b715 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011, Kevin Sawicki <kevin@github.com> and others
+ * Copyright (C) 2011, 2020 Kevin Sawicki <kevin@github.com> 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
@@ -24,6 +24,7 @@
 import org.eclipse.jgit.errors.NoWorkTreeException;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.RepositoryState;
 import org.eclipse.jgit.lib.StoredConfig;
@@ -310,6 +311,16 @@
 	}
 
 	@Test
+	public void testCheckoutFileWithConflict() throws Exception {
+		setupConflictingState();
+		assertEquals('[' + FILE1 + ']',
+				git.status().call().getConflicting().toString());
+		git.checkout().setStartPoint(Constants.HEAD).addPath(FILE1).call();
+		assertEquals("3", read(FILE1));
+		assertTrue(git.status().call().isClean());
+	}
+
+	@Test
 	public void testCheckoutOursWhenNoBase() throws Exception {
 		String file = "added.txt";
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
index 5477f56..8e84dfa 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
@@ -242,6 +242,46 @@
 	}
 
 	@Test
+	public void testSetStage() {
+		DirCacheEntry e = new DirCacheEntry("some/path", DirCacheEntry.STAGE_1);
+		e.setAssumeValid(true);
+		e.setCreationTime(2L);
+		e.setFileMode(FileMode.EXECUTABLE_FILE);
+		e.setLastModified(EPOCH.plusMillis(3L));
+		e.setLength(100L);
+		e.setObjectId(ObjectId
+				.fromString("0123456789012345678901234567890123456789"));
+		e.setUpdateNeeded(true);
+		e.setStage(DirCacheEntry.STAGE_2);
+
+		assertTrue(e.isAssumeValid());
+		assertEquals(2L, e.getCreationTime());
+		assertEquals(
+				ObjectId.fromString("0123456789012345678901234567890123456789"),
+				e.getObjectId());
+		assertEquals(FileMode.EXECUTABLE_FILE, e.getFileMode());
+		assertEquals(EPOCH.plusMillis(3L), e.getLastModifiedInstant());
+		assertEquals(100L, e.getLength());
+		assertEquals(DirCacheEntry.STAGE_2, e.getStage());
+		assertTrue(e.isUpdateNeeded());
+		assertEquals("some/path", e.getPathString());
+
+		e.setStage(DirCacheEntry.STAGE_0);
+
+		assertTrue(e.isAssumeValid());
+		assertEquals(2L, e.getCreationTime());
+		assertEquals(
+				ObjectId.fromString("0123456789012345678901234567890123456789"),
+				e.getObjectId());
+		assertEquals(FileMode.EXECUTABLE_FILE, e.getFileMode());
+		assertEquals(EPOCH.plusMillis(3L), e.getLastModifiedInstant());
+		assertEquals(100L, e.getLength());
+		assertEquals(DirCacheEntry.STAGE_0, e.getStage());
+		assertTrue(e.isUpdateNeeded());
+		assertEquals("some/path", e.getPathString());
+	}
+
+	@Test
 	public void testCopyMetaDataWithStage() {
 		copyMetaDataHelper(false);
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java
index 39a1f01..5778d28 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011, Robin Rosenberg and others
+ * Copyright (C) 2011, 2020 Robin Rosenberg 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
@@ -124,6 +124,32 @@
 	}
 
 	@Test
+	public void testPathEditWithStagesAndReset() throws Exception {
+		DirCache dc = DirCache.newInCore();
+		DirCacheBuilder builder = new DirCacheBuilder(dc, 3);
+		builder.add(createEntry("a", DirCacheEntry.STAGE_1));
+		builder.add(createEntry("a", DirCacheEntry.STAGE_2));
+		builder.add(createEntry("a", DirCacheEntry.STAGE_3));
+		builder.finish();
+
+		DirCacheEditor editor = dc.editor();
+		PathEdit edit = new PathEdit("a") {
+
+			@Override
+			public void apply(DirCacheEntry ent) {
+				ent.setStage(DirCacheEntry.STAGE_0);
+			}
+		};
+		editor.add(edit);
+		editor.finish();
+
+		assertEquals(1, dc.getEntryCount());
+		DirCacheEntry entry = dc.getEntry(0);
+		assertEquals("a", entry.getPathString());
+		assertEquals(DirCacheEntry.STAGE_0, entry.getStage());
+	}
+
+	@Test
 	public void testFileReplacesTree() throws Exception {
 		DirCache dc = DirCache.newInCore();
 		DirCacheEditor editor = dc.editor();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
index 0dc5d5e..847ab0a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com>
- * Copyright (C) 2011, Matthias Sohn <matthias.sohn@sap.com> and others
+ * Copyright (C) 2011, 2020 Matthias Sohn <matthias.sohn@sap.com> 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
@@ -503,6 +503,11 @@
 			editor.add(new PathEdit(path) {
 				@Override
 				public void apply(DirCacheEntry ent) {
+					if (ent.getStage() != DirCacheEntry.STAGE_0) {
+						// A checkout on a conflicting file stages the checked
+						// out file and resolves the conflict.
+						ent.setStage(DirCacheEntry.STAGE_0);
+					}
 					ent.setObjectId(blobId);
 					ent.setFileMode(mode);
 					checkoutPath(ent, r,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java
index 73d2807..8c342e2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java
@@ -1,6 +1,6 @@
 /*
- * Copyright (C) 2008-2009, Google Inc.
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2009, Google Inc.
+ * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.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
@@ -139,10 +139,28 @@
 					: eIdx;
 				fastAdd(ent);
 			} else {
-				// Apply to all entries of the current path (different stages)
 				lastIdx = cache.nextEntry(eIdx);
-				for (int i = eIdx; i < lastIdx; i++) {
-					final DirCacheEntry ent = cache.getEntry(i);
+				if (lastIdx > eIdx + 1) {
+					// Apply to all entries of the current path (different
+					// stages). If any apply() resets the stage to STAGE_0, take
+					// only that entry and omit all others.
+					DirCacheEntry[] tmp = new DirCacheEntry[lastIdx - eIdx];
+					int n = 0;
+					for (int i = eIdx; i < lastIdx; i++) {
+						DirCacheEntry ent = cache.getEntry(i);
+						e.apply(ent);
+						if (ent.getStage() == DirCacheEntry.STAGE_0) {
+							fastAdd(ent);
+							n = 0;
+							break;
+						}
+						tmp[n++] = ent;
+					}
+					for (int i = 0; i < n; i++) {
+						fastAdd(tmp[i]);
+					}
+				} else {
+					DirCacheEntry ent = cache.getEntry(eIdx);
 					e.apply(ent);
 					fastAdd(ent);
 				}
@@ -257,7 +275,9 @@
 	 * {@link #apply(DirCacheEntry)} method. The editor will invoke apply once
 	 * for each record in the index which matches the path name. If there are
 	 * multiple records (for example in stages 1, 2 and 3), the edit instance
-	 * will be called multiple times, once for each stage.
+	 * will be called multiple times, once for each stage. If any of these calls
+	 * resets the stage to 0, only this entry will be taken and entries for
+	 * other stages are discarded.
 	 */
 	public abstract static class PathEdit {
 		final byte[] path;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
index dcb8482..67edf50 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
@@ -542,6 +542,24 @@
 	}
 
 	/**
+	 * Sets the stage of an entry.
+	 *
+	 * @param stage
+	 *            to set, in the range [0..3]
+	 * @throws IllegalArgumentException
+	 *             if the stage is outside the range [0..3]
+	 * @since 5.10
+	 */
+	public void setStage(int stage) {
+		if ((stage & ~0x3) != 0) {
+			throw new IllegalArgumentException(
+					"Invalid stage, must be in range [0..3]"); //$NON-NLS-1$
+		}
+		byte flags = info[infoOffset + P_FLAGS];
+		info[infoOffset + P_FLAGS] = (byte) ((flags & 0xCF) | (stage << 4));
+	}
+
+	/**
 	 * Returns whether this entry should be skipped from the working tree.
 	 *
 	 * @return true if this entry should be skipepd.