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 4ecaeb604..46eec7436 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 @@ public void setUp() throws Exception { 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 void testLsRemoteHeadsTags() throws Exception { "" }, result.toArray()); } + @Test + public void testLsRemoteSymRefs() throws Exception { + final List 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 c116437c6..6112a272e 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_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. 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 36812c03a..055b48a15 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 @@ 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 @@ protected void run() { 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 @@ private void show(AnyObjectId id, String name) 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 b737bbec0..de25870bd 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 @@ public void testCloneRepository() throws IOException, 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 void testCloneRepositoryWithBranch() throws IOException, 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 void testCloneRepositoryWithBranch() throws IOException, 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 void testCloneRepositoryWithBranch() throws IOException, 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 void testCloneRepositoryWithBranchShortName() throws Exception { Git git2 = command.call(); addRepoToClose(git2.getRepository()); - assertNotNull(git2); assertEquals("refs/heads/test", git2.getRepository().getFullBranch()); } @@ -338,7 +333,6 @@ public void testCloneRepositoryWithTagName() throws Exception { 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 void testCloneRepositoryOnlyOneBranch() throws Exception { 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 void testBareCloneRepositoryOnlyOneBranch() throws Exception { 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 void testBareCloneRepositoryMirror() throws Exception { 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 void testCloneRepositoryOnlyOneTag() throws Exception { 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 void testCloneRepositoryAllBranchesTakesPreference() 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 void testCloneRepositoryAllBranchesIndependent() throws Exception { 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 void testCloneRepositoryWhenDestinationDirectoryExistsAndIsNotEmpty() 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 void testCloneRepositoryWithMultipleHeadBranches() throws Exception { clone.setURI(fileUri()); Git git2 = clone.call(); addRepoToClose(git2.getRepository()); - assertNotNull(git2); assertEquals(Constants.MASTER, git2.getRepository().getBranch()); } @@ -595,7 +581,6 @@ public void testCloneRepositoryWithSubmodules() throws Exception { 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 void testCloneRepositoryWithNestedSubmodules() throws Exception { 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 void testCloneNoTags() throws IOException, JGitInternalException, 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 void testCloneFollowTags() throws IOException, JGitInternalException, 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 00f84e9ae..12ec2aae5 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 @@ public void setUp() throws Exception { 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 void testLsRemoteWithoutLocalRepository() throws Exception { 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 refs = lsRemoteCommand.call(); + assertNotNull(refs); + assertEquals(6, refs.size()); + + Optional 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 000000000..64b16f659 --- /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 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 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 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 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 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 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 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 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 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 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 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 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 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 30d7f9adc..aba86fc36 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 @@ private Ref findBranchToCheckout(FetchResult result) { 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 1417faee8..3a3639862 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,9 +234,108 @@ private void readAdvertisedRefsImpl() throws IOException { throw duplicateAdvertisement(name); } } + updateWithSymRefs(avail, extractSymRefsFromCapabilities(remoteCapablities)); available(avail); } + /** + * Finds values in the given capabilities of the form: + * + *
+	 * symref=source:target
+	 * 
+ * + * 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 extractSymRefsFromCapabilities(Collection capabilities) { + final Map 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. + *

+ * 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. + *

+ * If refMap already contains an entry for symRef.key, it is replaced. + *

+ *

+ *

+ * For example, given: + *

+ * + *
+	 * refMap.put("refs/heads/main", ref);
+	 * symRefs.put("HEAD", "refs/heads/main");
+	 * 
+ * + * then: + * + *
+	 * updateWithSymRefs(refMap, symRefs);
+	 * 
+ * + * has the effect of: + * + *
+	 * refMap.put("HEAD",
+	 * 		new SymbolicRef("HEAD", refMap.get(symRefs.remove("HEAD"))))
+	 * 
+ *

+ * Any entry in symRefs whose value is not a key in refMap is ignored. Any + * circular symRefs are ignored. + *

+ *

+ * Upon completion, symRefs will contain only any unresolvable entries. + *

+ * + * @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 refMap, Map symRefs) { + boolean haveNewRefMapEntries = !refMap.isEmpty(); + while (!symRefs.isEmpty() && haveNewRefMapEntries) { + haveNewRefMapEntries = false; + final Iterator> iterator = symRefs.entrySet().iterator(); + while (iterator.hasNext()) { + final Map.Entry 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.