Merge branch 'stable-5.10'

* stable-5.10:
  Remove unused imports
  Silence API warnings
  Remove erraneously merged source features
  Prepare 5.3.9-SNAPSHOT builds
  JGit v5.3.8.202011260953-r
  Prepare 5.1.15-SNAPSHOT builds
  JGit v5.1.14.202011251942-r
  GC#deleteOrphans: log warning for deleted orphaned files
  GC#deleteOrphans: handle failure to list files in pack directory
  Ensure that GC#deleteOrphans respects pack lock
  PacketLineIn: ensure that END != DELIM
  Update API warning filters
  Remove unused imports

Change-Id: I25f50c3807a4e6b22a264320ea7ed3758e2a75ec
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/LsRemoteTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/LsRemoteTest.java
index 4ecaeb6..46eec74 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/LsRemoteTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/LsRemoteTest.java
@@ -33,7 +33,7 @@
 		git.add().addFilepattern("Test.txt").call();
 		git.commit().setMessage("Initial commit").call();
 
-		// create a master branch and switch to it
+		// create a test branch and switch to it
 		git.branchCreate().setName("test").call();
 		RefUpdate rup = db.updateRef(Constants.HEAD);
 		rup.link("refs/heads/test");
@@ -104,4 +104,22 @@
 				"" }, result.toArray());
 	}
 
+	@Test
+	public void testLsRemoteSymRefs() throws Exception {
+		final List<String> result = CLIGitCommand.execute(
+				"git ls-remote --symref " + shellQuote(db.getDirectory()), db);
+		assertArrayEquals(new String[] {
+				"ref: refs/heads/test	HEAD",
+				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	HEAD",
+				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	refs/heads/master",
+				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	refs/heads/test",
+				"efc02078d83a5226986ae917323acec7e1e8b7cb	refs/tags/tag1",
+				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	refs/tags/tag1^{}",
+				"4e4b837e0fd4ba83c003678b03592dc1509a4115	refs/tags/tag2",
+				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	refs/tags/tag2^{}",
+				"489384bf8ace47522fe32093d2ceb85b65a6cbb1	refs/tags/tag3",
+				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	refs/tags/tag3^{}",
+				"" }, result.toArray());
+	}
+
 }
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index c116437..6112a27 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -256,6 +256,7 @@
 usage_LsRemote=List references in a remote repository
 usage_lsRemoteHeads=Show only refs starting with refs/heads
 usage_lsRemoteTags=Show only refs starting with refs/tags
+usage_lsRemoteSymref=In addition to the object pointed at, show the underlying ref pointed at when showing a symbolic ref.
 usage_LsTree=List the contents of a tree object
 usage_MakeCacheTree=Show the current cache tree structure
 usage_Match=Only consider tags matching the given glob(7) pattern or patterns, excluding the "refs/tags/" prefix.
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/LsRemote.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/LsRemote.java
index 36812c0..055b48a 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/LsRemote.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/LsRemote.java
@@ -34,6 +34,9 @@
 	@Option(name = "--timeout", metaVar = "metaVar_service", usage = "usage_abortConnectionIfNoActivity")
 	int timeout = -1;
 
+	@Option(name = "--symref", usage = "usage_lsRemoteSymref")
+	private boolean symref;
+
 	@Argument(index = 0, metaVar = "metaVar_uriish", required = true)
 	private String remote;
 
