reftable: resolve symbolic references

resolve(Ref) helps callers recursively chase symbolic references and
is a useful function when wrapping a Reftable inside a RefDatabase, as
RefCursor does not resolve symbolic references during iteration.

Change-Id: I1ba143f403773497972e225dc92c35ecb989e154
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java
index b53853b..6809d7b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java
@@ -52,6 +52,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -240,6 +241,42 @@
 	}
 
 	@Test
+	public void resolveSymbolicRef() throws IOException {
+		Reftable t = read(write(
+				sym(HEAD, "refs/heads/tmp"),
+				sym("refs/heads/tmp", MASTER),
+				ref(MASTER, 1)));
+
+		Ref head = t.exactRef(HEAD);
+		assertNull(head.getObjectId());
+		assertEquals("refs/heads/tmp", head.getTarget().getName());
+
+		head = t.resolve(head);
+		assertNotNull(head);
+		assertEquals(id(1), head.getObjectId());
+
+		Ref master = t.exactRef(MASTER);
+		assertNotNull(master);
+		assertSame(master, t.resolve(master));
+	}
+
+	@Test
+	public void failDeepChainOfSymbolicRef() throws IOException {
+		Reftable t = read(write(
+				sym(HEAD, "refs/heads/1"),
+				sym("refs/heads/1", "refs/heads/2"),
+				sym("refs/heads/2", "refs/heads/3"),
+				sym("refs/heads/3", "refs/heads/4"),
+				sym("refs/heads/4", "refs/heads/5"),
+				sym("refs/heads/5", MASTER),
+				ref(MASTER, 1)));
+
+		Ref head = t.exactRef(HEAD);
+		assertNull(head.getObjectId());
+		assertNull(t.resolve(head));
+	}
+
+	@Test
 	public void oneDeletedRef() throws IOException {
 		String name = "refs/heads/gone";
 		Ref exp = newRef(name);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/Reftable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/Reftable.java
index e07bd28..1189ed3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/Reftable.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/Reftable.java
@@ -43,6 +43,8 @@
 
 package org.eclipse.jgit.internal.storage.reftable;
 
+import static org.eclipse.jgit.lib.RefDatabase.MAX_SYMBOLIC_REF_DEPTH;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.Collection;
@@ -51,6 +53,7 @@
 import org.eclipse.jgit.internal.storage.io.BlockSource;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.SymbolicRef;
 
 /** Abstract table of references. */
 public abstract class Reftable implements AutoCloseable {
@@ -218,6 +221,42 @@
 		}
 	}
 
+	/**
+	 * Resolve a symbolic reference to populate its value.
+	 *
+	 * @param symref
+	 *            reference to resolve.
+	 * @return resolved {@code symref}, or {@code null}.
+	 * @throws IOException
+	 *             if references cannot be read.
+	 */
+	@Nullable
+	public Ref resolve(Ref symref) throws IOException {
+		return resolve(symref, 0);
+	}
+
+	private Ref resolve(Ref ref, int depth) throws IOException {
+		if (!ref.isSymbolic()) {
+			return ref;
+		}
+
+		Ref dst = ref.getTarget();
+		if (MAX_SYMBOLIC_REF_DEPTH <= depth) {
+			return null; // claim it doesn't exist
+		}
+
+		dst = exactRef(dst.getName());
+		if (dst == null) {
+			return ref;
+		}
+
+		dst = resolve(dst, depth + 1);
+		if (dst == null) {
+			return null; // claim it doesn't exist
+		}
+		return new SymbolicRef(ref.getName(), dst);
+	}
+
 	@Override
 	public abstract void close() throws IOException;
 }