Separate RefUpdate.Result.REJECTED_{MISSING_OBJECT,OTHER_REASON}

ReceiveCommand.Result has a slightly richer set of possibilities, so it
makes sense for RefUpdate.Result to have more values in order to match.
In particular, this allows us to return REJECTED_MISSING_OBJECT from
RefUpdate when an object is missing.

The comment in RefUpdate#safeParse about expecting some old objects to be
missing is only applicable to the old ID, not the new ID. A missing new
ID is a bug or programmer error, and we should not update a ref to point
to one.

Fix various tests that started failing because they depended for no good
reason on setting refs to point to nonexistent objects; it's always easy
to create a real object when necessary.

It is possible that some downstream users of RefUpdate.Result might
choose to handle one of the new statuses differently, for example by
providing a more user-readable error message; that is not done in this
change.

Change-Id: I734b1c32d5404752447d9e20329471436ffe05fc
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
index 97130f2..29f4a57 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
@@ -75,6 +75,7 @@
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Ref.Storage;
@@ -1409,6 +1410,54 @@
 		assertEquals(A.getId(), refs.get("refs/heads/masters/x").getObjectId());
 	}
 
+	@Test
+	public void testBatchRefUpdateUpdateToMissingObject() throws IOException {
+		writeLooseRef("refs/heads/master", A);
+		ObjectId bad =
+				ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+		List<ReceiveCommand> commands = Arrays.asList(
+				new ReceiveCommand(A, bad, "refs/heads/master",
+						ReceiveCommand.Type.UPDATE),
+				new ReceiveCommand(zeroId(), B, "refs/heads/foo2",
+						ReceiveCommand.Type.CREATE));
+		BatchRefUpdate batchUpdate = refdir.newBatchUpdate();
+		batchUpdate.setAtomic(false);
+		batchUpdate.setAllowNonFastForwards(true);
+		batchUpdate.addCommand(commands);
+		batchUpdate.execute(new RevWalk(diskRepo), NullProgressMonitor.INSTANCE);
+		Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL);
+		assertEquals(ReceiveCommand.Result.REJECTED_MISSING_OBJECT,
+				commands.get(0).getResult());
+		assertEquals(ReceiveCommand.Result.OK, commands.get(1).getResult());
+		assertEquals("[HEAD, refs/heads/foo2, refs/heads/master]", refs.keySet()
+				.toString());
+		assertEquals(A.getId(), refs.get("refs/heads/master").getObjectId());
+		assertEquals(B.getId(), refs.get("refs/heads/foo2").getObjectId());
+	}
+
+	@Test
+	public void testBatchRefUpdateAddMissingObject() throws IOException {
+		writeLooseRef("refs/heads/master", A);
+		ObjectId bad =
+				ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+		List<ReceiveCommand> commands = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master",
+						ReceiveCommand.Type.UPDATE),
+				new ReceiveCommand(zeroId(), bad, "refs/heads/foo2",
+						ReceiveCommand.Type.CREATE));
+		BatchRefUpdate batchUpdate = refdir.newBatchUpdate();
+		batchUpdate.setAllowNonFastForwards(true);
+		batchUpdate.addCommand(commands);
+		batchUpdate.execute(new RevWalk(diskRepo), NullProgressMonitor.INSTANCE);
+		Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL);
+		assertEquals(ReceiveCommand.Result.OK, commands.get(0).getResult());
+		assertEquals(ReceiveCommand.Result.REJECTED_MISSING_OBJECT,
+				commands.get(1).getResult());
+		assertEquals("[HEAD, refs/heads/master]", refs.keySet()
+				.toString());
+		assertEquals(B.getId(), refs.get("refs/heads/master").getObjectId());
+	}
+
 	private void writeLooseRef(String name, AnyObjectId id) throws IOException {
 		writeLooseRef(name, id.name() + "\n");
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java
index 1203e83..34f9eb9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java
@@ -58,12 +58,10 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
-import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -969,27 +967,10 @@
 		RefUpdate ru = db.updateRef(name);
 		ru.setNewObjectId(bad);
 		Result update = ru.update();
-		assertEquals(Result.NEW, update);
+		assertEquals(Result.REJECTED_MISSING_OBJECT, update);
 
 		Ref ref = db.exactRef(name);
-		assertNotNull(ref);
-		assertFalse(ref.isPeeled());
-		assertEquals(bad, ref.getObjectId());
-
-		try (RevWalk rw = new RevWalk(db)) {
-			rw.parseAny(ref.getObjectId());
-			fail("Expected MissingObjectException");
-		} catch (MissingObjectException expected) {
-			assertEquals(bad, expected.getObjectId());
-		}
-
-		RefDirectory refdir = (RefDirectory) db.getRefDatabase();
-		try {
-			// Packing requires peeling, which fails.
-			refdir.pack(Arrays.asList(name));
-		} catch (MissingObjectException expected) {
-			assertEquals(bad, expected.getObjectId());
-		}
+		assertNull(ref);
 	}
 
 	@Test