@@ -47,6 +50,9 @@
 		try {
 			refs.addAll(command.call());
 			for (Ref r : refs) {
+				if (symref && r.isSymbolic()) {
+					show(r.getTarget(), r.getName());
+				}
 				show(r.getObjectId(), r.getName());
 				if (r.getPeeledObjectId() != null) {
 					show(r.getPeeledObjectId(), r.getName() + "^{}"); //$NON-NLS-1$
@@ -70,4 +76,13 @@
 		outw.print(name);
 		outw.println();
 	}
+
+	private void show(Ref ref, String name)
+			throws IOException {
+		outw.print("ref: ");
+		outw.print(ref.getName());
+		outw.print('\t');
+		outw.print(name);
+		outw.println();
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
index b737bbe..de25870 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
@@ -92,7 +92,6 @@
 		command.setURI(fileUri());
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
 		ObjectId id = git2.getRepository().resolve("tag-for-blob");
 		assertNotNull(id);
 		assertEquals(git2.getRepository().getFullBranch(), "refs/heads/test");
@@ -277,8 +276,7 @@
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
 
-		assertNotNull(git2);
-		assertEquals(git2.getRepository().getFullBranch(), "refs/heads/master");
+		assertEquals("refs/heads/master", git2.getRepository().getFullBranch());
 		assertEquals(
 				"refs/heads/master, refs/remotes/origin/master, refs/remotes/origin/test",
 				allRefNames(git2.branchList().setListMode(ListMode.ALL).call()));
@@ -293,7 +291,6 @@
 		git2 = command.call();
 		addRepoToClose(git2.getRepository());
 
-		assertNotNull(git2);
 		assertEquals(git2.getRepository().getFullBranch(), "refs/heads/master");
 		assertEquals("refs/remotes/origin/master, refs/remotes/origin/test",
 				allRefNames(git2.branchList().setListMode(ListMode.ALL).call()));
@@ -308,8 +305,7 @@
 		git2 = command.call();
 		addRepoToClose(git2.getRepository());
 
-		assertNotNull(git2);
-		assertEquals(git2.getRepository().getFullBranch(), "refs/heads/master");
+		assertEquals("refs/heads/master", git2.getRepository().getFullBranch());
 		assertEquals("refs/heads/master, refs/heads/test", allRefNames(git2
 				.branchList().setListMode(ListMode.ALL).call()));
 	}
@@ -324,7 +320,6 @@
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
 
-		assertNotNull(git2);
 		assertEquals("refs/heads/test", git2.getRepository().getFullBranch());
 	}
 
@@ -338,7 +333,6 @@
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
 
-		assertNotNull(git2);
 		ObjectId taggedCommit = db.resolve("tag-initial^{commit}");
 		assertEquals(taggedCommit.name(), git2
 				.getRepository().getFullBranch());
@@ -355,10 +349,9 @@
 		command.setURI(fileUri());
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
 		assertNull(git2.getRepository().resolve("tag-for-blob"));
 		assertNotNull(git2.getRepository().resolve("tag-initial"));
-		assertEquals(git2.getRepository().getFullBranch(), "refs/heads/master");
+		assertEquals("refs/heads/master", git2.getRepository().getFullBranch());
 		assertEquals("refs/remotes/origin/master", allRefNames(git2
 				.branchList().setListMode(ListMode.REMOTE).call()));
 		RemoteConfig cfg = new RemoteConfig(git2.getRepository().getConfig(),
@@ -383,10 +376,9 @@
 		command.setBare(true);
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
 		assertNull(git2.getRepository().resolve("tag-for-blob"));
 		assertNotNull(git2.getRepository().resolve("tag-initial"));
-		assertEquals(git2.getRepository().getFullBranch(), "refs/heads/master");
+		assertEquals("refs/heads/master", git2.getRepository().getFullBranch());
 		assertEquals("refs/heads/master", allRefNames(git2.branchList()
 				.setListMode(ListMode.ALL).call()));
 		RemoteConfig cfg = new RemoteConfig(git2.getRepository().getConfig(),
@@ -409,11 +401,10 @@
 		command.setURI(fileUri());
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
 		assertTrue(git2.getRepository().isBare());
 		assertNotNull(git2.getRepository().resolve("tag-for-blob"));
 		assertNotNull(git2.getRepository().resolve("tag-initial"));
-		assertEquals(git2.getRepository().getFullBranch(), "refs/heads/master");
+		assertEquals("refs/heads/master", git2.getRepository().getFullBranch());
 		assertEquals("refs/heads/master, refs/heads/test", allRefNames(
 				git2.branchList().setListMode(ListMode.ALL).call()));
 		assertNotNull(git2.getRepository().exactRef("refs/meta/foo/bar"));
@@ -436,7 +427,6 @@
 		command.setURI(fileUri());
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
 		assertNull(git2.getRepository().resolve("tag-for-blob"));
 		assertNull(git2.getRepository().resolve("refs/heads/master"));
 		assertNotNull(git2.getRepository().resolve("tag-initial"));
@@ -464,8 +454,7 @@
 		command.setURI(fileUri());
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
-		assertEquals(git2.getRepository().getFullBranch(), "refs/heads/test");
+		assertEquals("refs/heads/test", git2.getRepository().getFullBranch());
 		// Expect both remote branches to exist; setCloneAllBranches(true)
 		// should override any setBranchesToClone().
 		assertNotNull(
@@ -492,8 +481,7 @@
 		command.setURI(fileUri());
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
-		assertEquals(git2.getRepository().getFullBranch(), "refs/heads/test");
+		assertEquals("refs/heads/test", git2.getRepository().getFullBranch());
 		// Expect only the test branch; allBranches was re-set to false
 		assertNull(git2.getRepository().resolve("refs/remotes/origin/master"));
 		assertNotNull(git2.getRepository().resolve("refs/remotes/origin/test"));
@@ -525,7 +513,6 @@
 		command.setURI(fileUri());
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
 		// clone again
 		command = Git.cloneRepository();
 		command.setDirectory(directory);
@@ -551,7 +538,6 @@
 		clone.setURI(fileUri());
 		Git git2 = clone.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
 
 		assertEquals(Constants.MASTER, git2.getRepository().getBranch());
 	}
@@ -595,7 +581,6 @@
 		clone.setURI(fileUri());
 		Git git2 = clone.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
 
 		assertEquals(Constants.MASTER, git2.getRepository().getBranch());
 		assertTrue(new File(git2.getRepository().getWorkTree(), path
@@ -683,7 +668,6 @@
 		clone.setURI(git.getRepository().getDirectory().toURI().toString());
 		Git git2 = clone.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
 
 		assertEquals(Constants.MASTER, git2.getRepository().getBranch());
 		assertTrue(new File(git2.getRepository().getWorkTree(), path
@@ -813,7 +797,6 @@
 		command.setNoTags();
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
 		assertNotNull(git2.getRepository().resolve("refs/heads/test"));
 		assertNull(git2.getRepository().resolve("tag-initial"));
 		assertNull(git2.getRepository().resolve("tag-for-blob"));
@@ -833,13 +816,41 @@
 		command.setTagOption(TagOpt.FETCH_TAGS);
 		Git git2 = command.call();
 		addRepoToClose(git2.getRepository());
-		assertNotNull(git2);
 		assertNull(git2.getRepository().resolve("refs/heads/test"));
 		assertNotNull(git2.getRepository().resolve("tag-initial"));
 		assertNotNull(git2.getRepository().resolve("tag-for-blob"));
 		assertTagOption(git2.getRepository(), TagOpt.FETCH_TAGS);
 	}
 
+	@Test
+	public void testCloneWithHeadSymRefIsMasterCopy() throws IOException, GitAPIException {
+		// create a branch with the same head as master and switch to it
+		git.checkout().setStartPoint("master").setCreateBranch(true).setName("master-copy").call();
+
+		// when we clone the HEAD symref->master-copy means we start on master-copy and not master
+		File directory = createTempDirectory("testCloneRepositorySymRef_master-copy");
+		CloneCommand command = Git.cloneRepository();
+		command.setDirectory(directory);
+		command.setURI(fileUri());
+		Git git2 = command.call();
+		addRepoToClose(git2.getRepository());
+		assertEquals("refs/heads/master-copy", git2.getRepository().getFullBranch());
+	}
+
+	@Test
+	public void testCloneWithHeadSymRefIsNonMasterCopy() throws IOException, GitAPIException {
+		// create a branch with the same head as test and switch to it
+		git.checkout().setStartPoint("test").setCreateBranch(true).setName("test-copy").call();
+
+		File directory = createTempDirectory("testCloneRepositorySymRef_test-copy");
+		CloneCommand command = Git.cloneRepository();
+		command.setDirectory(directory);
+		command.setURI(fileUri());
+		Git git2 = command.call();
+		addRepoToClose(git2.getRepository());
+		assertEquals("refs/heads/test-copy", git2.getRepository().getFullBranch());
+	}
+
 	private void assertTagOption(Repository repo, TagOpt expectedTagOption)
 			throws URISyntaxException {
 		RemoteConfig remoteConfig = new RemoteConfig(
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java
index 00f84e9..12ec2aa 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java
@@ -11,9 +11,11 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 import java.io.File;
 import java.util.Collection;
+import java.util.Optional;
 
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.Constants;
@@ -34,7 +36,7 @@
 		git.add().addFilepattern("Test.txt").call();
 		git.commit().setMessage("Initial commit").call();
 
-		// create a master branch and switch to it
+		// create a test branch and switch to it
 		git.branchCreate().setName("test").call();
 		RefUpdate rup = db.updateRef(Constants.HEAD);
 		rup.link("refs/heads/test");
@@ -104,6 +106,28 @@
 		assertEquals(2, refs.size());
 	}
 
+	@Test
+	public void testLsRemoteWithSymRefs() throws Exception {
+		File directory = createTempDirectory("testRepository");
+		CloneCommand command = Git.cloneRepository();
+		command.setDirectory(directory);
+		command.setURI(fileUri());
+		command.setCloneAllBranches(true);
+		Git git2 = command.call();
+		addRepoToClose(git2.getRepository());
+
+
+		LsRemoteCommand lsRemoteCommand = git2.lsRemote();
+		Collection<Ref> refs = lsRemoteCommand.call();
+		assertNotNull(refs);
+		assertEquals(6, refs.size());
+
+		Optional<Ref> headRef = refs.stream().filter(ref -> ref.getName().equals(Constants.HEAD)).findFirst();
+		assertTrue("expected a HEAD Ref", headRef.isPresent());
+		assertTrue("expected HEAD Ref to be a Symbolic", headRef.get().isSymbolic());
+		assertEquals("refs/heads/test", headRef.get().getTarget().getName());
+	}
+
 	private String fileUri() {
 		return "file://" + git.getRepository().getWorkTree().getAbsolutePath();
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java
new file mode 100644
index 0000000..64b16f6
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2020, Lee Worrall 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.transport;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasKey;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.junit.Test;
+
+public class BasePackConnectionTest {
+
+	@Test
+	public void testExtractSymRefsFromCapabilities() {
+		final Map<String, String> symRefs = BasePackConnection
+				.extractSymRefsFromCapabilities(
+						Arrays.asList("symref=HEAD:refs/heads/main",
+								"symref=refs/heads/sym:refs/heads/other"));
+
+		assertEquals(2, symRefs.size());
+		assertEquals("refs/heads/main", symRefs.get("HEAD"));
+		assertEquals("refs/heads/other", symRefs.get("refs/heads/sym"));
+	}
+
+	@Test
+	public void testUpdateWithSymRefsAdds() {
+		final Ref mainRef = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE,
+				"refs/heads/main", ObjectId.fromString(
+						"0000000000000000000000000000000000000001"));
+
+		final Map<String, Ref> refMap = new HashMap<>();
+		refMap.put(mainRef.getName(), mainRef);
+		refMap.put("refs/heads/other",
+				new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/other",
+						ObjectId.fromString(
+								"0000000000000000000000000000000000000002")));
+
+		final Map<String, String> symRefs = new HashMap<>();
+		symRefs.put("HEAD", "refs/heads/main");
+
+		BasePackConnection.updateWithSymRefs(refMap, symRefs);
+
+		assertThat(refMap, hasKey("HEAD"));
+		final Ref headRef = refMap.get("HEAD");
+		assertThat(headRef, instanceOf(SymbolicRef.class));
+		final SymbolicRef headSymRef = (SymbolicRef) headRef;
+		assertEquals("HEAD", headSymRef.getName());
+		assertSame(mainRef, headSymRef.getTarget());
+	}
+
+	@Test
+	public void testUpdateWithSymRefsReplaces() {
+		final Ref mainRef = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE,
+				"refs/heads/main", ObjectId.fromString(
+						"0000000000000000000000000000000000000001"));
+
+		final Map<String, Ref> refMap = new HashMap<>();
+		refMap.put(mainRef.getName(), mainRef);
+		refMap.put("HEAD", new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "HEAD",
+				mainRef.getObjectId()));
+		refMap.put("refs/heads/other",
+				new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/other",
+						ObjectId.fromString(
+								"0000000000000000000000000000000000000002")));
+
+		final Map<String, String> symRefs = new HashMap<>();
+		symRefs.put("HEAD", "refs/heads/main");
+
+		BasePackConnection.updateWithSymRefs(refMap, symRefs);
+
+		assertThat(refMap, hasKey("HEAD"));
+		final Ref headRef = refMap.get("HEAD");
+		assertThat(headRef, instanceOf(SymbolicRef.class));
+		final SymbolicRef headSymRef = (SymbolicRef) headRef;
+		assertEquals("HEAD", headSymRef.getName());
+		assertSame(mainRef, headSymRef.getTarget());
+	}
+
+	@Test
+	public void testUpdateWithSymRefsWithIndirectsAdds() {
+		final Ref mainRef = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE,
+				"refs/heads/main", ObjectId.fromString(
+						"0000000000000000000000000000000000000001"));
+
+		final Map<String, Ref> refMap = new HashMap<>();
+		refMap.put(mainRef.getName(), mainRef);
+		refMap.put("refs/heads/other",
+				new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/other",
+						ObjectId.fromString(
+								"0000000000000000000000000000000000000002")));
+
+		final Map<String, String> symRefs = new LinkedHashMap<>(); // Ordered
+		symRefs.put("refs/heads/sym3", "refs/heads/sym2"); // Forward reference
+		symRefs.put("refs/heads/sym1", "refs/heads/main");
+		symRefs.put("refs/heads/sym2", "refs/heads/sym1"); // Backward reference
+
+		BasePackConnection.updateWithSymRefs(refMap, symRefs);
+
+		assertThat(refMap, hasKey("refs/heads/sym1"));
+		final Ref sym1Ref = refMap.get("refs/heads/sym1");
+		assertThat(sym1Ref, instanceOf(SymbolicRef.class));
+		final SymbolicRef sym1SymRef = (SymbolicRef) sym1Ref;
+		assertEquals("refs/heads/sym1", sym1SymRef.getName());
+		assertSame(mainRef, sym1SymRef.getTarget());
+
+		assertThat(refMap, hasKey("refs/heads/sym2"));
+		final Ref sym2Ref = refMap.get("refs/heads/sym2");
+		assertThat(sym2Ref, instanceOf(SymbolicRef.class));
+		final SymbolicRef sym2SymRef = (SymbolicRef) sym2Ref;
+		assertEquals("refs/heads/sym2", sym2SymRef.getName());
+		assertSame(sym1SymRef, sym2SymRef.getTarget());
+
+		assertThat(refMap, hasKey("refs/heads/sym3"));
+		final Ref sym3Ref = refMap.get("refs/heads/sym3");
+		assertThat(sym3Ref, instanceOf(SymbolicRef.class));
+		final SymbolicRef sym3SymRef = (SymbolicRef) sym3Ref;
+		assertEquals("refs/heads/sym3", sym3SymRef.getName());
+		assertSame(sym2SymRef, sym3SymRef.getTarget());
+	}
+
+	@Test
+	public void testUpdateWithSymRefsWithIndirectsReplaces() {
+		final Ref mainRef = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE,
+				"refs/heads/main", ObjectId.fromString(
+						"0000000000000000000000000000000000000001"));
+
+		final Map<String, Ref> refMap = new HashMap<>();
+		refMap.put(mainRef.getName(), mainRef);
+		refMap.put("refs/heads/sym1", new ObjectIdRef.Unpeeled(
+				Ref.Storage.LOOSE, "refs/heads/sym1", mainRef.getObjectId()));
+		refMap.put("refs/heads/sym2", new ObjectIdRef.Unpeeled(
+				Ref.Storage.LOOSE, "refs/heads/sym2", mainRef.getObjectId()));
+		refMap.put("refs/heads/sym3", new ObjectIdRef.Unpeeled(
+				Ref.Storage.LOOSE, "refs/heads/sym3", mainRef.getObjectId()));
+		refMap.put("refs/heads/other",
+				new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/other",
+						ObjectId.fromString(
+								"0000000000000000000000000000000000000002")));
+
+		final Map<String, String> symRefs = new LinkedHashMap<>(); // Ordered
+		symRefs.put("refs/heads/sym3", "refs/heads/sym2"); // Forward reference
+		symRefs.put("refs/heads/sym1", "refs/heads/main");
+		symRefs.put("refs/heads/sym2", "refs/heads/sym1"); // Backward reference
+
+		BasePackConnection.updateWithSymRefs(refMap, symRefs);
+
+		assertThat(refMap, hasKey("refs/heads/sym1"));
+		final Ref sym1Ref = refMap.get("refs/heads/sym1");
+		assertThat(sym1Ref, instanceOf(SymbolicRef.class));
+		final SymbolicRef sym1SymRef = (SymbolicRef) sym1Ref;
+		assertEquals("refs/heads/sym1", sym1SymRef.getName());
+		assertSame(mainRef, sym1SymRef.getTarget());
+
+		assertThat(refMap, hasKey("refs/heads/sym2"));
+		final Ref sym2Ref = refMap.get("refs/heads/sym2");
+		assertThat(sym2Ref, instanceOf(SymbolicRef.class));
+		final SymbolicRef sym2SymRef = (SymbolicRef) sym2Ref;
+		assertEquals("refs/heads/sym2", sym2SymRef.getName());
+		assertSame(sym1SymRef, sym2SymRef.getTarget());
+
+		assertThat(refMap, hasKey("refs/heads/sym3"));
+		final Ref sym3Ref = refMap.get("refs/heads/sym3");
+		assertThat(sym3Ref, instanceOf(SymbolicRef.class));
+		final SymbolicRef sym3SymRef = (SymbolicRef) sym3Ref;
+		assertEquals("refs/heads/sym3", sym3SymRef.getName());
+		assertSame(sym2SymRef, sym3SymRef.getTarget());
+	}
+
+	@Test
+	public void testUpdateWithSymRefsIgnoresSelfReference() {
+		final Ref mainRef = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE,
+				"refs/heads/main", ObjectId.fromString(
+						"0000000000000000000000000000000000000001"));
+
+		final Map<String, Ref> refMap = new HashMap<>();
+		refMap.put(mainRef.getName(), mainRef);
+		refMap.put("refs/heads/other",
+				new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/other",
+						ObjectId.fromString(
+								"0000000000000000000000000000000000000002")));
+
+		final Map<String, String> symRefs = new LinkedHashMap<>();
+		symRefs.put("refs/heads/sym1", "refs/heads/sym1");
+
+		BasePackConnection.updateWithSymRefs(refMap, symRefs);
+
+		assertEquals(2, refMap.size());
+		assertThat(refMap, not(hasKey("refs/heads/sym1")));
+	}
+
+	@Test
+	public void testUpdateWithSymRefsIgnoreCircularReference() {
+		final Ref mainRef = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE,
+				"refs/heads/main", ObjectId.fromString(
+						"0000000000000000000000000000000000000001"));
+
+		final Map<String, Ref> refMap = new HashMap<>();
+		refMap.put(mainRef.getName(), mainRef);
+		refMap.put("refs/heads/other",
+				new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/other",
+						ObjectId.fromString(
+								"0000000000000000000000000000000000000002")));
+
+		final Map<String, String> symRefs = new LinkedHashMap<>();
+		symRefs.put("refs/heads/sym2", "refs/heads/sym1");
+		symRefs.put("refs/heads/sym1", "refs/heads/sym2");
+
+		BasePackConnection.updateWithSymRefs(refMap, symRefs);
+
+		assertEquals(2, refMap.size());
+		assertThat(refMap, not(hasKey("refs/heads/sym1")));
+		assertThat(refMap, not(hasKey("refs/heads/sym2")));
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
index 30d7f9a..aba86fc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
@@ -413,6 +413,10 @@
 			return null;
 		}
 
