A SymbolicRef is added to the advertised refs for any symref in capabilities whose target is an advertised ref; this may replace an existing entry, such as HEAD. When cloning, if any advertised HEAD is symbolic then use the target rather than looking for an advertised ref with a matching objectId. Add --symref option to LsRemote command. Bug: 514052 Change-Id: Idfb48e6f6e8dcfe57a6896883fe6d84d533aa9d0 Signed-off-by: Lee Worrall <worrall.la@gmail.com>tags/v5.10.0.202012021225-rc1
@@ -33,7 +33,7 @@ public class LsRemoteTest extends CLIRepositoryTestCase { | |||
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 @@ public class LsRemoteTest extends CLIRepositoryTestCase { | |||
"" }, 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()); | |||
} | |||
} |
@@ -256,6 +256,7 @@ usage_LsFiles=Show information about files in the index and the working tree | |||
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. |
@@ -34,6 +34,9 @@ class LsRemote extends TextBuiltin { | |||
@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 @@ class LsRemote extends TextBuiltin { | |||
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 @@ class LsRemote extends TextBuiltin { | |||
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(); | |||
} | |||
} |
@@ -92,7 +92,6 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
Git git2 = command.call(); | |||
addRepoToClose(git2.getRepository()); | |||
assertNotNull(git2); | |||
assertEquals("refs/heads/test", git2.getRepository().getFullBranch()); | |||
} | |||
@@ -338,7 +333,6 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
command.setURI(fileUri()); | |||
Git git2 = command.call(); | |||
addRepoToClose(git2.getRepository()); | |||
assertNotNull(git2); | |||
// clone again | |||
command = Git.cloneRepository(); | |||
command.setDirectory(directory); | |||
@@ -551,7 +538,6 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
clone.setURI(fileUri()); | |||
Git git2 = clone.call(); | |||
addRepoToClose(git2.getRepository()); | |||
assertNotNull(git2); | |||
assertEquals(Constants.MASTER, git2.getRepository().getBranch()); | |||
} | |||
@@ -595,7 +581,6 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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 @@ public class CloneCommandTest extends RepositoryTestCase { | |||
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( |
@@ -11,9 +11,11 @@ package org.eclipse.jgit.api; | |||
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 @@ public class LsRemoteCommandTest extends RepositoryTestCase { | |||
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 @@ public class LsRemoteCommandTest extends RepositoryTestCase { | |||
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(); | |||
} |
@@ -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"))); | |||
} | |||
} |
@@ -413,6 +413,10 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { | |||
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; |
@@ -21,8 +21,11 @@ import java.io.InputStream; | |||
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.ObjectId; | |||
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 @@ import org.eclipse.jgit.util.io.TimeoutOutputStream; | |||
*/ | |||
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,9 +234,108 @@ abstract class BasePackConnection extends BaseConnection { | |||
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. |