@@ -1005,7 +986,7 @@
 		ru = db.updateRef(name);
 		ru.setNewObjectId(bad);
 		update = ru.update();
-		assertEquals(Result.REJECTED, update);
+		assertEquals(Result.REJECTED_MISSING_OBJECT, update);
 
 		Ref ref = db.exactRef(name);
 		assertNotNull(ref);
@@ -1018,33 +999,18 @@
 		RefUpdate ru = updateRef(name);
 		Result update = ru.update();
 		assertEquals(Result.NEW, update);
+		ObjectId oldId = ru.getNewObjectId();
 
 		ObjectId bad =
 				ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
 		ru = db.updateRef(name);
 		ru.setNewObjectId(bad);
 		update = ru.forceUpdate();
-		assertEquals(Result.FORCED, update);
+		assertEquals(Result.REJECTED_MISSING_OBJECT, update);
 
 		Ref ref = db.exactRef(name);
 		assertNotNull(ref);
-		assertFalse(ref.isPeeled());
-		assertEquals(bad, ref.getObjectId());
-
-		try (RevWalk rw = new RevWalk(db)) {
-			rw.parseAny(ref.getObjectId());
-			fail("Expected MissingObjectException");
-		} catch (MissingObjectException expected) {
-			assertEquals(bad, expected.getObjectId());
-		}
-
-		RefDirectory refdir = (RefDirectory) db.getRefDatabase();
-		try {
-			// Packing requires peeling, which fails.
-			refdir.pack(Arrays.asList(name));
-		} catch (MissingObjectException expected) {
-			assertEquals(bad, expected.getObjectId());
-		}
+		assertEquals(oldId, ref.getObjectId());
 	}
 
 	private static void writeReflog(Repository db, ObjectId newId, String msg,
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
index ae1e531..9d23d83 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
@@ -661,33 +661,39 @@
 
 	@Test
 	public void test028_LockPackedRef() throws IOException {
+		ObjectId id1;
+		ObjectId id2;
+		try (ObjectInserter ins = db.newObjectInserter()) {
+			id1 = ins.insert(
+					Constants.OBJ_BLOB, "contents1".getBytes(Constants.CHARSET));
+			id2 = ins.insert(
+					Constants.OBJ_BLOB, "contents2".getBytes(Constants.CHARSET));
+			ins.flush();
+		}
+
 		writeTrashFile(".git/packed-refs",
-				"7f822839a2fe9760f386cbbbcb3f92c5fe81def7 refs/heads/foobar");
+				id1.name() + " refs/heads/foobar");
 		writeTrashFile(".git/HEAD", "ref: refs/heads/foobar\n");
 		BUG_WorkAroundRacyGitIssues("packed-refs");
 		BUG_WorkAroundRacyGitIssues("HEAD");
 
 		ObjectId resolve = db.resolve("HEAD");
-		assertEquals("7f822839a2fe9760f386cbbbcb3f92c5fe81def7", resolve.name());
+		assertEquals(id1, resolve);
 
 		RefUpdate lockRef = db.updateRef("HEAD");
-		ObjectId newId = ObjectId
-				.fromString("07f822839a2fe9760f386cbbbcb3f92c5fe81def");
-		lockRef.setNewObjectId(newId);
+		lockRef.setNewObjectId(id2);
 		assertEquals(RefUpdate.Result.FORCED, lockRef.forceUpdate());
 
 		assertTrue(new File(db.getDirectory(), "refs/heads/foobar").exists());
-		assertEquals(newId, db.resolve("refs/heads/foobar"));
+		assertEquals(id2, db.resolve("refs/heads/foobar"));
 
 		// Again. The ref already exists
 		RefUpdate lockRef2 = db.updateRef("HEAD");
-		ObjectId newId2 = ObjectId
-				.fromString("7f822839a2fe9760f386cbbbcb3f92c5fe81def7");
-		lockRef2.setNewObjectId(newId2);
+		lockRef2.setNewObjectId(id1);
 		assertEquals(RefUpdate.Result.FORCED, lockRef2.forceUpdate());
 
 		assertTrue(new File(db.getDirectory(), "refs/heads/foobar").exists());
-		assertEquals(newId2, db.resolve("refs/heads/foobar"));
+		assertEquals(id1, db.resolve("refs/heads/foobar"));
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleStatusTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleStatusTest.java
index 61df9d9..5832518 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleStatusTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleStatusTest.java
@@ -59,11 +59,11 @@
 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -256,11 +256,16 @@
 	}
 
 	@Test