+		if (idHEAD != null && idHEAD.isSymbolic()) {
+			return idHEAD.getTarget();
+		}
+
 		Ref master = result.getAdvertisedRef(Constants.R_HEADS
 				+ Constants.MASTER);
 		ObjectId objectId = master != null ? master.getObjectId() : null;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
index 1417fae..3a36398 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
@@ -21,8 +21,11 @@
 import java.io.OutputStream;
 import java.text.MessageFormat;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.Map;
 import java.util.Set;
 
 import org.eclipse.jgit.errors.InvalidObjectIdException;
@@ -35,6 +38,7 @@
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SymbolicRef;
 import org.eclipse.jgit.util.io.InterruptTimer;
 import org.eclipse.jgit.util.io.TimeoutInputStream;
 import org.eclipse.jgit.util.io.TimeoutOutputStream;
@@ -49,6 +53,8 @@
  */
 abstract class BasePackConnection extends BaseConnection {
 
+	protected static final String CAPABILITY_SYMREF_PREFIX = "symref="; //$NON-NLS-1$
+
 	/** The repository this transport fetches into, or pushes out of. */
 	protected final Repository local;
 
@@ -228,10 +234,109 @@
 					throw duplicateAdvertisement(name);
 			}
 		}
+		updateWithSymRefs(avail, extractSymRefsFromCapabilities(remoteCapablities));
 		available(avail);
 	}
 
 	/**
+	 * Finds values in the given capabilities of the form:
+	 *
+	 * <pre>
+	 * symref=<em>source</em>:<em>target</em>
+	 * </pre>
+	 *
+	 * And returns a Map of source->target entries.
+	 *
+	 * @param capabilities
+	 *            the capabilities lines
+	 * @return a Map of the symref entries from capabilities
+	 * @throws NullPointerException
+	 *             if capabilities, or any entry in it, is null
+	 */
+	static Map<String, String> extractSymRefsFromCapabilities(Collection<String> capabilities) {
+		final Map<String, String> symRefs = new LinkedHashMap<>();
+		for (String option : capabilities) {
+			if (option.startsWith(CAPABILITY_SYMREF_PREFIX)) {
+				String[] symRef = option
+						.substring(CAPABILITY_SYMREF_PREFIX.length())
+						.split(":", 2); //$NON-NLS-1$
+				if (symRef.length == 2) {
+					symRefs.put(symRef[0], symRef[1]);
+				}
+			}
+		}
+		return symRefs;
+	}
+
+	/**
+	 * Updates the given refMap with {@link SymbolicRef}s defined by the given
+	 * symRefs.
+	 * <p>
+	 * For each entry, symRef, in symRefs, whose value is a key in refMap, adds
+	 * a new entry to refMap with that same key and value of a new
+	 * {@link SymbolicRef} with source=symRef.key and
+	 * target=refMap.get(symRef.value), then removes that entry from symRefs.
+	 * <p>
+	 * If refMap already contains an entry for symRef.key, it is replaced.
+	 * </p>
+	 * </p>
+	 * <p>
+	 * For example, given:
+	 * </p>
+	 *
+	 * <pre>
+	 * refMap.put("refs/heads/main", ref);
+	 * symRefs.put("HEAD", "refs/heads/main");
+	 * </pre>
+	 *
+	 * then:
+	 *
+	 * <pre>
+	 * updateWithSymRefs(refMap, symRefs);
+	 * </pre>
+	 *
+	 * has the <em>effect</em> of:
+	 *
+	 * <pre>
+	 * refMap.put("HEAD",
+	 * 		new SymbolicRef("HEAD", refMap.get(symRefs.remove("HEAD"))))
+	 * </pre>
+	 * <p>
+	 * Any entry in symRefs whose value is not a key in refMap is ignored. Any
+	 * circular symRefs are ignored.
+	 * </p>
+	 * <p>
+	 * Upon completion, symRefs will contain only any unresolvable entries.
+	 * </p>
+	 *
+	 * @param refMap
+	 *            a non-null, modifiable, Map to update, and the provider of
+	 *            symref targets.
+	 * @param symRefs
+	 *            a non-null, modifiable, Map of symrefs.
+	 * @throws NullPointerException
+	 *             if refMap or symRefs is null
+	 */
+	static void updateWithSymRefs(Map<String, Ref> refMap, Map<String, String> symRefs) {
+		boolean haveNewRefMapEntries = !refMap.isEmpty();
+		while (!symRefs.isEmpty() && haveNewRefMapEntries) {
+			haveNewRefMapEntries = false;
+			final Iterator<Map.Entry<String, String>> iterator = symRefs.entrySet().iterator();
+			while (iterator.hasNext()) {
+				final Map.Entry<String, String> symRef = iterator.next();
+				if (!symRefs.containsKey(symRef.getValue())) { // defer forward reference
+					final Ref r = refMap.get(symRef.getValue());
+					if (r != null) {
+						refMap.put(symRef.getKey(), new SymbolicRef(symRef.getKey(), r));
+						haveNewRefMapEntries = true;
+						iterator.remove();
+					}
+				}
+			}
+		}
+	}
+
+	/**
 	 * Create an exception to indicate problems finding a remote repository. The
 	 * caller is expected to throw the returned exception.
 	 *