-	public void repositoryWithInitializedSubmodule() throws IOException,
-			GitAPIException {
-		final ObjectId id = ObjectId
-				.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
-		final String path = "sub";
+	public void repositoryWithInitializedSubmodule() throws Exception {
+		String path = "sub";
+		Repository subRepo = Git.init().setBare(false)
+				.setDirectory(new File(db.getWorkTree(), path)).call()
+				.getRepository();
+		assertNotNull(subRepo);
+
+		TestRepository<?> subTr = new TestRepository<>(subRepo);
+		ObjectId id = subTr.branch(Constants.HEAD).commit().create().copy();
+
 		DirCache cache = db.lockDirCache();
 		DirCacheEditor editor = cache.editor();
 		editor.add(new PathEdit(path) {
@@ -287,15 +292,6 @@
 				ConfigConstants.CONFIG_KEY_URL, url);
 		modulesConfig.save();
 
-		Repository subRepo = Git.init().setBare(false)
-				.setDirectory(new File(db.getWorkTree(), path)).call()
-				.getRepository();
-		assertNotNull(subRepo);
-
-		RefUpdate update = subRepo.updateRef(Constants.HEAD, true);
-		update.setNewObjectId(id);
-		update.forceUpdate();
-
 		SubmoduleStatusCommand command = new SubmoduleStatusCommand(db);
 		Map<String, SubmoduleStatus> statuses = command.call();
 		assertNotNull(statuses);
@@ -312,11 +308,16 @@
 	}
 
 	@Test
-	public void repositoryWithDifferentRevCheckedOutSubmodule()
-			throws IOException, GitAPIException {
-		final ObjectId id = ObjectId
-				.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
-		final String path = "sub";
+	public void repositoryWithDifferentRevCheckedOutSubmodule() throws Exception {
+		String path = "sub";
+		Repository subRepo = Git.init().setBare(false)
+				.setDirectory(new File(db.getWorkTree(), path)).call()
+				.getRepository();
+		assertNotNull(subRepo);
+
+		TestRepository<?> subTr = new TestRepository<>(subRepo);
+		ObjectId id = subTr.branch(Constants.HEAD).commit().create().copy();
+
 		DirCache cache = db.lockDirCache();
 		DirCacheEditor editor = cache.editor();
 		editor.add(new PathEdit(path) {
@@ -343,15 +344,7 @@
 				ConfigConstants.CONFIG_KEY_URL, url);
 		modulesConfig.save();
 
-		Repository subRepo = Git.init().setBare(false)
-				.setDirectory(new File(db.getWorkTree(), path)).call()
-				.getRepository();
-		assertNotNull(subRepo);
-
-		RefUpdate update = subRepo.updateRef(Constants.HEAD, true);
-		update.setNewObjectId(ObjectId
-				.fromString("aaaa0000aaaa0000aaaa0000aaaa0000aaaa0000"));
-		update.forceUpdate();
+		ObjectId newId = subTr.branch(Constants.HEAD).commit().create().copy();
 
 		SubmoduleStatusCommand command = new SubmoduleStatusCommand(db);
 		Map<String, SubmoduleStatus> statuses = command.call();
@@ -365,7 +358,7 @@
 		assertNotNull(status);
 		assertEquals(path, status.getPath());
 		assertEquals(id, status.getIndexId());
-		assertEquals(update.getNewObjectId(), status.getHeadId());
+		assertEquals(newId, status.getHeadId());
 		assertEquals(SubmoduleStatusType.REV_CHECKED_OUT, status.getType());
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java
index 3c5ecfb..4a4db12 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java
@@ -59,6 +59,7 @@
 import java.util.concurrent.TimeoutException;
 
 import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -410,6 +411,11 @@
 		for (ReceiveCommand cmd : commands) {
 			try {
 				if (cmd.getResult() == NOT_ATTEMPTED) {
+					if (isMissing(walk, cmd.getOldId())
+							|| isMissing(walk, cmd.getNewId())) {
+						cmd.setResult(ReceiveCommand.Result.REJECTED_MISSING_OBJECT);
+						continue;
+					}
 					cmd.updateType(walk);
 					switch (cmd.getType()) {
 					case CREATE:
@@ -481,6 +487,19 @@
 		monitor.endTask();
 	}
 
+	private static boolean isMissing(RevWalk walk, ObjectId id)
+			throws IOException {
+		if (id.equals(ObjectId.zeroId())) {
+			return false; // Explicit add or delete is not missing.
+		}
+		try {
+			walk.parseAny(id);
+			return false;
+		} catch (MissingObjectException e) {
+			return true;
+		}
+	}
+
 	/**
 	 * Wait for timestamps to be in the past, aborting commands on timeout.
 	 *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java
index 61fda94..0778645 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java
@@ -58,7 +58,13 @@
  * Creates, updates or deletes any reference.
  */
 public abstract class RefUpdate {
-	/** Status of an update request. */
+	/**
+	 * Status of an update request.
+	 * <p>
+	 * New values may be added to this enum in the future. Callers may assume that
+	 * unknown values are failures, and may generally treat them the same as
+	 * {@link #REJECTED_OTHER_REASON}.
+	 */
 	public static enum Result {
 		/** The ref update/delete has not been attempted by the caller. */
 		NOT_ATTEMPTED,
@@ -114,6 +120,10 @@
 		 * merged into the new value. The configuration did not allow a forced
 		 * update/delete to take place, so ref still contains the old value. No
 		 * previous history was lost.
+		 * <p>
+		 * <em>Note:</em> Despite the general name, this result only refers to the
+		 * non-fast-forward case. For more general errors, see {@link
+		 * #REJECTED_OTHER_REASON}.
 		 */
 		REJECTED,
 
@@ -139,7 +149,25 @@
 		 * The ref was renamed from another name
 		 * <p>
 		 */
-		RENAMED
+		RENAMED,
+
+		/**
+		 * One or more objects aren't in the repository.
+		 * <p>
+		 * This is severe indication of either repository corruption on the
+		 * server side, or a bug in the client wherein the client did not supply
+		 * all required objects during the pack transfer.
+		 *
+		 * @since 4.9
+		 */
+		REJECTED_MISSING_OBJECT,
+
+		/**
+		 * Rejected for some other reason not covered by another enum value.
+		 *
+		 * @since 4.9
+		 */
+		REJECTED_OTHER_REASON;
 	}
 
 	/** New value the caller wants this ref to have. */
@@ -637,34 +665,47 @@
 		RevObject oldObj;
 
 		// don't make expensive conflict check if this is an existing Ref
-		if (oldValue == null && checkConflicting && getRefDatabase().isNameConflicting(getName()))
+		if (oldValue == null && checkConflicting
+				&& getRefDatabase().isNameConflicting(getName())) {
 			return Result.LOCK_FAILURE;
+		}
 		try {
 			// If we're detaching a symbolic reference, we should update the reference
 			// itself. Otherwise, we will update the leaf reference, which should be
 			// an ObjectIdRef.
-			if (!tryLock(!detachingSymbolicRef))
+			if (!tryLock(!detachingSymbolicRef)) {
 				return Result.LOCK_FAILURE;
+			}
 			if (expValue != null) {
 				final ObjectId o;
 				o = oldValue != null ? oldValue : ObjectId.zeroId();
-				if (!AnyObjectId.equals(expValue, o))
+				if (!AnyObjectId.equals(expValue, o)) {
 					return Result.LOCK_FAILURE;
+				}
 			}
-			if (oldValue == null)
+			try {
+				newObj = safeParseNew(walk, newValue);
+			} catch (MissingObjectException e) {
+				return Result.REJECTED_MISSING_OBJECT;
+			}
+
+			if (oldValue == null) {
 				return store.execute(Result.NEW);
+			}
 
-			newObj = safeParse(walk, newValue);
-			oldObj = safeParse(walk, oldValue);
-			if (newObj == oldObj && !detachingSymbolicRef)
+			oldObj = safeParseOld(walk, oldValue);
+			if (newObj == oldObj && !detachingSymbolicRef) {
 				return store.execute(Result.NO_CHANGE);
+			}
 
-			if (isForceUpdate())
+			if (isForceUpdate()) {
 				return store.execute(Result.FORCED);
+			}
 
 			if (newObj instanceof RevCommit && oldObj instanceof RevCommit) {
-				if (walk.isMergedInto((RevCommit) oldObj, (RevCommit) newObj))
+				if (walk.isMergedInto((RevCommit) oldObj, (RevCommit) newObj)) {
 					return store.execute(Result.FAST_FORWARD);
+				}
 			}
 
 			return Result.REJECTED;
@@ -684,16 +725,23 @@
 		checkConflicting = check;
 	}
 
-	private static RevObject safeParse(final RevWalk rw, final AnyObjectId id)
+	private static RevObject safeParseNew(RevWalk rw, AnyObjectId newId)
+			throws IOException {
+		if (newId == null || ObjectId.zeroId().equals(newId)) {
+			return null;
+		}
+		return rw.parseAny(newId);
+	}
+
+	private static RevObject safeParseOld(RevWalk rw, AnyObjectId oldId)
 			throws IOException {
 		try {
-			return id != null ? rw.parseAny(id) : null;
+			return oldId != null ? rw.parseAny(oldId) : null;
 		} catch (MissingObjectException e) {
-			// We can expect some objects to be missing, like if we are
-			// trying to force a deletion of a branch and the object it
-			// points to has been pruned from the database due to freak
-			// corruption accidents (it happens with 'git new-work-dir').
-			//
+			// We can expect some old objects to be missing, like if we are trying to
+			// force a deletion of a branch and the object it points to has been
+			// pruned from the database due to freak corruption accidents (it happens
+			// with 'git new-work-dir').
 			return null;
 		}
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
index a3f7501..01ceef1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
@@ -467,6 +467,14 @@
 			setResult(Result.REJECTED_CURRENT_BRANCH);
 			break;
 
+		case REJECTED_MISSING_OBJECT:
+			setResult(Result.REJECTED_MISSING_OBJECT);
+			break;
+
+		case REJECTED_OTHER_REASON:
+			setResult(Result.REJECTED_OTHER_REASON);
+			break;
+
 		default:
 			setResult(Result.REJECTED_OTHER_REASON, r.name());
 			break;