diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/InfoRefsServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/InfoRefsServlet.java index 7bad51717..b766196fd 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/InfoRefsServlet.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/InfoRefsServlet.java @@ -47,7 +47,6 @@ import static org.eclipse.jgit.http.server.ServletUtils.send; import java.io.IOException; -import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServlet; @@ -98,9 +97,9 @@ protected void end() { adv.init(walk, ADVERTISED); adv.setDerefTags(true); - Map refs = new HashMap(db.getAllRefs()); + Map refs = db.getAllRefs(); refs.remove(Constants.HEAD); - adv.send(refs.values()); + adv.send(refs); return out.toString().getBytes(Constants.CHARACTER_ENCODING); } } diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java index 60dbe27ac..7a1dd1604 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java @@ -50,18 +50,19 @@ import java.util.Map; import java.util.Map.Entry; -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.ExampleMode; -import org.kohsuke.args4j.Option; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefComparator; +import org.eclipse.jgit.lib.RefRename; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.pgm.opt.CmdLineParser; import org.eclipse.jgit.revwalk.RevWalk; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.ExampleMode; +import org.kohsuke.args4j.Option; @Command(common = true, usage = "List, create, or delete branches") class Branch extends TextBuiltin { @@ -81,6 +82,9 @@ class Branch extends TextBuiltin { @Option(name = "--create-force", aliases = { "-f" }, usage = "force create branch even exists") private boolean createForce = false; + @Option(name = "-m", usage = "move/rename a branch") + private boolean rename = false; + @Option(name = "--verbose", aliases = { "-v" }, usage = "be verbose") private boolean verbose = false; @@ -102,7 +106,36 @@ protected void run() throws Exception { if (branches.size() > 2) throw die("Too many refs given\n" + new CmdLineParser(this).printExample(ExampleMode.ALL)); - if (branches.size() > 0) { + if (rename) { + String src, dst; + if (branches.size() == 1) { + final Ref head = db.getRef(Constants.HEAD); + if (head != null && head.isSymbolic()) + src = head.getLeaf().getName(); + else + throw die("Cannot rename detached HEAD"); + dst = branches.get(0); + } else { + src = branches.get(0); + final Ref old = db.getRef(src); + if (old == null) + throw die(String.format("%s does not exist", src)); + if (!old.getName().startsWith(Constants.R_HEADS)) + throw die(String.format("%s is not a branch", src)); + src = old.getName(); + dst = branches.get(1); + } + + if (!dst.startsWith(Constants.R_HEADS)) + dst = Constants.R_HEADS + dst; + if (!Repository.isValidRefName(dst)) + throw die(String.format("%s is not a valid ref name", dst)); + + RefRename r = db.renameRef(src, dst); + if (r.rename() != Result.RENAMED) + throw die(String.format("%s cannot be renamed", src)); + + } else if (branches.size() > 0) { String newHead = branches.get(0); String startBranch; if (branches.size() == 2) @@ -143,7 +176,7 @@ private void list() throws Exception { Ref head = refs.get(Constants.HEAD); // This can happen if HEAD is stillborn if (head != null) { - String current = head.getName(); + String current = head.getLeaf().getName(); if (current.equals(Constants.HEAD)) addRef("(no branch)", head); addRefs(refs, Constants.R_HEADS, !remote); diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java index 10bb357e2..a6c50ff19 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008-2010, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -164,8 +164,11 @@ private Ref guessHEAD(final FetchResult result) { private void doCheckout(final Ref branch) throws IOException { if (branch == null) throw die("cannot checkout; no HEAD advertised by remote"); - if (!Constants.HEAD.equals(branch.getName())) - db.writeSymref(Constants.HEAD, branch.getName()); + if (!Constants.HEAD.equals(branch.getName())) { + RefUpdate u = db.updateRef(Constants.HEAD); + u.disableRefLog(); + u.link(branch.getName()); + } final Commit commit = db.mapCommit(branch.getObjectId()); final RefUpdate u = db.updateRef(Constants.HEAD); diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java index ecaf19bd1..4b5975669 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2010, Google Inc. * Copyright (C) 2006-2008, Robin Rosenberg * Copyright (C) 2008, Shawn O. Pearce * and other copyright owners as documented in the project's IP log. @@ -92,7 +93,7 @@ protected void show(final RevCommit c) throws Exception { if (list != null) { out.print(" ("); for (Iterator i = list.iterator(); i.hasNext(); ) { - out.print(i.next().getOrigName()); + out.print(i.next().getName()); if (i.hasNext()) out.print(" "); } diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/ShowRef.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/ShowRef.java index 7dbb21c5d..d34f373db 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/ShowRef.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/ShowRef.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2010, Google Inc. * Copyright (C) 2008, Jonas Fonseca * Copyright (C) 2008, Shawn O. Pearce * and other copyright owners as documented in the project's IP log. @@ -44,21 +45,32 @@ package org.eclipse.jgit.pgm; -import java.util.TreeMap; +import java.util.Map; +import java.util.SortedMap; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefComparator; +import org.eclipse.jgit.util.RefMap; class ShowRef extends TextBuiltin { @Override protected void run() throws Exception { - for (final Ref r : new TreeMap(db.getAllRefs()).values()) { + for (final Ref r : getSortedRefs()) { show(r.getObjectId(), r.getName()); if (r.getPeeledObjectId() != null) show(r.getPeeledObjectId(), r.getName() + "^{}"); } } + private Iterable getSortedRefs() { + Map all = db.getAllRefs(); + if (all instanceof RefMap + || (all instanceof SortedMap && ((SortedMap) all).comparator() == null)) + return all.values(); + return RefComparator.sort(all.values()); + } + private void show(final AnyObjectId id, final String name) { out.print(id.name()); out.print('\t'); diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildCommitGraph.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildCommitGraph.java index 50b889849..0a5f2a0c9 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildCommitGraph.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildCommitGraph.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009, Google Inc. + * Copyright (C) 2009-2010, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -63,6 +63,7 @@ import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.LockFile; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ObjectWriter; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.ProgressMonitor; @@ -303,7 +304,8 @@ private Map computeNewRefs() throws IOException { } throw new MissingObjectException(id, type); } - refs.put(name, new Ref(Ref.Storage.PACKED, name, id)); + refs.put(name, new ObjectIdRef.Unpeeled(Ref.Storage.PACKED, + name, id)); } } finally { br.close(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdRefTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdRefTest.java new file mode 100644 index 000000000..1774a428d --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdRefTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.lib; + +import junit.framework.TestCase; + +public class ObjectIdRefTest extends TestCase { + private static final ObjectId ID_A = ObjectId + .fromString("41eb0d88f833b558bddeb269b7ab77399cdf98ed"); + + private static final ObjectId ID_B = ObjectId + .fromString("698dd0b8d0c299f080559a1cffc7fe029479a408"); + + private static final String name = "refs/heads/a.test.ref"; + + public void testConstructor_PeeledStatusNotKnown() { + ObjectIdRef r; + + r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, name, ID_A); + assertSame(Ref.Storage.LOOSE, r.getStorage()); + assertSame(name, r.getName()); + assertSame(ID_A, r.getObjectId()); + assertFalse("not peeled", r.isPeeled()); + assertNull("no peel id", r.getPeeledObjectId()); + assertSame("leaf is this", r, r.getLeaf()); + assertSame("target is this", r, r.getTarget()); + assertFalse("not symbolic", r.isSymbolic()); + + r = new ObjectIdRef.Unpeeled(Ref.Storage.PACKED, name, ID_A); + assertSame(Ref.Storage.PACKED, r.getStorage()); + + r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE_PACKED, name, ID_A); + assertSame(Ref.Storage.LOOSE_PACKED, r.getStorage()); + + r = new ObjectIdRef.Unpeeled(Ref.Storage.NEW, name, null); + assertSame(Ref.Storage.NEW, r.getStorage()); + assertSame(name, r.getName()); + assertNull("no id on new ref", r.getObjectId()); + assertFalse("not peeled", r.isPeeled()); + assertNull("no peel id", r.getPeeledObjectId()); + assertSame("leaf is this", r, r.getLeaf()); + assertSame("target is this", r, r.getTarget()); + assertFalse("not symbolic", r.isSymbolic()); + } + + public void testConstructor_Peeled() { + ObjectIdRef r; + + r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, name, ID_A); + assertSame(Ref.Storage.LOOSE, r.getStorage()); + assertSame(name, r.getName()); + assertSame(ID_A, r.getObjectId()); + assertFalse("not peeled", r.isPeeled()); + assertNull("no peel id", r.getPeeledObjectId()); + assertSame("leaf is this", r, r.getLeaf()); + assertSame("target is this", r, r.getTarget()); + assertFalse("not symbolic", r.isSymbolic()); + + r = new ObjectIdRef.PeeledNonTag(Ref.Storage.LOOSE, name, ID_A); + assertTrue("is peeled", r.isPeeled()); + assertNull("no peel id", r.getPeeledObjectId()); + + r = new ObjectIdRef.PeeledTag(Ref.Storage.LOOSE, name, ID_A, ID_B); + assertTrue("is peeled", r.isPeeled()); + assertSame(ID_B, r.getPeeledObjectId()); + } + + public void testToString() { + ObjectIdRef r; + + r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, name, ID_A); + assertEquals("Ref[" + name + "=" + ID_A.name() + "]", r.toString()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDirectoryTest.java new file mode 100644 index 000000000..a2812901b --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDirectoryTest.java @@ -0,0 +1,1024 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.lib; + +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.eclipse.jgit.lib.Constants.R_TAGS; +import static org.eclipse.jgit.lib.Ref.Storage.LOOSE; +import static org.eclipse.jgit.lib.Ref.Storage.NEW; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTag; + +public class RefDirectoryTest extends LocalDiskRepositoryTestCase { + private Repository diskRepo; + + private TestRepository repo; + + private RefDirectory refdir; + + private RevCommit A; + + private RevCommit B; + + private RevTag v1_0; + + protected void setUp() throws Exception { + super.setUp(); + + diskRepo = createBareRepository(); + refdir = (RefDirectory) diskRepo.getRefDatabase(); + + repo = new TestRepository(diskRepo); + A = repo.commit().create(); + B = repo.commit(repo.getRevWalk().parseCommit(A)); + v1_0 = repo.tag("v1_0", B); + repo.getRevWalk().parseBody(v1_0); + } + + public void testCreate() throws IOException { + // setUp above created the directory. We just have to test it. + File d = diskRepo.getDirectory(); + assertSame(diskRepo, refdir.getRepository()); + + assertTrue(new File(d, "refs").isDirectory()); + assertTrue(new File(d, "logs").isDirectory()); + assertTrue(new File(d, "logs/refs").isDirectory()); + assertFalse(new File(d, "packed-refs").exists()); + + assertTrue(new File(d, "refs/heads").isDirectory()); + assertTrue(new File(d, "refs/tags").isDirectory()); + assertEquals(2, new File(d, "refs").list().length); + assertEquals(0, new File(d, "refs/heads").list().length); + assertEquals(0, new File(d, "refs/tags").list().length); + + assertTrue(new File(d, "logs/refs/heads").isDirectory()); + assertFalse(new File(d, "logs/HEAD").exists()); + assertEquals(0, new File(d, "logs/refs/heads").list().length); + + assertEquals("ref: refs/heads/master\n", read(new File(d, HEAD))); + } + + public void testGetRefs_EmptyDatabase() throws IOException { + Map all; + + all = refdir.getRefs(RefDatabase.ALL); + assertTrue("no references", all.isEmpty()); + + all = refdir.getRefs(R_HEADS); + assertTrue("no references", all.isEmpty()); + + all = refdir.getRefs(R_TAGS); + assertTrue("no references", all.isEmpty()); + } + + public void testGetRefs_HeadOnOneBranch() throws IOException { + Map all; + Ref head, master; + + writeLooseRef("refs/heads/master", A); + + all = refdir.getRefs(RefDatabase.ALL); + assertEquals(2, all.size()); + assertTrue("has HEAD", all.containsKey(HEAD)); + assertTrue("has master", all.containsKey("refs/heads/master")); + + head = all.get(HEAD); + master = all.get("refs/heads/master"); + + assertEquals(HEAD, head.getName()); + assertTrue(head.isSymbolic()); + assertSame(LOOSE, head.getStorage()); + assertSame("uses same ref as target", master, head.getTarget()); + + assertEquals("refs/heads/master", master.getName()); + assertFalse(master.isSymbolic()); + assertSame(LOOSE, master.getStorage()); + assertEquals(A, master.getObjectId()); + } + + public void testGetRefs_DeatchedHead1() throws IOException { + Map all; + Ref head; + + writeLooseRef(HEAD, A); + BUG_WorkAroundRacyGitIssues(HEAD); + + all = refdir.getRefs(RefDatabase.ALL); + assertEquals(1, all.size()); + assertTrue("has HEAD", all.containsKey(HEAD)); + + head = all.get(HEAD); + + assertEquals(HEAD, head.getName()); + assertFalse(head.isSymbolic()); + assertSame(LOOSE, head.getStorage()); + assertEquals(A, head.getObjectId()); + } + + public void testGetRefs_DeatchedHead2() throws IOException { + Map all; + Ref head, master; + + writeLooseRef(HEAD, A); + writeLooseRef("refs/heads/master", B); + BUG_WorkAroundRacyGitIssues(HEAD); + + all = refdir.getRefs(RefDatabase.ALL); + assertEquals(2, all.size()); + + head = all.get(HEAD); + master = all.get("refs/heads/master"); + + assertEquals(HEAD, head.getName()); + assertFalse(head.isSymbolic()); + assertSame(LOOSE, head.getStorage()); + assertEquals(A, head.getObjectId()); + + assertEquals("refs/heads/master", master.getName()); + assertFalse(master.isSymbolic()); + assertSame(LOOSE, master.getStorage()); + assertEquals(B, master.getObjectId()); + } + + public void testGetRefs_DeeplyNestedBranch() throws IOException { + String name = "refs/heads/a/b/c/d/e/f/g/h/i/j/k"; + Map all; + Ref r; + + writeLooseRef(name, A); + + all = refdir.getRefs(RefDatabase.ALL); + assertEquals(1, all.size()); + + r = all.get(name); + assertEquals(name, r.getName()); + assertFalse(r.isSymbolic()); + assertSame(LOOSE, r.getStorage()); + assertEquals(A, r.getObjectId()); + } + + public void testGetRefs_HeadBranchNotBorn() throws IOException { + Map all; + Ref a, b; + + writeLooseRef("refs/heads/A", A); + writeLooseRef("refs/heads/B", B); + + all = refdir.getRefs(RefDatabase.ALL); + assertEquals(2, all.size()); + assertFalse("no HEAD", all.containsKey(HEAD)); + + a = all.get("refs/heads/A"); + b = all.get("refs/heads/B"); + + assertEquals(A, a.getObjectId()); + assertEquals(B, b.getObjectId()); + + assertEquals("refs/heads/A", a.getName()); + assertEquals("refs/heads/B", b.getName()); + } + + public void testGetRefs_LooseOverridesPacked() throws IOException { + Map heads; + Ref a; + + writeLooseRef("refs/heads/master", B); + writePackedRef("refs/heads/master", A); + + heads = refdir.getRefs(R_HEADS); + assertEquals(1, heads.size()); + + a = heads.get("master"); + assertEquals("refs/heads/master", a.getName()); + assertEquals(B, a.getObjectId()); + } + + public void testGetRefs_IgnoresGarbageRef1() throws IOException { + Map heads; + Ref a; + + writeLooseRef("refs/heads/A", A); + write(new File(diskRepo.getDirectory(), "refs/heads/bad"), "FAIL\n"); + + heads = refdir.getRefs(RefDatabase.ALL); + assertEquals(1, heads.size()); + + a = heads.get("refs/heads/A"); + assertEquals("refs/heads/A", a.getName()); + assertEquals(A, a.getObjectId()); + } + + public void testGetRefs_IgnoresGarbageRef2() throws IOException { + Map heads; + Ref a; + + writeLooseRef("refs/heads/A", A); + write(new File(diskRepo.getDirectory(), "refs/heads/bad"), ""); + + heads = refdir.getRefs(RefDatabase.ALL); + assertEquals(1, heads.size()); + + a = heads.get("refs/heads/A"); + assertEquals("refs/heads/A", a.getName()); + assertEquals(A, a.getObjectId()); + } + + public void testGetRefs_IgnoresGarbageRef3() throws IOException { + Map heads; + Ref a; + + writeLooseRef("refs/heads/A", A); + write(new File(diskRepo.getDirectory(), "refs/heads/bad"), "\n"); + + heads = refdir.getRefs(RefDatabase.ALL); + assertEquals(1, heads.size()); + + a = heads.get("refs/heads/A"); + assertEquals("refs/heads/A", a.getName()); + assertEquals(A, a.getObjectId()); + } + + public void testGetRefs_IgnoresGarbageRef4() throws IOException { + Map heads; + Ref a, b, c; + + writeLooseRef("refs/heads/A", A); + writeLooseRef("refs/heads/B", B); + writeLooseRef("refs/heads/C", A); + heads = refdir.getRefs(RefDatabase.ALL); + assertEquals(3, heads.size()); + assertTrue(heads.containsKey("refs/heads/A")); + assertTrue(heads.containsKey("refs/heads/B")); + assertTrue(heads.containsKey("refs/heads/C")); + + writeLooseRef("refs/heads/B", "FAIL\n"); + BUG_WorkAroundRacyGitIssues("refs/heads/B"); + + heads = refdir.getRefs(RefDatabase.ALL); + assertEquals(2, heads.size()); + + a = heads.get("refs/heads/A"); + b = heads.get("refs/heads/B"); + c = heads.get("refs/heads/C"); + + assertEquals("refs/heads/A", a.getName()); + assertEquals(A, a.getObjectId()); + + assertNull("no refs/heads/B", b); + + assertEquals("refs/heads/C", c.getName()); + assertEquals(A, c.getObjectId()); + } + + public void testGetRefs_InvalidName() throws IOException { + writeLooseRef("refs/heads/A", A); + + assertTrue("empty refs/heads", refdir.getRefs("refs/heads").isEmpty()); + assertTrue("empty objects", refdir.getRefs("objects").isEmpty()); + assertTrue("empty objects/", refdir.getRefs("objects/").isEmpty()); + } + + public void testGetRefs_HeadsOnly_AllLoose() throws IOException { + Map heads; + Ref a, b; + + writeLooseRef("refs/heads/A", A); + writeLooseRef("refs/heads/B", B); + writeLooseRef("refs/tags/v1.0", v1_0); + + heads = refdir.getRefs(R_HEADS); + assertEquals(2, heads.size()); + + a = heads.get("A"); + b = heads.get("B"); + + assertEquals("refs/heads/A", a.getName()); + assertEquals("refs/heads/B", b.getName()); + + assertEquals(A, a.getObjectId()); + assertEquals(B, b.getObjectId()); + } + + public void testGetRefs_HeadsOnly_AllPacked1() throws IOException { + Map heads; + Ref a; + + deleteLooseRef(HEAD); + writePackedRef("refs/heads/A", A); + + heads = refdir.getRefs(R_HEADS); + assertEquals(1, heads.size()); + + a = heads.get("A"); + + assertEquals("refs/heads/A", a.getName()); + assertEquals(A, a.getObjectId()); + } + + public void testGetRefs_HeadsOnly_SymrefToPacked() throws IOException { + Map heads; + Ref master, other; + + writeLooseRef("refs/heads/other", "ref: refs/heads/master\n"); + writePackedRef("refs/heads/master", A); + + heads = refdir.getRefs(R_HEADS); + assertEquals(2, heads.size()); + + master = heads.get("master"); + other = heads.get("other"); + + assertEquals("refs/heads/master", master.getName()); + assertEquals(A, master.getObjectId()); + + assertEquals("refs/heads/other", other.getName()); + assertEquals(A, other.getObjectId()); + assertSame(master, other.getTarget()); + } + + public void testGetRefs_HeadsOnly_Mixed() throws IOException { + Map heads; + Ref a, b; + + writeLooseRef("refs/heads/A", A); + writeLooseRef("refs/heads/B", B); + writePackedRef("refs/tags/v1.0", v1_0); + + heads = refdir.getRefs(R_HEADS); + assertEquals(2, heads.size()); + + a = heads.get("A"); + b = heads.get("B"); + + assertEquals("refs/heads/A", a.getName()); + assertEquals("refs/heads/B", b.getName()); + + assertEquals(A, a.getObjectId()); + assertEquals(B, b.getObjectId()); + } + + public void testGetRefs_TagsOnly_AllLoose() throws IOException { + Map tags; + Ref a; + + writeLooseRef("refs/heads/A", A); + writeLooseRef("refs/tags/v1.0", v1_0); + + tags = refdir.getRefs(R_TAGS); + assertEquals(1, tags.size()); + + a = tags.get("v1.0"); + + assertEquals("refs/tags/v1.0", a.getName()); + assertEquals(v1_0, a.getObjectId()); + } + + public void testGetRefs_TagsOnly_AllPacked() throws IOException { + Map tags; + Ref a; + + deleteLooseRef(HEAD); + writePackedRef("refs/tags/v1.0", v1_0); + + tags = refdir.getRefs(R_TAGS); + assertEquals(1, tags.size()); + + a = tags.get("v1.0"); + + assertEquals("refs/tags/v1.0", a.getName()); + assertEquals(v1_0, a.getObjectId()); + } + + public void testGetRefs_DiscoversNewLoose1() throws IOException { + Map orig, next; + Ref orig_r, next_r; + + writeLooseRef("refs/heads/master", A); + orig = refdir.getRefs(RefDatabase.ALL); + + writeLooseRef("refs/heads/next", B); + next = refdir.getRefs(RefDatabase.ALL); + + assertEquals(2, orig.size()); + assertEquals(3, next.size()); + + assertFalse(orig.containsKey("refs/heads/next")); + assertTrue(next.containsKey("refs/heads/next")); + + orig_r = orig.get("refs/heads/master"); + next_r = next.get("refs/heads/master"); + assertEquals(A, orig_r.getObjectId()); + assertSame("uses cached instance", orig_r, next_r); + assertSame("same HEAD", orig_r, orig.get(HEAD).getTarget()); + assertSame("same HEAD", orig_r, next.get(HEAD).getTarget()); + + next_r = next.get("refs/heads/next"); + assertSame(LOOSE, next_r.getStorage()); + assertEquals(B, next_r.getObjectId()); + } + + public void testGetRefs_DiscoversNewLoose2() throws IOException { + Map orig, next, news; + + writeLooseRef("refs/heads/pu", A); + orig = refdir.getRefs(RefDatabase.ALL); + + writeLooseRef("refs/heads/new/B", B); + news = refdir.getRefs("refs/heads/new/"); + next = refdir.getRefs(RefDatabase.ALL); + + assertEquals(1, orig.size()); + assertEquals(2, next.size()); + assertEquals(1, news.size()); + + assertTrue(orig.containsKey("refs/heads/pu")); + assertTrue(next.containsKey("refs/heads/pu")); + assertFalse(news.containsKey("refs/heads/pu")); + + assertFalse(orig.containsKey("refs/heads/new/B")); + assertTrue(next.containsKey("refs/heads/new/B")); + assertTrue(news.containsKey("B")); + } + + public void testGetRefs_DiscoversModifiedLoose() throws IOException { + Map all; + + writeLooseRef("refs/heads/master", A); + all = refdir.getRefs(RefDatabase.ALL); + assertEquals(A, all.get(HEAD).getObjectId()); + + writeLooseRef("refs/heads/master", B); + BUG_WorkAroundRacyGitIssues("refs/heads/master"); + all = refdir.getRefs(RefDatabase.ALL); + assertEquals(B, all.get(HEAD).getObjectId()); + } + + public void testGetRef_DiscoversModifiedLoose() throws IOException { + Map all; + + writeLooseRef("refs/heads/master", A); + all = refdir.getRefs(RefDatabase.ALL); + assertEquals(A, all.get(HEAD).getObjectId()); + + writeLooseRef("refs/heads/master", B); + BUG_WorkAroundRacyGitIssues("refs/heads/master"); + + Ref master = refdir.getRef("refs/heads/master"); + assertEquals(B, master.getObjectId()); + } + + public void testGetRefs_DiscoversDeletedLoose1() throws IOException { + Map orig, next; + Ref orig_r, next_r; + + writeLooseRef("refs/heads/B", B); + writeLooseRef("refs/heads/master", A); + orig = refdir.getRefs(RefDatabase.ALL); + + deleteLooseRef("refs/heads/B"); + next = refdir.getRefs(RefDatabase.ALL); + + assertEquals(3, orig.size()); + assertEquals(2, next.size()); + + assertTrue(orig.containsKey("refs/heads/B")); + assertFalse(next.containsKey("refs/heads/B")); + + orig_r = orig.get("refs/heads/master"); + next_r = next.get("refs/heads/master"); + assertEquals(A, orig_r.getObjectId()); + assertSame("uses cached instance", orig_r, next_r); + assertSame("same HEAD", orig_r, orig.get(HEAD).getTarget()); + assertSame("same HEAD", orig_r, next.get(HEAD).getTarget()); + + orig_r = orig.get("refs/heads/B"); + assertSame(LOOSE, orig_r.getStorage()); + assertEquals(B, orig_r.getObjectId()); + } + + public void testGetRef_DiscoversDeletedLoose() throws IOException { + Map all; + + writeLooseRef("refs/heads/master", A); + all = refdir.getRefs(RefDatabase.ALL); + assertEquals(A, all.get(HEAD).getObjectId()); + + deleteLooseRef("refs/heads/master"); + assertNull(refdir.getRef("refs/heads/master")); + assertTrue(refdir.getRefs(RefDatabase.ALL).isEmpty()); + } + + public void testGetRefs_DiscoversDeletedLoose2() throws IOException { + Map orig, next; + + writeLooseRef("refs/heads/master", A); + writeLooseRef("refs/heads/pu", B); + orig = refdir.getRefs(RefDatabase.ALL); + + deleteLooseRef("refs/heads/pu"); + next = refdir.getRefs(RefDatabase.ALL); + + assertEquals(3, orig.size()); + assertEquals(2, next.size()); + + assertTrue(orig.containsKey("refs/heads/pu")); + assertFalse(next.containsKey("refs/heads/pu")); + } + + public void testGetRefs_DiscoversDeletedLoose3() throws IOException { + Map orig, next; + + writeLooseRef("refs/heads/master", A); + writeLooseRef("refs/heads/next", B); + writeLooseRef("refs/heads/pu", B); + writeLooseRef("refs/tags/v1.0", v1_0); + orig = refdir.getRefs(RefDatabase.ALL); + + deleteLooseRef("refs/heads/pu"); + deleteLooseRef("refs/heads/next"); + next = refdir.getRefs(RefDatabase.ALL); + + assertEquals(5, orig.size()); + assertEquals(3, next.size()); + + assertTrue(orig.containsKey("refs/heads/pu")); + assertTrue(orig.containsKey("refs/heads/next")); + assertFalse(next.containsKey("refs/heads/pu")); + assertFalse(next.containsKey("refs/heads/next")); + } + + public void testGetRefs_DiscoversDeletedLoose4() throws IOException { + Map orig, next; + Ref orig_r, next_r; + + writeLooseRef("refs/heads/B", B); + writeLooseRef("refs/heads/master", A); + orig = refdir.getRefs(RefDatabase.ALL); + + deleteLooseRef("refs/heads/master"); + next = refdir.getRefs("refs/heads/"); + + assertEquals(3, orig.size()); + assertEquals(1, next.size()); + + assertTrue(orig.containsKey("refs/heads/B")); + assertTrue(orig.containsKey("refs/heads/master")); + assertTrue(next.containsKey("B")); + assertFalse(next.containsKey("master")); + + orig_r = orig.get("refs/heads/B"); + next_r = next.get("B"); + assertEquals(B, orig_r.getObjectId()); + assertSame("uses cached instance", orig_r, next_r); + } + + public void testGetRefs_DiscoversDeletedLoose5() throws IOException { + Map orig, next; + + writeLooseRef("refs/heads/master", A); + writeLooseRef("refs/heads/pu", B); + orig = refdir.getRefs(RefDatabase.ALL); + + deleteLooseRef("refs/heads/pu"); + writeLooseRef("refs/tags/v1.0", v1_0); + next = refdir.getRefs(RefDatabase.ALL); + + assertEquals(3, orig.size()); + assertEquals(3, next.size()); + + assertTrue(orig.containsKey("refs/heads/pu")); + assertFalse(orig.containsKey("refs/tags/v1.0")); + assertFalse(next.containsKey("refs/heads/pu")); + assertTrue(next.containsKey("refs/tags/v1.0")); + } + + public void testGetRefs_SkipsLockFiles() throws IOException { + Map all; + + writeLooseRef("refs/heads/master", A); + writeLooseRef("refs/heads/pu.lock", B); + all = refdir.getRefs(RefDatabase.ALL); + + assertEquals(2, all.size()); + + assertTrue(all.containsKey(HEAD)); + assertTrue(all.containsKey("refs/heads/master")); + assertFalse(all.containsKey("refs/heads/pu.lock")); + } + + public void testGetRefs_CycleInSymbolicRef() throws IOException { + Map all; + Ref r; + + writeLooseRef("refs/1", "ref: refs/2\n"); + writeLooseRef("refs/2", "ref: refs/3\n"); + writeLooseRef("refs/3", "ref: refs/4\n"); + writeLooseRef("refs/4", "ref: refs/5\n"); + writeLooseRef("refs/5", "ref: refs/end\n"); + writeLooseRef("refs/end", A); + + all = refdir.getRefs(RefDatabase.ALL); + r = all.get("refs/1"); + assertNotNull("has 1", r); + + assertEquals("refs/1", r.getName()); + assertEquals(A, r.getObjectId()); + assertTrue(r.isSymbolic()); + + r = r.getTarget(); + assertEquals("refs/2", r.getName()); + assertEquals(A, r.getObjectId()); + assertTrue(r.isSymbolic()); + + r = r.getTarget(); + assertEquals("refs/3", r.getName()); + assertEquals(A, r.getObjectId()); + assertTrue(r.isSymbolic()); + + r = r.getTarget(); + assertEquals("refs/4", r.getName()); + assertEquals(A, r.getObjectId()); + assertTrue(r.isSymbolic()); + + r = r.getTarget(); + assertEquals("refs/5", r.getName()); + assertEquals(A, r.getObjectId()); + assertTrue(r.isSymbolic()); + + r = r.getTarget(); + assertEquals("refs/end", r.getName()); + assertEquals(A, r.getObjectId()); + assertFalse(r.isSymbolic()); + + writeLooseRef("refs/5", "ref: refs/6\n"); + writeLooseRef("refs/6", "ref: refs/end\n"); + BUG_WorkAroundRacyGitIssues("refs/5"); + all = refdir.getRefs(RefDatabase.ALL); + r = all.get("refs/1"); + assertNull("mising 1 due to cycle", r); + } + + public void testGetRefs_PackedNotPeeled_Sorted() throws IOException { + Map all; + + writePackedRefs("" + // + A.name() + " refs/heads/master\n" + // + B.name() + " refs/heads/other\n" + // + v1_0.name() + " refs/tags/v1.0\n"); + all = refdir.getRefs(RefDatabase.ALL); + + assertEquals(4, all.size()); + final Ref head = all.get(HEAD); + final Ref master = all.get("refs/heads/master"); + final Ref other = all.get("refs/heads/other"); + final Ref tag = all.get("refs/tags/v1.0"); + + assertEquals(A, master.getObjectId()); + assertFalse(master.isPeeled()); + assertNull(master.getPeeledObjectId()); + + assertEquals(B, other.getObjectId()); + assertFalse(other.isPeeled()); + assertNull(other.getPeeledObjectId()); + + assertSame(master, head.getTarget()); + assertEquals(A, head.getObjectId()); + assertFalse(head.isPeeled()); + assertNull(head.getPeeledObjectId()); + + assertEquals(v1_0, tag.getObjectId()); + assertFalse(tag.isPeeled()); + assertNull(tag.getPeeledObjectId()); + } + + public void testGetRef_PackedNotPeeled_WrongSort() throws IOException { + writePackedRefs("" + // + v1_0.name() + " refs/tags/v1.0\n" + // + B.name() + " refs/heads/other\n" + // + A.name() + " refs/heads/master\n"); + + final Ref head = refdir.getRef(HEAD); + final Ref master = refdir.getRef("refs/heads/master"); + final Ref other = refdir.getRef("refs/heads/other"); + final Ref tag = refdir.getRef("refs/tags/v1.0"); + + assertEquals(A, master.getObjectId()); + assertFalse(master.isPeeled()); + assertNull(master.getPeeledObjectId()); + + assertEquals(B, other.getObjectId()); + assertFalse(other.isPeeled()); + assertNull(other.getPeeledObjectId()); + + assertSame(master, head.getTarget()); + assertEquals(A, head.getObjectId()); + assertFalse(head.isPeeled()); + assertNull(head.getPeeledObjectId()); + + assertEquals(v1_0, tag.getObjectId()); + assertFalse(tag.isPeeled()); + assertNull(tag.getPeeledObjectId()); + } + + public void testGetRefs_PackedWithPeeled() throws IOException { + Map all; + + writePackedRefs("# pack-refs with: peeled \n" + // + A.name() + " refs/heads/master\n" + // + B.name() + " refs/heads/other\n" + // + v1_0.name() + " refs/tags/v1.0\n" + // + "^" + v1_0.getObject().name() + "\n"); + all = refdir.getRefs(RefDatabase.ALL); + + assertEquals(4, all.size()); + final Ref head = all.get(HEAD); + final Ref master = all.get("refs/heads/master"); + final Ref other = all.get("refs/heads/other"); + final Ref tag = all.get("refs/tags/v1.0"); + + assertEquals(A, master.getObjectId()); + assertTrue(master.isPeeled()); + assertNull(master.getPeeledObjectId()); + + assertEquals(B, other.getObjectId()); + assertTrue(other.isPeeled()); + assertNull(other.getPeeledObjectId()); + + assertSame(master, head.getTarget()); + assertEquals(A, head.getObjectId()); + assertTrue(head.isPeeled()); + assertNull(head.getPeeledObjectId()); + + assertEquals(v1_0, tag.getObjectId()); + assertTrue(tag.isPeeled()); + assertEquals(v1_0.getObject(), tag.getPeeledObjectId()); + } + + public void testGetRef_EmptyDatabase() throws IOException { + Ref r; + + r = refdir.getRef(HEAD); + assertTrue(r.isSymbolic()); + assertSame(LOOSE, r.getStorage()); + assertEquals("refs/heads/master", r.getTarget().getName()); + assertSame(NEW, r.getTarget().getStorage()); + assertNull(r.getTarget().getObjectId()); + + assertNull(refdir.getRef("refs/heads/master")); + assertNull(refdir.getRef("refs/tags/v1.0")); + assertNull(refdir.getRef("FETCH_HEAD")); + assertNull(refdir.getRef("NOT.A.REF.NAME")); + assertNull(refdir.getRef("master")); + assertNull(refdir.getRef("v1.0")); + } + + public void testGetRef_FetchHead() throws IOException { + // This is an odd special case where we need to make sure we read + // exactly the first 40 bytes of the file and nothing further on + // that line, or the remainder of the file. + write(new File(diskRepo.getDirectory(), "FETCH_HEAD"), A.name() + + "\tnot-for-merge" + + "\tbranch 'master' of git://egit.eclipse.org/jgit\n"); + + Ref r = refdir.getRef("FETCH_HEAD"); + assertFalse(r.isSymbolic()); + assertEquals(A, r.getObjectId()); + assertEquals("FETCH_HEAD", r.getName()); + assertFalse(r.isPeeled()); + assertNull(r.getPeeledObjectId()); + } + + public void testGetRef_AnyHeadWithGarbage() throws IOException { + write(new File(diskRepo.getDirectory(), "refs/heads/A"), A.name() + + "012345 . this is not a standard reference\n" + + "#and even more junk\n"); + + Ref r = refdir.getRef("refs/heads/A"); + assertFalse(r.isSymbolic()); + assertEquals(A, r.getObjectId()); + assertEquals("refs/heads/A", r.getName()); + assertFalse(r.isPeeled()); + assertNull(r.getPeeledObjectId()); + } + + public void testGetRefs_CorruptSymbolicReference() throws IOException { + String name = "refs/heads/A"; + writeLooseRef(name, "ref: \n"); + assertTrue(refdir.getRefs(RefDatabase.ALL).isEmpty()); + } + + public void testGetRef_CorruptSymbolicReference() throws IOException { + String name = "refs/heads/A"; + writeLooseRef(name, "ref: \n"); + try { + refdir.getRef(name); + fail("read an invalid reference"); + } catch (IOException err) { + String msg = err.getMessage(); + assertEquals("Not a ref: " + name + ": ref:", msg); + } + } + + public void testGetRefs_CorruptObjectIdReference() throws IOException { + String name = "refs/heads/A"; + String content = "zoo" + A.name(); + writeLooseRef(name, content + "\n"); + assertTrue(refdir.getRefs(RefDatabase.ALL).isEmpty()); + } + + public void testGetRef_CorruptObjectIdReference() throws IOException { + String name = "refs/heads/A"; + String content = "zoo" + A.name(); + writeLooseRef(name, content + "\n"); + try { + refdir.getRef(name); + fail("read an invalid reference"); + } catch (IOException err) { + String msg = err.getMessage(); + assertEquals("Not a ref: " + name + ": " + content, msg); + } + } + + public void testIsNameConflicting() throws IOException { + writeLooseRef("refs/heads/a/b", A); + writePackedRef("refs/heads/q", B); + + // new references cannot replace an existing container + assertTrue(refdir.isNameConflicting("refs")); + assertTrue(refdir.isNameConflicting("refs/heads")); + assertTrue(refdir.isNameConflicting("refs/heads/a")); + + // existing reference is not conflicting + assertFalse(refdir.isNameConflicting("refs/heads/a/b")); + + // new references are not conflicting + assertFalse(refdir.isNameConflicting("refs/heads/a/d")); + assertFalse(refdir.isNameConflicting("refs/heads/master")); + + // existing reference must not be used as a container + assertTrue(refdir.isNameConflicting("refs/heads/a/b/c")); + assertTrue(refdir.isNameConflicting("refs/heads/q/master")); + } + + public void testPeelLooseTag() throws IOException { + writeLooseRef("refs/tags/v1_0", v1_0); + writeLooseRef("refs/tags/current", "ref: refs/tags/v1_0\n"); + + final Ref tag = refdir.getRef("refs/tags/v1_0"); + final Ref cur = refdir.getRef("refs/tags/current"); + + assertEquals(v1_0, tag.getObjectId()); + assertFalse(tag.isSymbolic()); + assertFalse(tag.isPeeled()); + assertNull(tag.getPeeledObjectId()); + + assertEquals(v1_0, cur.getObjectId()); + assertTrue(cur.isSymbolic()); + assertFalse(cur.isPeeled()); + assertNull(cur.getPeeledObjectId()); + + final Ref tag_p = refdir.peel(tag); + final Ref cur_p = refdir.peel(cur); + + assertNotSame(tag, tag_p); + assertFalse(tag_p.isSymbolic()); + assertTrue(tag_p.isPeeled()); + assertEquals(v1_0, tag_p.getObjectId()); + assertEquals(v1_0.getObject(), tag_p.getPeeledObjectId()); + assertSame(tag_p, refdir.peel(tag_p)); + + assertNotSame(cur, cur_p); + assertEquals("refs/tags/current", cur_p.getName()); + assertTrue(cur_p.isSymbolic()); + assertEquals("refs/tags/v1_0", cur_p.getTarget().getName()); + assertTrue(cur_p.isPeeled()); + assertEquals(v1_0, cur_p.getObjectId()); + assertEquals(v1_0.getObject(), cur_p.getPeeledObjectId()); + + // reuses cached peeling later, but not immediately due to + // the implementation so we have to fetch it once. + final Ref tag_p2 = refdir.getRef("refs/tags/v1_0"); + assertFalse(tag_p2.isSymbolic()); + assertTrue(tag_p2.isPeeled()); + assertEquals(v1_0, tag_p2.getObjectId()); + assertEquals(v1_0.getObject(), tag_p2.getPeeledObjectId()); + + assertSame(tag_p2, refdir.getRef("refs/tags/v1_0")); + assertSame(tag_p2, refdir.getRef("refs/tags/current").getTarget()); + assertSame(tag_p2, refdir.peel(tag_p2)); + } + + public void testPeelCommit() throws IOException { + writeLooseRef("refs/heads/master", A); + + Ref master = refdir.getRef("refs/heads/master"); + assertEquals(A, master.getObjectId()); + assertFalse(master.isPeeled()); + assertNull(master.getPeeledObjectId()); + + Ref master_p = refdir.peel(master); + assertNotSame(master, master_p); + assertEquals(A, master_p.getObjectId()); + assertTrue(master_p.isPeeled()); + assertNull(master_p.getPeeledObjectId()); + + // reuses cached peeling later, but not immediately due to + // the implementation so we have to fetch it once. + Ref master_p2 = refdir.getRef("refs/heads/master"); + assertNotSame(master, master_p2); + assertEquals(A, master_p2.getObjectId()); + assertTrue(master_p2.isPeeled()); + assertNull(master_p2.getPeeledObjectId()); + assertSame(master_p2, refdir.peel(master_p2)); + } + + private void writeLooseRef(String name, AnyObjectId id) throws IOException { + writeLooseRef(name, id.name() + "\n"); + } + + private void writeLooseRef(String name, String content) throws IOException { + write(new File(diskRepo.getDirectory(), name), content); + } + + private void writePackedRef(String name, AnyObjectId id) throws IOException { + writePackedRefs(id.name() + " " + name + "\n"); + } + + private void writePackedRefs(String content) throws IOException { + File pr = new File(diskRepo.getDirectory(), "packed-refs"); + write(pr, content); + } + + private void deleteLooseRef(String name) { + File path = new File(diskRepo.getDirectory(), name); + assertTrue("deleted " + name, path.delete()); + } + + /** + * Kick the timestamp of a local file. + *

+ * We shouldn't have to make these method calls. The cache is using file + * system timestamps, and on many systems unit tests run faster than the + * modification clock. Dumping the cache after we make an edit behind + * RefDirectory's back allows the tests to pass. + * + * @param name + * the file in the repository to force a time change on. + */ + private void BUG_WorkAroundRacyGitIssues(String name) { + File path = new File(diskRepo.getDirectory(), name); + long old = path.lastModified(); + long set = 1250379778668L; // Sat Aug 15 20:12:58 GMT-03:30 2009 + path.setLastModified(set); + assertTrue("time changed", old != path.lastModified()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java index 67ed6abb2..100b758e6 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2009-2010, Google Inc. * Copyright (C) 2009, Robin Rosenberg * Copyright (C) 2009, Robin Rosenberg * and other copyright owners as documented in the project's IP log. @@ -58,15 +59,26 @@ */ public class RefTest extends SampleDataRepositoryTestCase { + private void writeSymref(String src, String dst) throws IOException { + RefUpdate u = db.updateRef(src); + switch (u.link(dst)) { + case NEW: + case FORCED: + case NO_CHANGE: + break; + default: + fail("link " + src + " to " + dst); + } + } + public void testReadAllIncludingSymrefs() throws Exception { ObjectId masterId = db.resolve("refs/heads/master"); RefUpdate updateRef = db.updateRef("refs/remotes/origin/master"); updateRef.setNewObjectId(masterId); updateRef.setForceUpdate(true); updateRef.update(); - db - .writeSymref("refs/remotes/origin/HEAD", - "refs/remotes/origin/master"); + writeSymref("refs/remotes/origin/HEAD", + "refs/remotes/origin/master"); ObjectId r = db.resolve("refs/remotes/origin/HEAD"); assertEquals(masterId, r); @@ -75,7 +87,7 @@ public void testReadAllIncludingSymrefs() throws Exception { Ref refHEAD = allRefs.get("refs/remotes/origin/HEAD"); assertNotNull(refHEAD); assertEquals(masterId, refHEAD.getObjectId()); - assertTrue(refHEAD.isPeeled()); + assertFalse(refHEAD.isPeeled()); assertNull(refHEAD.getPeeledObjectId()); Ref refmaster = allRefs.get("refs/remotes/origin/master"); @@ -85,9 +97,13 @@ public void testReadAllIncludingSymrefs() throws Exception { } public void testReadSymRefToPacked() throws IOException { - db.writeSymref("HEAD", "refs/heads/b"); + writeSymref("HEAD", "refs/heads/b"); Ref ref = db.getRef("HEAD"); - assertEquals(Ref.Storage.LOOSE_PACKED, ref.getStorage()); + assertEquals(Ref.Storage.LOOSE, ref.getStorage()); + assertTrue("is symref", ref.isSymbolic()); + ref = ref.getTarget(); + assertEquals("refs/heads/b", ref.getName()); + assertEquals(Ref.Storage.PACKED, ref.getStorage()); } public void testReadSymRefToLoosePacked() throws IOException { @@ -98,9 +114,12 @@ public void testReadSymRefToLoosePacked() throws IOException { Result update = updateRef.update(); assertEquals(Result.FORCED, update); // internal - db.writeSymref("HEAD", "refs/heads/master"); + writeSymref("HEAD", "refs/heads/master"); Ref ref = db.getRef("HEAD"); - assertEquals(Ref.Storage.LOOSE_PACKED, ref.getStorage()); + assertEquals(Ref.Storage.LOOSE, ref.getStorage()); + ref = ref.getTarget(); + assertEquals("refs/heads/master", ref.getName()); + assertEquals(Ref.Storage.LOOSE, ref.getStorage()); } public void testReadLooseRef() throws IOException { @@ -129,7 +148,7 @@ public void testReadLoosePackedRef() throws IOException, os.close(); ref = db.getRef("refs/heads/master"); - assertEquals(Storage.LOOSE_PACKED, ref.getStorage()); + assertEquals(Storage.LOOSE, ref.getStorage()); } /** @@ -149,18 +168,26 @@ public void testReadSimplePackedRefSameRepo() throws IOException { assertEquals(Result.FORCED, update); ref = db.getRef("refs/heads/master"); - assertEquals(Storage.LOOSE_PACKED, ref.getStorage()); + assertEquals(Storage.LOOSE, ref.getStorage()); } - public void testOrigResolvedNamesBranch() throws IOException { + public void testResolvedNamesBranch() throws IOException { Ref ref = db.getRef("a"); assertEquals("refs/heads/a", ref.getName()); - assertEquals("refs/heads/a", ref.getOrigName()); } - public void testOrigResolvedNamesSymRef() throws IOException { - Ref ref = db.getRef("HEAD"); - assertEquals("refs/heads/master", ref.getName()); - assertEquals("HEAD", ref.getOrigName()); + public void testResolvedSymRef() throws IOException { + Ref ref = db.getRef(Constants.HEAD); + assertEquals(Constants.HEAD, ref.getName()); + assertTrue("is symbolic ref", ref.isSymbolic()); + assertSame(Ref.Storage.LOOSE, ref.getStorage()); + + Ref dst = ref.getTarget(); + assertNotNull("has target", dst); + assertEquals("refs/heads/master", dst.getName()); + + assertSame(dst.getObjectId(), ref.getObjectId()); + assertSame(dst.getPeeledObjectId(), ref.getPeeledObjectId()); + assertEquals(dst.isPeeled(), ref.isPeeled()); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefUpdateTest.java index d851528cd..c8a5b33f6 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefUpdateTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefUpdateTest.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2008, Charles O'Farrell + * Copyright (C) 2009-2010, Google Inc. * Copyright (C) 2008-2009, Robin Rosenberg * and other copyright owners as documented in the project's IP log. * @@ -56,6 +57,18 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { + private void writeSymref(String src, String dst) throws IOException { + RefUpdate u = db.updateRef(src); + switch (u.link(dst)) { + case NEW: + case FORCED: + case NO_CHANGE: + break; + default: + fail("link " + src + " to " + dst); + } + } + private RefUpdate updateRef(final String name) throws IOException { final RefUpdate ref = db.updateRef(name); ref.setNewObjectId(db.resolve(Constants.HEAD)); @@ -260,10 +273,10 @@ public void testDeleteForce() throws IOException { delete(ref, Result.FORCED); } - public void testRefKeySameAsOrigName() { + public void testRefKeySameAsName() { Map allRefs = db.getAllRefs(); for (Entry e : allRefs.entrySet()) { - assertEquals(e.getKey(), e.getValue().getOrigName()); + assertEquals(e.getKey(), e.getValue().getName()); } } @@ -308,7 +321,7 @@ public void testUpdateRefDetached() throws Exception { assertEquals(ppid, db.resolve("HEAD")); Ref ref = db.getRef("HEAD"); assertEquals("HEAD", ref.getName()); - assertEquals("HEAD", ref.getOrigName()); + assertTrue("is detached", !ref.isSymbolic()); // the branch HEAD referred to is left untouched assertEquals(pid, db.resolve("refs/heads/master")); @@ -328,7 +341,7 @@ public void testUpdateRefDetached() throws Exception { */ public void testUpdateRefDetachedUnbornHead() throws Exception { ObjectId ppid = db.resolve("refs/heads/master^"); - db.writeSymref("HEAD", "refs/heads/unborn"); + writeSymref("HEAD", "refs/heads/unborn"); RefUpdate updateRef = db.updateRef("HEAD", true); updateRef.setForceUpdate(true); updateRef.setNewObjectId(ppid); @@ -337,7 +350,7 @@ public void testUpdateRefDetachedUnbornHead() throws Exception { assertEquals(ppid, db.resolve("HEAD")); Ref ref = db.getRef("HEAD"); assertEquals("HEAD", ref.getName()); - assertEquals("HEAD", ref.getOrigName()); + assertTrue("is detached", !ref.isSymbolic()); // the branch HEAD referred to is left untouched assertNull(db.resolve("refs/heads/unborn")); @@ -414,11 +427,14 @@ public void testRefsCacheAfterUpdate() throws Exception { updateRef.setNewObjectId(oldValue); update = updateRef.update(); assertEquals(Result.FAST_FORWARD, update); + allRefs = db.getAllRefs(); - assertEquals("refs/heads/master", allRefs.get("refs/heads/master").getName()); - assertEquals("refs/heads/master", allRefs.get("refs/heads/master").getOrigName()); - assertEquals("refs/heads/master", allRefs.get("HEAD").getName()); - assertEquals("HEAD", allRefs.get("HEAD").getOrigName()); + Ref master = allRefs.get("refs/heads/master"); + Ref head = allRefs.get("HEAD"); + assertEquals("refs/heads/master", master.getName()); + assertEquals("HEAD", head.getName()); + assertTrue("is symbolic reference", head.isSymbolic()); + assertSame(master, head.getTarget()); } /** @@ -430,21 +446,24 @@ public void testRefsCacheAfterUpdate() throws Exception { * * @throws Exception */ - public void testRefsCacheAfterUpdateLoosOnly() throws Exception { + public void testRefsCacheAfterUpdateLooseOnly() throws Exception { // Do not use the defalt repo for this case. Map allRefs = db.getAllRefs(); ObjectId oldValue = db.resolve("HEAD"); - db.writeSymref(Constants.HEAD, "refs/heads/newref"); + writeSymref(Constants.HEAD, "refs/heads/newref"); RefUpdate updateRef = db.updateRef(Constants.HEAD); updateRef.setForceUpdate(true); updateRef.setNewObjectId(oldValue); Result update = updateRef.update(); assertEquals(Result.NEW, update); + allRefs = db.getAllRefs(); - assertEquals("refs/heads/newref", allRefs.get("HEAD").getName()); - assertEquals("HEAD", allRefs.get("HEAD").getOrigName()); - assertEquals("refs/heads/newref", allRefs.get("refs/heads/newref").getName()); - assertEquals("refs/heads/newref", allRefs.get("refs/heads/newref").getOrigName()); + Ref head = allRefs.get("HEAD"); + Ref newref = allRefs.get("refs/heads/newref"); + assertEquals("refs/heads/newref", newref.getName()); + assertEquals("HEAD", head.getName()); + assertTrue("is symbolic reference", head.isSymbolic()); + assertSame(newref, head.getTarget()); } /** @@ -575,8 +594,8 @@ public void testRenameBranchHasPreviousLog() throws IOException { ObjectId oldHead = db.resolve(Constants.HEAD); assertFalse("precondition for this test, branch b != HEAD", rb .equals(oldHead)); - RefLogWriter.writeReflog(db, rb, rb, "Just a message", "refs/heads/b"); - assertTrue("no log on old branch", new File(db.getDirectory(), + writeReflog(db, rb, rb, "Just a message", "refs/heads/b"); + assertTrue("log on old branch", new File(db.getDirectory(), "logs/refs/heads/b").exists()); RefRename renameRef = db.renameRef("refs/heads/b", "refs/heads/new/name"); @@ -595,11 +614,11 @@ public void testRenameBranchHasPreviousLog() throws IOException { public void testRenameCurrentBranch() throws IOException { ObjectId rb = db.resolve("refs/heads/b"); - db.writeSymref(Constants.HEAD, "refs/heads/b"); + writeSymref(Constants.HEAD, "refs/heads/b"); ObjectId oldHead = db.resolve(Constants.HEAD); assertTrue("internal test condition, b == HEAD", rb.equals(oldHead)); - RefLogWriter.writeReflog(db, rb, rb, "Just a message", "refs/heads/b"); - assertTrue("no log on old branch", new File(db.getDirectory(), + writeReflog(db, rb, rb, "Just a message", "refs/heads/b"); + assertTrue("log on old branch", new File(db.getDirectory(), "logs/refs/heads/b").exists()); RefRename renameRef = db.renameRef("refs/heads/b", "refs/heads/new/name"); @@ -625,10 +644,9 @@ public void testRenameBranchAlsoInPack() throws IOException { updateRef.setForceUpdate(true); Result update = updateRef.update(); assertEquals("internal check new ref is loose", Result.FORCED, update); - assertEquals(Ref.Storage.LOOSE_PACKED, db.getRef("refs/heads/b") - .getStorage()); - RefLogWriter.writeReflog(db, rb, rb, "Just a message", "refs/heads/b"); - assertTrue("no log on old branch", new File(db.getDirectory(), + assertEquals(Ref.Storage.LOOSE, db.getRef("refs/heads/b").getStorage()); + writeReflog(db, rb, rb, "Just a message", "refs/heads/b"); + assertTrue("log on old branch", new File(db.getDirectory(), "logs/refs/heads/b").exists()); RefRename renameRef = db.renameRef("refs/heads/b", "refs/heads/new/name"); @@ -654,10 +672,10 @@ public void testRenameBranchAlsoInPack() throws IOException { public void tryRenameWhenLocked(String toLock, String fromName, String toName, String headPointsTo) throws IOException { // setup - db.writeSymref(Constants.HEAD, headPointsTo); + writeSymref(Constants.HEAD, headPointsTo); ObjectId oldfromId = db.resolve(fromName); ObjectId oldHeadId = db.resolve(Constants.HEAD); - RefLogWriter.writeReflog(db, oldfromId, oldfromId, "Just a message", + writeReflog(db, oldfromId, oldfromId, "Just a message", fromName); List oldFromLog = db .getReflogReader(fromName).getReverseEntries(); @@ -691,8 +709,8 @@ public void tryRenameWhenLocked(String toLock, String fromName, assertEquals(oldFromLog.toString(), db.getReflogReader(fromName) .getReverseEntries().toString()); if (oldHeadId != null) - assertEquals(oldHeadLog, db.getReflogReader(Constants.HEAD) - .getReverseEntries()); + assertEquals(oldHeadLog.toString(), db.getReflogReader( + Constants.HEAD).getReverseEntries().toString()); } finally { lockFile.unlock(); } @@ -733,12 +751,6 @@ public void testRenameBranchCannotLockAFileHEADisToLockTo() "refs/heads/new/name", "refs/heads/new/name"); } - public void testRenameBranchCannotLockAFileHEADisToLockTmp() - throws IOException { - tryRenameWhenLocked("RENAMED-REF.." + Thread.currentThread().getId(), - "refs/heads/b", "refs/heads/new/name", "refs/heads/new/name"); - } - public void testRenameBranchCannotLockAFileHEADisOtherLockFrom() throws IOException { tryRenameWhenLocked("refs/heads/b", "refs/heads/b", @@ -751,23 +763,17 @@ public void testRenameBranchCannotLockAFileHEADisOtherLockTo() "refs/heads/new/name", "refs/heads/a"); } - public void testRenameBranchCannotLockAFileHEADisOtherLockTmp() - throws IOException { - tryRenameWhenLocked("RENAMED-REF.." + Thread.currentThread().getId(), - "refs/heads/b", "refs/heads/new/name", "refs/heads/a"); - } - public void testRenameRefNameColission1avoided() throws IOException { // setup ObjectId rb = db.resolve("refs/heads/b"); - db.writeSymref(Constants.HEAD, "refs/heads/a"); + writeSymref(Constants.HEAD, "refs/heads/a"); RefUpdate updateRef = db.updateRef("refs/heads/a"); updateRef.setNewObjectId(rb); updateRef.setRefLogMessage("Setup", false); assertEquals(Result.FAST_FORWARD, updateRef.update()); ObjectId oldHead = db.resolve(Constants.HEAD); assertTrue(rb.equals(oldHead)); // assumption for this test - RefLogWriter.writeReflog(db, rb, rb, "Just a message", "refs/heads/a"); + writeReflog(db, rb, rb, "Just a message", "refs/heads/a"); assertTrue("internal check, we have a log", new File(db.getDirectory(), "logs/refs/heads/a").exists()); @@ -792,7 +798,7 @@ public void testRenameRefNameColission1avoided() throws IOException { public void testRenameRefNameColission2avoided() throws IOException { // setup ObjectId rb = db.resolve("refs/heads/b"); - db.writeSymref(Constants.HEAD, "refs/heads/prefix/a"); + writeSymref(Constants.HEAD, "refs/heads/prefix/a"); RefUpdate updateRef = db.updateRef("refs/heads/prefix/a"); updateRef.setNewObjectId(rb); updateRef.setRefLogMessage("Setup", false); @@ -800,7 +806,7 @@ public void testRenameRefNameColission2avoided() throws IOException { assertEquals(Result.FORCED, updateRef.update()); ObjectId oldHead = db.resolve(Constants.HEAD); assertTrue(rb.equals(oldHead)); // assumption for this test - RefLogWriter.writeReflog(db, rb, rb, "Just a message", + writeReflog(db, rb, rb, "Just a message", "refs/heads/prefix/a"); assertTrue("internal check, we have a log", new File(db.getDirectory(), "logs/refs/heads/prefix/a").exists()); @@ -823,4 +829,13 @@ public void testRenameRefNameColission2avoided() throws IOException { assertEquals("Branch: renamed prefix/a to prefix", db.getReflogReader( "HEAD").getReverseEntries().get(0).getComment()); } + + private void writeReflog(Repository db, ObjectId oldId, ObjectId newId, + String msg, String refName) throws IOException { + RefDirectory refs = (RefDirectory) db.getRefDatabase(); + RefDirectoryUpdate update = refs.newUpdate(refName, true); + update.setOldObjectId(oldId); + update.setNewObjectId(newId); + refs.log(update, msg, true); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java index 8e5f6fc83..88bcf7671 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java @@ -1,7 +1,7 @@ /* * Copyright (C) 2009, Christian Halstrick * Copyright (C) 2009, Christian Halstrick, Matthias Sohn, SAP AG - * Copyright (C) 2009, Google Inc. + * Copyright (C) 2009-2010, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -54,7 +54,7 @@ public void testlogAllRefUpdates() throws Exception { // check that there are no entries in the reflog and turn off writing // reflogs - assertNull(db.getReflogReader(Constants.HEAD)); + assertEquals(0, db.getReflogReader(Constants.HEAD).getReverseEntries().size()); db.getConfig().setBoolean("core", null, "logallrefupdates", false); // do one commit and check that reflog size is 0: no reflogs should be diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SymbolicRefTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SymbolicRefTest.java new file mode 100644 index 000000000..bff4fc34f --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SymbolicRefTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.lib; + +import junit.framework.TestCase; + +public class SymbolicRefTest extends TestCase { + private static final ObjectId ID_A = ObjectId + .fromString("41eb0d88f833b558bddeb269b7ab77399cdf98ed"); + + private static final ObjectId ID_B = ObjectId + .fromString("698dd0b8d0c299f080559a1cffc7fe029479a408"); + + private static final String targetName = "refs/heads/a.test.ref"; + + private static final String name = "refs/remotes/origin/HEAD"; + + public void testConstructor() { + Ref t; + SymbolicRef r; + + t = new ObjectIdRef.Unpeeled(Ref.Storage.NEW, targetName, null); + r = new SymbolicRef(name, t); + assertSame(Ref.Storage.LOOSE, r.getStorage()); + assertSame(name, r.getName()); + assertNull("no id on new ref", r.getObjectId()); + assertFalse("not peeled", r.isPeeled()); + assertNull("no peel id", r.getPeeledObjectId()); + assertSame("leaf is t", t, r.getLeaf()); + assertSame("target is t", t, r.getTarget()); + assertTrue("is symbolic", r.isSymbolic()); + + t = new ObjectIdRef.Unpeeled(Ref.Storage.PACKED, targetName, ID_A); + r = new SymbolicRef(name, t); + assertSame(Ref.Storage.LOOSE, r.getStorage()); + assertSame(name, r.getName()); + assertSame(ID_A, r.getObjectId()); + assertFalse("not peeled", r.isPeeled()); + assertNull("no peel id", r.getPeeledObjectId()); + assertSame("leaf is t", t, r.getLeaf()); + assertSame("target is t", t, r.getTarget()); + assertTrue("is symbolic", r.isSymbolic()); + } + + public void testLeaf() { + Ref a; + SymbolicRef b, c, d; + + a = new ObjectIdRef.PeeledTag(Ref.Storage.PACKED, targetName, ID_A, ID_B); + b = new SymbolicRef("B", a); + c = new SymbolicRef("C", b); + d = new SymbolicRef("D", c); + + assertSame(c, d.getTarget()); + assertSame(b, c.getTarget()); + assertSame(a, b.getTarget()); + + assertSame(a, d.getLeaf()); + assertSame(a, c.getLeaf()); + assertSame(a, b.getLeaf()); + assertSame(a, a.getLeaf()); + + assertSame(ID_A, d.getObjectId()); + assertSame(ID_A, c.getObjectId()); + assertSame(ID_A, b.getObjectId()); + + assertTrue(d.isPeeled()); + assertTrue(c.isPeeled()); + assertTrue(b.isPeeled()); + + assertSame(ID_B, d.getPeeledObjectId()); + assertSame(ID_B, c.getPeeledObjectId()); + assertSame(ID_B, b.getPeeledObjectId()); + } + + public void testToString() { + Ref a; + SymbolicRef b, c, d; + + a = new ObjectIdRef.PeeledTag(Ref.Storage.PACKED, targetName, ID_A, ID_B); + b = new SymbolicRef("B", a); + c = new SymbolicRef("C", b); + d = new SymbolicRef("D", c); + + assertEquals("SymbolicRef[D -> C -> B -> " + targetName + "=" + + ID_A.name() + "]", d.toString()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0003_Basic.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0003_Basic.java index e0cc34510..ce8a79ef9 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0003_Basic.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0003_Basic.java @@ -543,6 +543,7 @@ public void test025_packedRefs() throws IOException { w.println("0ce2ebdb36076ef0b38adbe077a07d43b43e3807 refs/tags/test022"); w.println("^b5d3b45a96b340441f5abb9080411705c51cc86c"); w.close(); + ((RefDirectory)db.getRefDatabase()).rescan(); Tag mapTag20 = db.mapTag("test020"); assertNotNull("have tag test020", mapTag20); @@ -673,6 +674,8 @@ public void test027_UnpackedRefHigherPriorityThanPacked() throws IOException { public void test028_LockPackedRef() throws IOException { writeTrashFile(".git/packed-refs", "7f822839a2fe9760f386cbbbcb3f92c5fe81def7 refs/heads/foobar"); writeTrashFile(".git/HEAD", "ref: refs/heads/foobar\n"); + BUG_WorkAroundRacyGitIssues("packed-refs"); + BUG_WorkAroundRacyGitIssues("HEAD"); ObjectId resolve = db.resolve("HEAD"); assertEquals("7f822839a2fe9760f386cbbbcb3f92c5fe81def7", resolve.name()); @@ -727,4 +730,23 @@ public void test30_stripWorkDir() { assertEquals("subdir/File.java", Repository.stripWorkDir(db.getWorkDir(), file)); } + + /** + * Kick the timestamp of a local file. + *

+ * We shouldn't have to make these method calls. The cache is using file + * system timestamps, and on many systems unit tests run faster than the + * modification clock. Dumping the cache after we make an edit behind + * RefDirectory's back allows the tests to pass. + * + * @param name + * the file in the repository to force a time change on. + */ + private void BUG_WorkAroundRacyGitIssues(String name) { + File path = new File(db.getDirectory(), name); + long old = path.lastModified(); + long set = 1250379778668L; // Sat Aug 15 20:12:58 GMT-03:30 2009 + path.setLastModified(set); + assertTrue("time changed", old != path.lastModified()); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java index 8e997e3f8..99edbd98c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java @@ -51,6 +51,7 @@ import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; @@ -88,7 +89,7 @@ public void testUpdateFastForward() throws IOException { final RemoteRefUpdate rru = new RemoteRefUpdate(db, "2c349335b7f797072cf729c4f3bb0914ecb6dec9", "refs/heads/master", false, null, null); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("ac7e7e44c1885efb472ad54a78327d66bfc4ecef")); testOneUpdateStatus(rru, ref, Status.OK, true); } @@ -103,7 +104,7 @@ public void testUpdateNonFastForwardUnknownObject() throws IOException { final RemoteRefUpdate rru = new RemoteRefUpdate(db, "2c349335b7f797072cf729c4f3bb0914ecb6dec9", "refs/heads/master", false, null, null); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("0000000000000000000000000000000000000001")); testOneUpdateStatus(rru, ref, Status.REJECTED_NONFASTFORWARD, null); } @@ -118,7 +119,7 @@ public void testUpdateNonFastForward() throws IOException { final RemoteRefUpdate rru = new RemoteRefUpdate(db, "ac7e7e44c1885efb472ad54a78327d66bfc4ecef", "refs/heads/master", false, null, null); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("2c349335b7f797072cf729c4f3bb0914ecb6dec9")); testOneUpdateStatus(rru, ref, Status.REJECTED_NONFASTFORWARD, null); } @@ -132,7 +133,7 @@ public void testUpdateNonFastForwardForced() throws IOException { final RemoteRefUpdate rru = new RemoteRefUpdate(db, "ac7e7e44c1885efb472ad54a78327d66bfc4ecef", "refs/heads/master", true, null, null); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("2c349335b7f797072cf729c4f3bb0914ecb6dec9")); testOneUpdateStatus(rru, ref, Status.OK, false); } @@ -157,7 +158,7 @@ public void testUpdateCreateRef() throws IOException { public void testUpdateDelete() throws IOException { final RemoteRefUpdate rru = new RemoteRefUpdate(db, null, "refs/heads/master", false, null, null); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("2c349335b7f797072cf729c4f3bb0914ecb6dec9")); testOneUpdateStatus(rru, ref, Status.OK, true); } @@ -183,7 +184,7 @@ public void testUpdateUpToDate() throws IOException { final RemoteRefUpdate rru = new RemoteRefUpdate(db, "2c349335b7f797072cf729c4f3bb0914ecb6dec9", "refs/heads/master", false, null, null); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("2c349335b7f797072cf729c4f3bb0914ecb6dec9")); testOneUpdateStatus(rru, ref, Status.UP_TO_DATE, null); } @@ -198,7 +199,7 @@ public void testUpdateExpectedRemote() throws IOException { "2c349335b7f797072cf729c4f3bb0914ecb6dec9", "refs/heads/master", false, null, ObjectId .fromString("ac7e7e44c1885efb472ad54a78327d66bfc4ecef")); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("ac7e7e44c1885efb472ad54a78327d66bfc4ecef")); testOneUpdateStatus(rru, ref, Status.OK, true); } @@ -214,7 +215,7 @@ public void testUpdateUnexpectedRemote() throws IOException { "2c349335b7f797072cf729c4f3bb0914ecb6dec9", "refs/heads/master", false, null, ObjectId .fromString("0000000000000000000000000000000000000001")); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("ac7e7e44c1885efb472ad54a78327d66bfc4ecef")); testOneUpdateStatus(rru, ref, Status.REJECTED_REMOTE_CHANGED, null); } @@ -231,7 +232,7 @@ public void testUpdateUnexpectedRemoteVsForce() throws IOException { "2c349335b7f797072cf729c4f3bb0914ecb6dec9", "refs/heads/master", true, null, ObjectId .fromString("0000000000000000000000000000000000000001")); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("ac7e7e44c1885efb472ad54a78327d66bfc4ecef")); testOneUpdateStatus(rru, ref, Status.REJECTED_REMOTE_CHANGED, null); } @@ -246,7 +247,7 @@ public void testUpdateRejectedByConnection() throws IOException { final RemoteRefUpdate rru = new RemoteRefUpdate(db, "2c349335b7f797072cf729c4f3bb0914ecb6dec9", "refs/heads/master", false, null, null); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("ac7e7e44c1885efb472ad54a78327d66bfc4ecef")); testOneUpdateStatus(rru, ref, Status.REJECTED_OTHER_REASON, null); } @@ -260,7 +261,7 @@ public void testUpdateRejectedByConnection() throws IOException { public void testUpdateMixedCases() throws IOException { final RemoteRefUpdate rruOk = new RemoteRefUpdate(db, null, "refs/heads/master", false, null, null); - final Ref refToChange = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref refToChange = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("2c349335b7f797072cf729c4f3bb0914ecb6dec9")); final RemoteRefUpdate rruReject = new RemoteRefUpdate(db, null, "refs/heads/nonexisting", false, null, null); @@ -282,7 +283,7 @@ public void testTrackingRefUpdateEnabled() throws IOException { final RemoteRefUpdate rru = new RemoteRefUpdate(db, "2c349335b7f797072cf729c4f3bb0914ecb6dec9", "refs/heads/master", false, "refs/remotes/test/master", null); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("ac7e7e44c1885efb472ad54a78327d66bfc4ecef")); refUpdates.add(rru); advertisedRefs.add(ref); @@ -303,7 +304,7 @@ public void testTrackingRefUpdateDisabled() throws IOException { final RemoteRefUpdate rru = new RemoteRefUpdate(db, "2c349335b7f797072cf729c4f3bb0914ecb6dec9", "refs/heads/master", false, null, null); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("ac7e7e44c1885efb472ad54a78327d66bfc4ecef")); refUpdates.add(rru); advertisedRefs.add(ref); @@ -320,7 +321,7 @@ public void testTrackingRefUpdateOnReject() throws IOException { final RemoteRefUpdate rru = new RemoteRefUpdate(db, "ac7e7e44c1885efb472ad54a78327d66bfc4ecef", "refs/heads/master", false, null, null); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("2c349335b7f797072cf729c4f3bb0914ecb6dec9")); final PushResult result = testOneUpdateStatus(rru, ref, Status.REJECTED_NONFASTFORWARD, null); @@ -336,7 +337,7 @@ public void testPushResult() throws IOException { final RemoteRefUpdate rru = new RemoteRefUpdate(db, "2c349335b7f797072cf729c4f3bb0914ecb6dec9", "refs/heads/master", false, "refs/remotes/test/master", null); - final Ref ref = new Ref(Ref.Storage.LOOSE, "refs/heads/master", + final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("ac7e7e44c1885efb472ad54a78327d66bfc4ecef")); refUpdates.add(rru); advertisedRefs.add(ref); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java index 38dbe0962..955b0c95e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java @@ -46,6 +46,7 @@ import junit.framework.TestCase; +import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.Ref; public class RefSpecTest extends TestCase { @@ -59,12 +60,12 @@ public void testMasterMaster() { assertEquals(sn + ":" + sn, rs.toString()); assertEquals(rs, new RefSpec(rs.toString())); - Ref r = new Ref(Ref.Storage.LOOSE, sn, null); + Ref r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, sn, null); assertTrue(rs.matchSource(r)); assertTrue(rs.matchDestination(r)); assertSame(rs, rs.expandFromSource(r)); - r = new Ref(Ref.Storage.LOOSE, sn + "-and-more", null); + r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, sn + "-and-more", null); assertFalse(rs.matchSource(r)); assertFalse(rs.matchDestination(r)); } @@ -91,12 +92,12 @@ public void testForceMasterMaster() { assertEquals("+" + sn + ":" + sn, rs.toString()); assertEquals(rs, new RefSpec(rs.toString())); - Ref r = new Ref(Ref.Storage.LOOSE, sn, null); + Ref r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, sn, null); assertTrue(rs.matchSource(r)); assertTrue(rs.matchDestination(r)); assertSame(rs, rs.expandFromSource(r)); - r = new Ref(Ref.Storage.LOOSE, sn + "-and-more", null); + r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, sn + "-and-more", null); assertFalse(rs.matchSource(r)); assertFalse(rs.matchDestination(r)); } @@ -111,12 +112,12 @@ public void testMaster() { assertEquals(sn, rs.toString()); assertEquals(rs, new RefSpec(rs.toString())); - Ref r = new Ref(Ref.Storage.LOOSE, sn, null); + Ref r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, sn, null); assertTrue(rs.matchSource(r)); assertFalse(rs.matchDestination(r)); assertSame(rs, rs.expandFromSource(r)); - r = new Ref(Ref.Storage.LOOSE, sn + "-and-more", null); + r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, sn + "-and-more", null); assertFalse(rs.matchSource(r)); assertFalse(rs.matchDestination(r)); } @@ -131,12 +132,12 @@ public void testForceMaster() { assertEquals("+" + sn, rs.toString()); assertEquals(rs, new RefSpec(rs.toString())); - Ref r = new Ref(Ref.Storage.LOOSE, sn, null); + Ref r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, sn, null); assertTrue(rs.matchSource(r)); assertFalse(rs.matchDestination(r)); assertSame(rs, rs.expandFromSource(r)); - r = new Ref(Ref.Storage.LOOSE, sn + "-and-more", null); + r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, sn + "-and-more", null); assertFalse(rs.matchSource(r)); assertFalse(rs.matchDestination(r)); } @@ -151,12 +152,12 @@ public void testDeleteMaster() { assertEquals(":" + sn, rs.toString()); assertEquals(rs, new RefSpec(rs.toString())); - Ref r = new Ref(Ref.Storage.LOOSE, sn, null); + Ref r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, sn, null); assertFalse(rs.matchSource(r)); assertTrue(rs.matchDestination(r)); assertSame(rs, rs.expandFromSource(r)); - r = new Ref(Ref.Storage.LOOSE, sn + "-and-more", null); + r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, sn + "-and-more", null); assertFalse(rs.matchSource(r)); assertFalse(rs.matchDestination(r)); } @@ -175,7 +176,7 @@ public void testForceRemotesOrigin() { Ref r; RefSpec expanded; - r = new Ref(Ref.Storage.LOOSE, "refs/heads/master", null); + r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", null); assertTrue(rs.matchSource(r)); assertFalse(rs.matchDestination(r)); expanded = rs.expandFromSource(r); @@ -185,11 +186,11 @@ public void testForceRemotesOrigin() { assertEquals(r.getName(), expanded.getSource()); assertEquals("refs/remotes/origin/master", expanded.getDestination()); - r = new Ref(Ref.Storage.LOOSE, "refs/remotes/origin/next", null); + r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/remotes/origin/next", null); assertFalse(rs.matchSource(r)); assertTrue(rs.matchDestination(r)); - r = new Ref(Ref.Storage.LOOSE, "refs/tags/v1.0", null); + r = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/tags/v1.0", null); assertFalse(rs.matchSource(r)); assertFalse(rs.matchDestination(r)); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefListTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefListTest.java new file mode 100644 index 000000000..c6471ded9 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefListTest.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.util; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import junit.framework.TestCase; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; + +public class RefListTest extends TestCase { + private static final ObjectId ID = ObjectId + .fromString("41eb0d88f833b558bddeb269b7ab77399cdf98ed"); + + private static final Ref REF_A = newRef("A"); + + private static final Ref REF_B = newRef("B"); + + private static final Ref REF_c = newRef("c"); + + public void testEmpty() { + RefList list = RefList.emptyList(); + assertEquals(0, list.size()); + assertTrue(list.isEmpty()); + assertFalse(list.iterator().hasNext()); + assertEquals(-1, list.find("a")); + assertEquals(-1, list.find("z")); + assertFalse(list.contains("a")); + assertNull(list.get("a")); + try { + list.get(0); + fail("RefList.emptyList should have 0 element array"); + } catch (ArrayIndexOutOfBoundsException err) { + // expected + } + } + + public void testEmptyBuilder() { + RefList list = new RefList.Builder().toRefList(); + assertEquals(0, list.size()); + assertFalse(list.iterator().hasNext()); + assertEquals(-1, list.find("a")); + assertEquals(-1, list.find("z")); + assertFalse(list.contains("a")); + assertNull(list.get("a")); + assertTrue(list.asList().isEmpty()); + assertEquals("[]", list.toString()); + + // default array capacity should be 16, with no bounds checking. + assertNull(list.get(16 - 1)); + try { + list.get(16); + fail("default RefList should have 16 element array"); + } catch (ArrayIndexOutOfBoundsException err) { + // expected + } + } + + public void testBuilder_AddThenSort() { + RefList.Builder builder = new RefList.Builder(1); + builder.add(REF_B); + builder.add(REF_A); + + RefList list = builder.toRefList(); + assertEquals(2, list.size()); + assertSame(REF_B, list.get(0)); + assertSame(REF_A, list.get(1)); + + builder.sort(); + list = builder.toRefList(); + assertEquals(2, list.size()); + assertSame(REF_A, list.get(0)); + assertSame(REF_B, list.get(1)); + } + + public void testBuilder_AddAll() { + RefList.Builder builder = new RefList.Builder(1); + Ref[] src = { REF_A, REF_B, REF_c, REF_A }; + builder.addAll(src, 1, 2); + + RefList list = builder.toRefList(); + assertEquals(2, list.size()); + assertSame(REF_B, list.get(0)); + assertSame(REF_c, list.get(1)); + } + + public void testBuilder_Set() { + RefList.Builder builder = new RefList.Builder(); + builder.add(REF_A); + builder.add(REF_A); + + assertEquals(2, builder.size()); + assertSame(REF_A, builder.get(0)); + assertSame(REF_A, builder.get(1)); + + RefList list = builder.toRefList(); + assertEquals(2, list.size()); + assertSame(REF_A, list.get(0)); + assertSame(REF_A, list.get(1)); + builder.set(1, REF_B); + + list = builder.toRefList(); + assertEquals(2, list.size()); + assertSame(REF_A, list.get(0)); + assertSame(REF_B, list.get(1)); + } + + public void testBuilder_Remove() { + RefList.Builder builder = new RefList.Builder(); + builder.add(REF_A); + builder.add(REF_B); + builder.remove(0); + + assertEquals(1, builder.size()); + assertSame(REF_B, builder.get(0)); + } + + public void testSet() { + RefList one = toList(REF_A, REF_A); + RefList two = one.set(1, REF_B); + assertNotSame(one, two); + + // one is not modified + assertEquals(2, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_A, one.get(1)); + + // but two is + assertEquals(2, two.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_B, two.get(1)); + } + + public void testAddToEmptyList() { + RefList one = toList(); + RefList two = one.add(0, REF_B); + assertNotSame(one, two); + + // one is not modified, but two is + assertEquals(0, one.size()); + assertEquals(1, two.size()); + assertFalse(two.isEmpty()); + assertSame(REF_B, two.get(0)); + } + + public void testAddToFrontOfList() { + RefList one = toList(REF_A); + RefList two = one.add(0, REF_B); + assertNotSame(one, two); + + // one is not modified, but two is + assertEquals(1, one.size()); + assertSame(REF_A, one.get(0)); + assertEquals(2, two.size()); + assertSame(REF_B, two.get(0)); + assertSame(REF_A, two.get(1)); + } + + public void testAddToEndOfList() { + RefList one = toList(REF_A); + RefList two = one.add(1, REF_B); + assertNotSame(one, two); + + // one is not modified, but two is + assertEquals(1, one.size()); + assertSame(REF_A, one.get(0)); + assertEquals(2, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(REF_B, two.get(1)); + } + + public void testAddToMiddleOfListByInsertionPosition() { + RefList one = toList(REF_A, REF_c); + + assertEquals(-2, one.find(REF_B.getName())); + + RefList two = one.add(one.find(REF_B.getName()), REF_B); + assertNotSame(one, two); + + // one is not modified, but two is + assertEquals(2, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_c, one.get(1)); + + assertEquals(3, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(REF_B, two.get(1)); + assertSame(REF_c, two.get(2)); + } + + public void testPutNewEntry() { + RefList one = toList(REF_A, REF_c); + RefList two = one.put(REF_B); + assertNotSame(one, two); + + // one is not modified, but two is + assertEquals(2, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_c, one.get(1)); + + assertEquals(3, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(REF_B, two.get(1)); + assertSame(REF_c, two.get(2)); + } + + public void testPutReplaceEntry() { + Ref otherc = newRef(REF_c.getName()); + assertNotSame(REF_c, otherc); + + RefList one = toList(REF_A, REF_c); + RefList two = one.put(otherc); + assertNotSame(one, two); + + // one is not modified, but two is + assertEquals(2, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_c, one.get(1)); + + assertEquals(2, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(otherc, two.get(1)); + } + + public void testRemoveFrontOfList() { + RefList one = toList(REF_A, REF_B, REF_c); + RefList two = one.remove(0); + assertNotSame(one, two); + + assertEquals(3, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_B, one.get(1)); + assertSame(REF_c, one.get(2)); + + assertEquals(2, two.size()); + assertSame(REF_B, two.get(0)); + assertSame(REF_c, two.get(1)); + } + + public void testRemoveMiddleOfList() { + RefList one = toList(REF_A, REF_B, REF_c); + RefList two = one.remove(1); + assertNotSame(one, two); + + assertEquals(3, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_B, one.get(1)); + assertSame(REF_c, one.get(2)); + + assertEquals(2, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(REF_c, two.get(1)); + } + + public void testRemoveEndOfList() { + RefList one = toList(REF_A, REF_B, REF_c); + RefList two = one.remove(2); + assertNotSame(one, two); + + assertEquals(3, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_B, one.get(1)); + assertSame(REF_c, one.get(2)); + + assertEquals(2, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(REF_B, two.get(1)); + } + + public void testRemoveMakesEmpty() { + RefList one = toList(REF_A); + RefList two = one.remove(1); + assertNotSame(one, two); + assertSame(two, RefList.emptyList()); + } + + public void testToString() { + StringBuilder exp = new StringBuilder(); + exp.append("["); + exp.append(REF_A); + exp.append(", "); + exp.append(REF_B); + exp.append("]"); + + RefList list = toList(REF_A, REF_B); + assertEquals(exp.toString(), list.toString()); + } + + public void testBuilder_ToString() { + StringBuilder exp = new StringBuilder(); + exp.append("["); + exp.append(REF_A); + exp.append(", "); + exp.append(REF_B); + exp.append("]"); + + RefList.Builder list = new RefList.Builder(); + list.add(REF_A); + list.add(REF_B); + assertEquals(exp.toString(), list.toString()); + } + + public void testFindContainsGet() { + RefList list = toList(REF_A, REF_B, REF_c); + + assertEquals(0, list.find("A")); + assertEquals(1, list.find("B")); + assertEquals(2, list.find("c")); + + assertEquals(-1, list.find("0")); + assertEquals(-2, list.find("AB")); + assertEquals(-3, list.find("a")); + assertEquals(-4, list.find("z")); + + assertSame(REF_A, list.get("A")); + assertSame(REF_B, list.get("B")); + assertSame(REF_c, list.get("c")); + assertNull(list.get("AB")); + assertNull(list.get("z")); + + assertTrue(list.contains("A")); + assertTrue(list.contains("B")); + assertTrue(list.contains("c")); + assertFalse(list.contains("AB")); + assertFalse(list.contains("z")); + } + + public void testIterable() { + RefList list = toList(REF_A, REF_B, REF_c); + + int idx = 0; + for (Ref ref : list) + assertSame(list.get(idx++), ref); + assertEquals(3, idx); + + Iterator i = RefList.emptyList().iterator(); + try { + i.next(); + fail("did not throw NoSuchElementException"); + } catch (NoSuchElementException err) { + // expected + } + + i = list.iterator(); + assertTrue(i.hasNext()); + assertSame(REF_A, i.next()); + try { + i.remove(); + fail("did not throw UnsupportedOperationException"); + } catch (UnsupportedOperationException err) { + // expected + } + } + + public void testCopyLeadingPrefix() { + RefList one = toList(REF_A, REF_B, REF_c); + RefList two = one.copy(2).toRefList(); + assertNotSame(one, two); + + assertEquals(3, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_B, one.get(1)); + assertSame(REF_c, one.get(2)); + + assertEquals(2, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(REF_B, two.get(1)); + } + + public void testCopyConstructorReusesArray() { + RefList.Builder one = new RefList.Builder(); + one.add(REF_A); + + RefList two = new RefList(one.toRefList()); + one.set(0, REF_B); + assertSame(REF_B, two.get(0)); + } + + private RefList toList(Ref... refs) { + RefList.Builder b = new RefList.Builder(refs.length); + b.addAll(refs, 0, refs.length); + return b.toRefList(); + } + + private static Ref newRef(final String name) { + return new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, name, ID); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java new file mode 100644 index 000000000..c4c2383f5 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.util; + +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; + +import junit.framework.TestCase; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.SymbolicRef; + +public class RefMapTest extends TestCase { + private static final ObjectId ID_ONE = ObjectId + .fromString("41eb0d88f833b558bddeb269b7ab77399cdf98ed"); + + private static final ObjectId ID_TWO = ObjectId + .fromString("698dd0b8d0c299f080559a1cffc7fe029479a408"); + + private RefList packed; + + private RefList loose; + + private RefList resolved; + + protected void setUp() throws Exception { + super.setUp(); + packed = RefList.emptyList(); + loose = RefList.emptyList(); + resolved = RefList.emptyList(); + } + + public void testEmpty_NoPrefix1() { + RefMap map = new RefMap("", packed, loose, resolved); + assertTrue(map.isEmpty()); // before size was computed + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); // after size was computed + + assertFalse(map.entrySet().iterator().hasNext()); + assertFalse(map.keySet().iterator().hasNext()); + assertFalse(map.containsKey("a")); + assertNull(map.get("a")); + } + + public void testEmpty_NoPrefix2() { + RefMap map = new RefMap(); + assertTrue(map.isEmpty()); // before size was computed + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); // after size was computed + + assertFalse(map.entrySet().iterator().hasNext()); + assertFalse(map.keySet().iterator().hasNext()); + assertFalse(map.containsKey("a")); + assertNull(map.get("a")); + } + + public void testNotEmpty_NoPrefix() { + final Ref master = newRef("refs/heads/master", ID_ONE); + packed = toList(master); + + RefMap map = new RefMap("", packed, loose, resolved); + assertFalse(map.isEmpty()); // before size was computed + assertEquals(1, map.size()); + assertFalse(map.isEmpty()); // after size was computed + assertSame(master, map.values().iterator().next()); + } + + public void testEmpty_WithPrefix() { + final Ref master = newRef("refs/heads/master", ID_ONE); + packed = toList(master); + + RefMap map = new RefMap("refs/tags/", packed, loose, resolved); + assertTrue(map.isEmpty()); // before size was computed + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); // after size was computed + + assertFalse(map.entrySet().iterator().hasNext()); + assertFalse(map.keySet().iterator().hasNext()); + } + + public void testNotEmpty_WithPrefix() { + final Ref master = newRef("refs/heads/master", ID_ONE); + packed = toList(master); + + RefMap map = new RefMap("refs/heads/", packed, loose, resolved); + assertFalse(map.isEmpty()); // before size was computed + assertEquals(1, map.size()); + assertFalse(map.isEmpty()); // after size was computed + assertSame(master, map.values().iterator().next()); + } + + public void testClear() { + final Ref master = newRef("refs/heads/master", ID_ONE); + loose = toList(master); + + RefMap map = new RefMap("", packed, loose, resolved); + assertSame(master, map.get("refs/heads/master")); + + map.clear(); + assertNull(map.get("refs/heads/master")); + assertTrue(map.isEmpty()); + assertEquals(0, map.size()); + } + + public void testIterator_RefusesRemove() { + final Ref master = newRef("refs/heads/master", ID_ONE); + loose = toList(master); + + RefMap map = new RefMap("", packed, loose, resolved); + Iterator itr = map.values().iterator(); + assertTrue(itr.hasNext()); + assertSame(master, itr.next()); + try { + itr.remove(); + fail("iterator allowed remove"); + } catch (UnsupportedOperationException err) { + // expected + } + } + + public void testIterator_FailsAtEnd() { + final Ref master = newRef("refs/heads/master", ID_ONE); + loose = toList(master); + + RefMap map = new RefMap("", packed, loose, resolved); + Iterator itr = map.values().iterator(); + assertTrue(itr.hasNext()); + assertSame(master, itr.next()); + try { + itr.next(); + fail("iterator allowed next"); + } catch (NoSuchElementException err) { + // expected + } + } + + public void testIterator_MissingUnresolvedSymbolicRefIsBug() { + final Ref master = newRef("refs/heads/master", ID_ONE); + final Ref headR = newRef("HEAD", master); + + loose = toList(master); + // loose should have added newRef("HEAD", "refs/heads/master") + resolved = toList(headR); + + RefMap map = new RefMap("", packed, loose, resolved); + Iterator itr = map.values().iterator(); + try { + itr.hasNext(); + fail("iterator did not catch bad input"); + } catch (IllegalStateException err) { + // expected + } + } + + public void testMerge_HeadMaster() { + final Ref master = newRef("refs/heads/master", ID_ONE); + final Ref headU = newRef("HEAD", "refs/heads/master"); + final Ref headR = newRef("HEAD", master); + + loose = toList(headU, master); + resolved = toList(headR); + + RefMap map = new RefMap("", packed, loose, resolved); + assertEquals(2, map.size()); + assertFalse(map.isEmpty()); + assertTrue(map.containsKey("refs/heads/master")); + assertSame(master, map.get("refs/heads/master")); + + // resolved overrides loose given same name + assertSame(headR, map.get("HEAD")); + + Iterator itr = map.values().iterator(); + assertTrue(itr.hasNext()); + assertSame(headR, itr.next()); + assertTrue(itr.hasNext()); + assertSame(master, itr.next()); + assertFalse(itr.hasNext()); + } + + public void testMerge_PackedLooseLoose() { + final Ref refA = newRef("A", ID_ONE); + final Ref refB_ONE = newRef("B", ID_ONE); + final Ref refB_TWO = newRef("B", ID_TWO); + final Ref refc = newRef("c", ID_ONE); + + packed = toList(refA, refB_ONE); + loose = toList(refB_TWO, refc); + + RefMap map = new RefMap("", packed, loose, resolved); + assertEquals(3, map.size()); + assertFalse(map.isEmpty()); + assertTrue(map.containsKey(refA.getName())); + assertSame(refA, map.get(refA.getName())); + + // loose overrides packed given same name + assertSame(refB_TWO, map.get(refB_ONE.getName())); + + Iterator itr = map.values().iterator(); + assertTrue(itr.hasNext()); + assertSame(refA, itr.next()); + assertTrue(itr.hasNext()); + assertSame(refB_TWO, itr.next()); + assertTrue(itr.hasNext()); + assertSame(refc, itr.next()); + assertFalse(itr.hasNext()); + } + + public void testMerge_WithPrefix() { + final Ref a = newRef("refs/heads/A", ID_ONE); + final Ref b = newRef("refs/heads/foo/bar/B", ID_TWO); + final Ref c = newRef("refs/heads/foo/rab/C", ID_TWO); + final Ref g = newRef("refs/heads/g", ID_ONE); + packed = toList(a, b, c, g); + + RefMap map = new RefMap("refs/heads/foo/", packed, loose, resolved); + assertEquals(2, map.size()); + + assertSame(b, map.get("bar/B")); + assertSame(c, map.get("rab/C")); + assertNull(map.get("refs/heads/foo/bar/B")); + assertNull(map.get("refs/heads/A")); + + assertTrue(map.containsKey("bar/B")); + assertTrue(map.containsKey("rab/C")); + assertFalse(map.containsKey("refs/heads/foo/bar/B")); + assertFalse(map.containsKey("refs/heads/A")); + + Iterator> itr = map.entrySet().iterator(); + Map.Entry ent; + assertTrue(itr.hasNext()); + ent = itr.next(); + assertEquals("bar/B", ent.getKey()); + assertSame(b, ent.getValue()); + assertTrue(itr.hasNext()); + ent = itr.next(); + assertEquals("rab/C", ent.getKey()); + assertSame(c, ent.getValue()); + assertFalse(itr.hasNext()); + } + + public void testPut_KeyMustMatchName_NoPrefix() { + final Ref refA = newRef("refs/heads/A", ID_ONE); + RefMap map = new RefMap("", packed, loose, resolved); + try { + map.put("FOO", refA); + fail("map accepted invalid key/value pair"); + } catch (IllegalArgumentException err) { + // expected + } + } + + public void testPut_KeyMustMatchName_WithPrefix() { + final Ref refA = newRef("refs/heads/A", ID_ONE); + RefMap map = new RefMap("refs/heads/", packed, loose, resolved); + try { + map.put("FOO", refA); + fail("map accepted invalid key/value pair"); + } catch (IllegalArgumentException err) { + // expected + } + } + + public void testPut_NoPrefix() { + final Ref refA_one = newRef("refs/heads/A", ID_ONE); + final Ref refA_two = newRef("refs/heads/A", ID_TWO); + + packed = toList(refA_one); + + RefMap map = new RefMap("", packed, loose, resolved); + assertSame(refA_one, map.get(refA_one.getName())); + assertSame(refA_one, map.put(refA_one.getName(), refA_two)); + + // map changed, but packed, loose did not + assertSame(refA_two, map.get(refA_one.getName())); + assertSame(refA_one, packed.get(0)); + assertEquals(0, loose.size()); + + assertSame(refA_two, map.put(refA_one.getName(), refA_one)); + assertSame(refA_one, map.get(refA_one.getName())); + } + + public void testPut_WithPrefix() { + final Ref refA_one = newRef("refs/heads/A", ID_ONE); + final Ref refA_two = newRef("refs/heads/A", ID_TWO); + + packed = toList(refA_one); + + RefMap map = new RefMap("refs/heads/", packed, loose, resolved); + assertSame(refA_one, map.get("A")); + assertSame(refA_one, map.put("A", refA_two)); + + // map changed, but packed, loose did not + assertSame(refA_two, map.get("A")); + assertSame(refA_one, packed.get(0)); + assertEquals(0, loose.size()); + + assertSame(refA_two, map.put("A", refA_one)); + assertSame(refA_one, map.get("A")); + } + + public void testPut_CollapseResolved() { + final Ref master = newRef("refs/heads/master", ID_ONE); + final Ref headU = newRef("HEAD", "refs/heads/master"); + final Ref headR = newRef("HEAD", master); + final Ref a = newRef("refs/heads/A", ID_ONE); + + loose = toList(headU, master); + resolved = toList(headR); + + RefMap map = new RefMap("", packed, loose, resolved); + assertNull(map.put(a.getName(), a)); + assertSame(a, map.get(a.getName())); + assertSame(headR, map.get("HEAD")); + } + + public void testRemove() { + final Ref master = newRef("refs/heads/master", ID_ONE); + final Ref headU = newRef("HEAD", "refs/heads/master"); + final Ref headR = newRef("HEAD", master); + + packed = toList(master); + loose = toList(headU, master); + resolved = toList(headR); + + RefMap map = new RefMap("", packed, loose, resolved); + assertNull(map.remove("not.a.reference")); + + assertSame(master, map.remove("refs/heads/master")); + assertNull(map.get("refs/heads/master")); + + assertSame(headR, map.remove("HEAD")); + assertNull(map.get("HEAD")); + + assertTrue(map.isEmpty()); + } + + public void testToString_NoPrefix() { + final Ref a = newRef("refs/heads/A", ID_ONE); + final Ref b = newRef("refs/heads/B", ID_TWO); + + packed = toList(a, b); + + StringBuilder exp = new StringBuilder(); + exp.append("["); + exp.append(a.toString()); + exp.append(", "); + exp.append(b.toString()); + exp.append("]"); + + RefMap map = new RefMap("", packed, loose, resolved); + assertEquals(exp.toString(), map.toString()); + } + + public void testToString_WithPrefix() { + final Ref a = newRef("refs/heads/A", ID_ONE); + final Ref b = newRef("refs/heads/foo/B", ID_TWO); + final Ref c = newRef("refs/heads/foo/C", ID_TWO); + final Ref g = newRef("refs/heads/g", ID_ONE); + + packed = toList(a, b, c, g); + + StringBuilder exp = new StringBuilder(); + exp.append("["); + exp.append(b.toString()); + exp.append(", "); + exp.append(c.toString()); + exp.append("]"); + + RefMap map = new RefMap("refs/heads/foo/", packed, loose, resolved); + assertEquals(exp.toString(), map.toString()); + } + + public void testEntryType() { + final Ref a = newRef("refs/heads/A", ID_ONE); + final Ref b = newRef("refs/heads/B", ID_TWO); + + packed = toList(a, b); + + RefMap map = new RefMap("refs/heads/", packed, loose, resolved); + Iterator> itr = map.entrySet().iterator(); + Map.Entry ent_a = itr.next(); + Map.Entry ent_b = itr.next(); + + assertEquals(ent_a.hashCode(), "A".hashCode()); + assertTrue(ent_a.equals(ent_a)); + assertFalse(ent_a.equals(ent_b)); + + assertEquals(a.toString(), ent_a.toString()); + } + + public void testEntryTypeSet() { + final Ref refA_one = newRef("refs/heads/A", ID_ONE); + final Ref refA_two = newRef("refs/heads/A", ID_TWO); + + packed = toList(refA_one); + + RefMap map = new RefMap("refs/heads/", packed, loose, resolved); + assertSame(refA_one, map.get("A")); + + Map.Entry ent = map.entrySet().iterator().next(); + assertEquals("A", ent.getKey()); + assertSame(refA_one, ent.getValue()); + + assertSame(refA_one, ent.setValue(refA_two)); + assertSame(refA_two, ent.getValue()); + assertSame(refA_two, map.get("A")); + assertEquals(1, map.size()); + } + + private RefList toList(Ref... refs) { + RefList.Builder b = new RefList.Builder(refs.length); + b.addAll(refs, 0, refs.length); + return b.toRefList(); + } + + private static Ref newRef(String name, String dst) { + return newRef(name, + new ObjectIdRef.Unpeeled(Ref.Storage.NEW, dst, null)); + } + + private static Ref newRef(String name, Ref dst) { + return new SymbolicRef(name, dst); + } + + private static Ref newRef(String name, ObjectId id) { + return new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, name, id); + } +} diff --git a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AWTPlotRenderer.java b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AWTPlotRenderer.java index 007a0e8d4..4a5d4603c 100644 --- a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AWTPlotRenderer.java +++ b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AWTPlotRenderer.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2010, Google Inc. * Copyright (C) 2008, Robin Rosenberg * Copyright (C) 2008, Shawn O. Pearce * and other copyright owners as documented in the project's IP log. @@ -146,7 +147,7 @@ void paintTriangleDown(final int cx, final int y, final int h) { @Override protected int drawLabel(int x, int y, Ref ref) { String txt; - String name = ref.getOrigName(); + String name = ref.getName(); if (name.startsWith(Constants.R_HEADS)) { g.setBackground(Color.GREEN); txt = name.substring(Constants.R_HEADS.length()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/LockFile.java index b6da244e8..bd773c470 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/LockFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/LockFile.java @@ -49,6 +49,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.FilenameFilter; import java.io.IOException; import java.io.OutputStream; import java.nio.channels.FileLock; @@ -65,6 +66,15 @@ * name. */ public class LockFile { + static final String SUFFIX = ".lock"; //$NON-NLS-1$ + + /** Filter to skip over active lock files when listing a directory. */ + static final FilenameFilter FILTER = new FilenameFilter() { + public boolean accept(File dir, String name) { + return !name.endsWith(SUFFIX); + } + }; + private final File ref; private final File lck; @@ -87,7 +97,7 @@ public class LockFile { */ public LockFile(final File f) { ref = f; - lck = new File(ref.getParentFile(), ref.getName() + ".lock"); + lck = new File(ref.getParentFile(), ref.getName() + SUFFIX); } /** @@ -334,6 +344,30 @@ public void setNeedStatInformation(final boolean on) { needStatInformation = on; } + /** + * Wait until the lock file information differs from the old file. + *

+ * This method tests both the length and the last modification date. If both + * are the same, this method sleeps until it can force the new lock file's + * modification date to be later than the target file. + * + * @throws InterruptedException + * the thread was interrupted before the last modified date of + * the lock file was different from the last modified date of + * the target file. + */ + public void waitForStatChange() throws InterruptedException { + if (ref.length() == lck.length()) { + long otime = ref.lastModified(); + long ntime = lck.lastModified(); + while (otime == ntime) { + Thread.sleep(25 /* milliseconds */); + lck.setLastModified(System.currentTimeMillis()); + ntime = lck.lastModified(); + } + } + } + /** * Commit this change and release the lock. *

diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java new file mode 100644 index 000000000..babfc6f07 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2010, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.lib; + +/** A {@link Ref} that points directly at an {@link ObjectId}. */ +public abstract class ObjectIdRef implements Ref { + /** Any reference whose peeled value is not yet known. */ + public static class Unpeeled extends ObjectIdRef { + /** + * Create a new ref pairing. + * + * @param st + * method used to store this ref. + * @param name + * name of this ref. + * @param id + * current value of the ref. May be null to indicate a ref + * that does not exist yet. + */ + public Unpeeled(Storage st, String name, ObjectId id) { + super(st, name, id); + } + + public ObjectId getPeeledObjectId() { + return null; + } + + public boolean isPeeled() { + return false; + } + } + + /** An annotated tag whose peeled object has been cached. */ + public static class PeeledTag extends ObjectIdRef { + private final ObjectId peeledObjectId; + + /** + * Create a new ref pairing. + * + * @param st + * method used to store this ref. + * @param name + * name of this ref. + * @param id + * current value of the ref. + * @param p + * the first non-tag object that tag {@code id} points to. + */ + public PeeledTag(Storage st, String name, ObjectId id, ObjectId p) { + super(st, name, id); + peeledObjectId = p; + } + + public ObjectId getPeeledObjectId() { + return peeledObjectId; + } + + public boolean isPeeled() { + return true; + } + } + + /** A reference to a non-tag object coming from a cached source. */ + public static class PeeledNonTag extends ObjectIdRef { + /** + * Create a new ref pairing. + * + * @param st + * method used to store this ref. + * @param name + * name of this ref. + * @param id + * current value of the ref. May be null to indicate a ref + * that does not exist yet. + */ + public PeeledNonTag(Storage st, String name, ObjectId id) { + super(st, name, id); + } + + public ObjectId getPeeledObjectId() { + return null; + } + + public boolean isPeeled() { + return true; + } + } + + private final String name; + + private final Storage storage; + + private final ObjectId objectId; + + /** + * Create a new ref pairing. + * + * @param st + * method used to store this ref. + * @param name + * name of this ref. + * @param id + * current value of the ref. May be null to indicate a ref that + * does not exist yet. + */ + protected ObjectIdRef(Storage st, String name, ObjectId id) { + this.name = name; + this.storage = st; + this.objectId = id; + } + + public String getName() { + return name; + } + + public boolean isSymbolic() { + return false; + } + + public Ref getLeaf() { + return this; + } + + public Ref getTarget() { + return this; + } + + public ObjectId getObjectId() { + return objectId; + } + + public Storage getStorage() { + return storage; + } + + @Override + public String toString() { + StringBuilder r = new StringBuilder(); + r.append("Ref["); + r.append(getName()); + r.append('='); + r.append(ObjectId.toString(getObjectId())); + r.append(']'); + return r.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java index 162e399d9..f119c44fe 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java @@ -50,12 +50,12 @@ * identifier. The object identifier can be any valid Git object (blob, tree, * commit, annotated tag, ...). *

- * The ref name has the attributes of the ref that was asked for as well as - * the ref it was resolved to for symbolic refs plus the object id it points - * to and (for tags) the peeled target object id, i.e. the tag resolved - * recursively until a non-tag object is referenced. + * The ref name has the attributes of the ref that was asked for as well as the + * ref it was resolved to for symbolic refs plus the object id it points to and + * (for tags) the peeled target object id, i.e. the tag resolved recursively + * until a non-tag object is referenced. */ -public class Ref { +public interface Ref { /** Location where a {@link Ref} is stored. */ public static enum Storage { /** @@ -73,8 +73,7 @@ public static enum Storage { LOOSE(true, false), /** - * The ref is stored in the packed-refs file, with - * others. + * The ref is stored in the packed-refs file, with others. *

* Updating this ref requires rewriting the file, with perhaps many * other refs being included at the same time. @@ -122,123 +121,63 @@ public boolean isPacked() { } } - private final Storage storage; - - private final String name; - - private ObjectId objectId; - - private ObjectId peeledObjectId; - - private final String origName; - - private final boolean peeled; - - /** - * Create a new ref pairing. - * - * @param st - * method used to store this ref. - * @param origName - * The name used to resolve this ref - * @param refName - * name of this ref. - * @param id - * current value of the ref. May be null to indicate a ref that - * does not exist yet. - */ - public Ref(final Storage st, final String origName, final String refName, final ObjectId id) { - this(st, origName, refName, id, null, false); - } - - /** - * Create a new ref pairing. - * - * @param st - * method used to store this ref. - * @param refName - * name of this ref. - * @param id - * current value of the ref. May be null to indicate a ref that - * does not exist yet. - */ - public Ref(final Storage st, final String refName, final ObjectId id) { - this(st, refName, refName, id, null, false); - } - - /** - * Create a new ref pairing. - * - * @param st - * method used to store this ref. - * @param origName - * The name used to resolve this ref - * @param refName - * name of this ref. - * @param id - * current value of the ref. May be null to indicate a ref that - * does not exist yet. - * @param peel - * peeled value of the ref's tag. May be null if this is not a - * tag or not yet peeled (in which case the next parameter should be null) - * @param peeled - * true if peel represents a the peeled value of the object - */ - public Ref(final Storage st, final String origName, final String refName, final ObjectId id, - final ObjectId peel, final boolean peeled) { - storage = st; - this.origName = origName; - name = refName; - objectId = id; - peeledObjectId = peel; - this.peeled = peeled; - } - - /** - * Create a new ref pairing. - * - * @param st - * method used to store this ref. - * @param refName - * name of this ref. - * @param id - * current value of the ref. May be null to indicate a ref that - * does not exist yet. - * @param peel - * peeled value of the ref's tag. May be null if this is not a - * tag or the peeled value is not known. - * @param peeled - * true if peel represents a the peeled value of the object - */ - public Ref(final Storage st, final String refName, final ObjectId id, - final ObjectId peel, boolean peeled) { - this(st, refName, refName, id, peel, peeled); - } - /** * What this ref is called within the repository. * * @return name of this ref. */ - public String getName() { - return name; - } + public String getName(); /** - * @return the originally resolved name + * Test if this reference is a symbolic reference. + *

+ * A symbolic reference does not have its own {@link ObjectId} value, but + * instead points to another {@code Ref} in the same database and always + * uses that other reference's value as its own. + * + * @return true if this is a symbolic reference; false if this reference + * contains its own ObjectId. */ - public String getOrigName() { - return origName; - } + public abstract boolean isSymbolic(); + + /** + * Traverse target references until {@link #isSymbolic()} is false. + *

+ * If {@link #isSymbolic()} is false, returns {@code this}. + *

+ * If {@link #isSymbolic()} is true, this method recursively traverses + * {@link #getTarget()} until {@link #isSymbolic()} returns false. + *

+ * This method is effectively + * + *

+	 * return isSymbolic() ? getTarget().getLeaf() : this;
+	 * 
+ * + * @return the reference that actually stores the ObjectId value. + */ + public abstract Ref getLeaf(); + + /** + * Get the reference this reference points to, or {@code this}. + *

+ * If {@link #isSymbolic()} is true this method returns the reference it + * directly names, which might not be the leaf reference, but could be + * another symbolic reference. + *

+ * If this is a leaf level reference that contains its own ObjectId,this + * method returns {@code this}. + * + * @return the target reference, or {@code this}. + */ + public abstract Ref getTarget(); /** * Cached value of this ref. * * @return the value of this ref at the last time we read it. */ - public ObjectId getObjectId() { - return objectId; - } + public abstract ObjectId getObjectId(); /** * Cached value of ref^{} (the ref peeled to commit). @@ -247,18 +186,12 @@ public ObjectId getObjectId() { * blob) that the annotated tag refers to; null if this ref does not * refer to an annotated tag. */ - public ObjectId getPeeledObjectId() { - if (!peeled) - return null; - return peeledObjectId; - } + public abstract ObjectId getPeeledObjectId(); /** * @return whether the Ref represents a peeled tag */ - public boolean isPeeled() { - return peeled; - } + public abstract boolean isPeeled(); /** * How was this ref obtained? @@ -268,18 +201,5 @@ public boolean isPeeled() { * * @return type of ref. */ - public Storage getStorage() { - return storage; - } - - public String toString() { - String o = ""; - if (!origName.equals(name)) - o = "(" + origName + ")"; - return "Ref[" + o + name + "=" + ObjectId.toString(getObjectId()) + "]"; - } - - void setPeeledObjectId(final ObjectId id) { - peeledObjectId = id; - } + public abstract Storage getStorage(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java index fe6d210c1..f14488b73 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2008, Charles O'Farrell - * Copyright (C) 2008, Google Inc. + * Copyright (C) 2010, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -61,12 +61,12 @@ public class RefComparator implements Comparator { public static final RefComparator INSTANCE = new RefComparator(); public int compare(final Ref o1, final Ref o2) { - return o1.getOrigName().compareTo(o2.getOrigName()); + return compareTo(o1, o2); } /** * Sorts the collection of refs, returning a new collection. - * + * * @param refs * collection to be sorted * @return sorted collection of refs @@ -76,4 +76,30 @@ public static Collection sort(final Collection refs) { Collections.sort(r, INSTANCE); return r; } + + /** + * Compare a reference to a name. + * + * @param o1 + * the reference instance. + * @param o2 + * the name to compare to. + * @return standard Comparator result of < 0, 0, > 0. + */ + public static int compareTo(Ref o1, String o2) { + return o1.getName().compareTo(o2); + } + + /** + * Compare two references by name. + * + * @param o1 + * the reference instance. + * @param o2 + * the other reference instance. + * @return standard Comparator result of < 0, 0, > 0. + */ + public static int compareTo(final Ref o1, final Ref o2) { + return o1.getName().compareTo(o2.getName()); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java index fb6a27db3..21e7041b3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java @@ -1,6 +1,5 @@ /* - * Copyright (C) 2007-2009, Robin Rosenberg - * Copyright (C) 2006-2008, Shawn O. Pearce + * Copyright (C) 2010, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -44,499 +43,156 @@ package org.eclipse.jgit.lib; -import static org.eclipse.jgit.lib.Constants.R_TAGS; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStreamReader; -import java.util.HashMap; import java.util.Map; -import org.eclipse.jgit.errors.ObjectWritingException; -import org.eclipse.jgit.lib.Ref.Storage; -import org.eclipse.jgit.util.FS; -import org.eclipse.jgit.util.IO; -import org.eclipse.jgit.util.RawParseUtils; - -class RefDatabase { - private static final String REFS_SLASH = "refs/"; - - private static final String[] refSearchPaths = { "", REFS_SLASH, - R_TAGS, Constants.R_HEADS, Constants.R_REMOTES }; - - private final Repository db; - - private final File gitDir; - - private final File refsDir; - - private Map looseRefs; - private Map looseRefsMTime; - private Map looseSymRefs; - - private final File packedRefsFile; - - private Map packedRefs; - - private long packedRefsLastModified; - - private long packedRefsLength; - - int lastRefModification; - - int lastNotifiedRefModification; - - private int refModificationCounter; - - RefDatabase(final Repository r) { - db = r; - gitDir = db.getDirectory(); - refsDir = FS.resolve(gitDir, "refs"); - packedRefsFile = FS.resolve(gitDir, Constants.PACKED_REFS); - clearCache(); - } - - synchronized void clearCache() { - looseRefs = new HashMap(); - looseRefsMTime = new HashMap(); - packedRefs = new HashMap(); - looseSymRefs = new HashMap(); - packedRefsLastModified = 0; - packedRefsLength = 0; - } - - Repository getRepository() { - return db; - } - - void create() { - refsDir.mkdir(); - new File(refsDir, "heads").mkdir(); - new File(refsDir, "tags").mkdir(); - } - - ObjectId idOf(final String name) throws IOException { - refreshPackedRefs(); - final Ref r = readRefBasic(name, 0); - return r != null ? r.getObjectId() : null; - } - +/** + * Abstraction of name to {@link ObjectId} mapping. + *

+ * A reference database stores a mapping of reference names to {@link ObjectId}. + * Every {@link Repository} has a single reference database, mapping names to + * the tips of the object graph contained by the {@link ObjectDatabase}. + */ +public abstract class RefDatabase { /** - * Create a command to update, create or delete a ref in this repository. - * - * @param name - * name of the ref the caller wants to modify. - * @return an update command. The caller must finish populating this command - * and then invoke one of the update methods to actually make a - * change. - * @throws IOException - * a symbolic ref was passed in and could not be resolved back - * to the base ref, as the symbolic ref could not be read. + * Order of prefixes to search when using non-absolute references. + *

+ * The implementation's {@link #getRef(String)} method must take this search + * space into consideration when locating a reference by name. The first + * entry in the path is always {@code ""}, ensuring that absolute references + * are resolved without further mangling. */ - RefUpdate newUpdate(final String name) throws IOException { - return newUpdate(name, false); - } + protected static final String[] SEARCH_PATH = { "", //$NON-NLS-1$ + Constants.R_REFS, // + Constants.R_TAGS, // + Constants.R_HEADS, // + Constants.R_REMOTES // + }; /** - * Create a command to update, create or delete a ref in this repository. + * Maximum number of times a {@link SymbolicRef} can be traversed. + *

+ * If the reference is nested deeper than this depth, the implementation + * should either fail, or at least claim the reference does not exist. + */ + protected static final int MAX_SYMBOLIC_REF_DEPTH = 5; + + /** Magic value for {@link #getRefs(String)} to return all references. */ + public static final String ALL = "";//$NON-NLS-1$ + + /** + * Initialize a new reference database at this location. + * + * @throws IOException + * the database could not be created. + */ + public abstract void create() throws IOException; + + /** Close any resources held by this database. */ + public abstract void close(); + + /** + * Determine if a proposed reference name overlaps with an existing one. + *

+ * Reference names use '/' as a component separator, and may be stored in a + * hierarchical storage such as a directory on the local filesystem. + *

+ * If the reference "refs/heads/foo" exists then "refs/heads/foo/bar" must + * not exist, as a reference cannot have a value and also be a container for + * other references at the same time. + *

+ * If the reference "refs/heads/foo/bar" exists than the reference + * "refs/heads/foo" cannot exist, for the same reason. * * @param name - * name of the ref the caller wants to modify. + * proposed name. + * @return true if the name overlaps with an existing reference; false if + * using this name right now would be safe. + * @throws IOException + * the database could not be read to check for conflicts. + */ + public abstract boolean isNameConflicting(String name) throws IOException; + + /** + * Create a new update command to create, modify or delete a reference. + * + * @param name + * the name of the reference. * @param detach - * true to detach the ref, i.e. replace symref with object ref - * @return an update command. The caller must finish populating this command - * and then invoke one of the update methods to actually make a - * change. + * if {@code true} and {@code name} is currently a + * {@link SymbolicRef}, the update will replace it with an + * {@link ObjectIdRef}. Otherwise, the update will recursively + * traverse {@link SymbolicRef}s and operate on the leaf + * {@link ObjectIdRef}. + * @return a new update for the requested name; never null. * @throws IOException - * a symbolic ref was passed in and could not be resolved back - * to the base ref, as the symbolic ref could not be read. + * the reference space cannot be accessed. */ - RefUpdate newUpdate(final String name, boolean detach) throws IOException { - refreshPackedRefs(); - Ref r = readRefBasic(name, 0); - if (r == null) - r = new Ref(Ref.Storage.NEW, name, null); - else if (detach) - r = new Ref(Ref.Storage.NEW, name, r.getObjectId()); - return new RefUpdate(this, r, fileForRef(r.getName())); - } - - void stored(final String origName, final String name, final ObjectId id, final long time) { - synchronized (this) { - looseRefs.put(name, new Ref(Ref.Storage.LOOSE, name, name, id)); - looseRefsMTime.put(name, time); - setModified(); - } - db.fireRefsMaybeChanged(); - } + public abstract RefUpdate newUpdate(String name, boolean detach) + throws IOException; /** - * An set of update operations for renaming a ref + * Create a new update command to rename a reference. * - * @param fromRef Old ref name - * @param toRef New ref name - * @return a RefUpdate operation to rename a ref + * @param fromName + * name of reference to rename from + * @param toName + * name of reference to rename to + * @return an update command that knows how to rename a branch to another. * @throws IOException + * the reference space cannot be accessed. */ - RefRename newRename(String fromRef, String toRef) throws IOException { - refreshPackedRefs(); - Ref f = readRefBasic(fromRef, 0); - Ref t = new Ref(Ref.Storage.NEW, toRef, null); - RefUpdate refUpdateFrom = new RefUpdate(this, f, fileForRef(f.getName())); - RefUpdate refUpdateTo = new RefUpdate(this, t, fileForRef(t.getName())); - return new RefRename(refUpdateTo, refUpdateFrom); - } + public abstract RefRename newRename(String fromName, String toName) + throws IOException; /** - * Writes a symref (e.g. HEAD) to disk + * Read a single reference. + *

+ * Aside from taking advantage of {@link #SEARCH_PATH}, this method may be + * able to more quickly resolve a single reference name than obtaining the + * complete namespace by {@code getRefs(ALL).get(name)}. * * @param name - * symref name - * @param target - * pointed to ref + * the name of the reference. May be a short name which must be + * searched for using the standard {@link #SEARCH_PATH}. + * @return the reference (if it exists); else {@code null}. * @throws IOException + * the reference space cannot be accessed. */ - void link(final String name, final String target) throws IOException { - final byte[] content = Constants.encode("ref: " + target + "\n"); - lockAndWriteFile(fileForRef(name), content); - synchronized (this) { - looseSymRefs.remove(name); - setModified(); - } - db.fireRefsMaybeChanged(); - } - - void uncacheSymRef(String name) { - synchronized(this) { - looseSymRefs.remove(name); - setModified(); - } - } - - void uncacheRef(String name) { - looseRefs.remove(name); - looseRefsMTime.remove(name); - packedRefs.remove(name); - } - - private void setModified() { - lastRefModification = refModificationCounter++; - } - - Ref readRef(final String partialName) throws IOException { - refreshPackedRefs(); - for (int k = 0; k < refSearchPaths.length; k++) { - final Ref r = readRefBasic(refSearchPaths[k] + partialName, 0); - if (r != null && r.getObjectId() != null) - return r; - } - return null; - } + public abstract Ref getRef(String name) throws IOException; /** - * @return all known refs (heads, tags, remotes). + * Get a section of the reference namespace. + * + * @param prefix + * prefix to search the namespace with; must end with {@code /}. + * If the empty string ({@link #ALL}), obtain a complete snapshot + * of all references. + * @return modifiable map that is a complete snapshot of the current + * reference namespace, with {@code prefix} removed from the start + * of each key. The map can be an unsorted map. + * @throws IOException + * the reference space cannot be accessed. */ - Map getAllRefs() { - return readRefs(); - } + public abstract Map getRefs(String prefix) throws IOException; /** - * @return all tags; key is short tag name ("v1.0") and value of the entry - * contains the ref with the full tag name ("refs/tags/v1.0"). + * Peel a possibly unpeeled reference by traversing the annotated tags. + *

+ * If the reference cannot be peeled (as it does not refer to an annotated + * tag) the peeled id stays null, but {@link Ref#isPeeled()} will be true. + *

+ * Implementors should check {@link Ref#isPeeled()} before performing any + * additional work effort. + * + * @param ref + * The reference to peel + * @return {@code ref} if {@code ref.isPeeled()} is true; otherwise a new + * Ref object representing the same data as Ref, but isPeeled() will + * be true and getPeeledObjectId() will contain the peeled object + * (or null). + * @throws IOException + * the reference space or object space cannot be accessed. */ - Map getTags() { - final Map tags = new HashMap(); - for (final Ref r : readRefs().values()) { - if (r.getName().startsWith(R_TAGS)) - tags.put(r.getName().substring(R_TAGS.length()), r); - } - return tags; - } - - private Map readRefs() { - final HashMap avail = new HashMap(); - readPackedRefs(avail); - readLooseRefs(avail, REFS_SLASH, refsDir); - try { - final Ref r = readRefBasic(Constants.HEAD, 0); - if (r != null && r.getObjectId() != null) - avail.put(Constants.HEAD, r); - } catch (IOException e) { - // ignore here - } - db.fireRefsMaybeChanged(); - return avail; - } - - private synchronized void readPackedRefs(final Map avail) { - refreshPackedRefs(); - avail.putAll(packedRefs); - } - - private void readLooseRefs(final Map avail, - final String prefix, final File dir) { - final File[] entries = dir.listFiles(); - if (entries == null) - return; - - for (final File ent : entries) { - final String entName = ent.getName(); - if (".".equals(entName) || "..".equals(entName)) - continue; - if (ent.isDirectory()) { - readLooseRefs(avail, prefix + entName + "/", ent); - } else { - try { - final Ref ref = readRefBasic(prefix + entName, 0); - if (ref != null) - avail.put(ref.getOrigName(), ref); - } catch (IOException e) { - continue; - } - } - } - } - - Ref peel(final Ref ref) { - if (ref.isPeeled()) - return ref; - ObjectId peeled = null; - try { - Object target = db.mapObject(ref.getObjectId(), ref.getName()); - while (target instanceof Tag) { - final Tag tag = (Tag)target; - peeled = tag.getObjId(); - if (Constants.TYPE_TAG.equals(tag.getType())) - target = db.mapObject(tag.getObjId(), ref.getName()); - else - break; - } - } catch (IOException e) { - // Ignore a read error.  Callers will also get the same error - // if they try to use the result of getPeeledObjectId. - } - return new Ref(ref.getStorage(), ref.getName(), ref.getObjectId(), peeled, true); - - } - - private File fileForRef(final String name) { - if (name.startsWith(REFS_SLASH)) - return new File(refsDir, name.substring(REFS_SLASH.length())); - return new File(gitDir, name); - } - - private Ref readRefBasic(final String name, final int depth) throws IOException { - return readRefBasic(name, name, depth); - } - - private synchronized Ref readRefBasic(final String origName, - final String name, final int depth) throws IOException { - // Prefer loose ref to packed ref as the loose - // file can be more up-to-date than a packed one. - // - Ref ref = looseRefs.get(origName); - final File loose = fileForRef(name); - final long mtime = loose.lastModified(); - if (ref != null) { - Long cachedlastModified = looseRefsMTime.get(name); - if (cachedlastModified != null && cachedlastModified == mtime) { - if (packedRefs.containsKey(origName)) - return new Ref(Storage.LOOSE_PACKED, origName, ref - .getObjectId(), ref.getPeeledObjectId(), ref - .isPeeled()); - else - return ref; - } - looseRefs.remove(origName); - looseRefsMTime.remove(origName); - } - - if (mtime == 0) { - // If last modified is 0 the file does not exist. - // Try packed cache. - // - ref = packedRefs.get(name); - if (ref != null) - if (!ref.getOrigName().equals(origName)) - ref = new Ref(Storage.LOOSE_PACKED, origName, name, ref.getObjectId()); - return ref; - } - - String line = null; - try { - Long cachedlastModified = looseRefsMTime.get(name); - if (cachedlastModified != null && cachedlastModified == mtime) { - line = looseSymRefs.get(name); - } - if (line == null) { - line = readLine(loose); - looseRefsMTime.put(name, mtime); - looseSymRefs.put(name, line); - } - } catch (FileNotFoundException notLoose) { - return packedRefs.get(name); - } - - if (line == null || line.length() == 0) { - looseRefs.remove(origName); - looseRefsMTime.remove(origName); - return new Ref(Ref.Storage.LOOSE, origName, name, null); - } - - if (line.startsWith("ref: ")) { - if (depth >= 5) { - throw new IOException("Exceeded maximum ref depth of " + depth - + " at " + name + ". Circular reference?"); - } - - final String target = line.substring("ref: ".length()); - Ref r = readRefBasic(target, target, depth + 1); - Long cachedMtime = looseRefsMTime.get(name); - if (cachedMtime != null && cachedMtime != mtime) - setModified(); - looseRefsMTime.put(name, mtime); - if (r == null) - return new Ref(Ref.Storage.LOOSE, origName, target, null); - if (!origName.equals(r.getName())) - r = new Ref(Ref.Storage.LOOSE_PACKED, origName, r.getName(), r.getObjectId(), r.getPeeledObjectId(), true); - return r; - } - - setModified(); - - final ObjectId id; - try { - id = ObjectId.fromString(line); - } catch (IllegalArgumentException notRef) { - throw new IOException("Not a ref: " + name + ": " + line); - } - - Storage storage; - if (packedRefs.containsKey(name)) - storage = Ref.Storage.LOOSE_PACKED; - else - storage = Ref.Storage.LOOSE; - ref = new Ref(storage, name, id); - looseRefs.put(name, ref); - looseRefsMTime.put(name, mtime); - - if (!origName.equals(name)) { - ref = new Ref(Ref.Storage.LOOSE, origName, name, id); - looseRefs.put(origName, ref); - } - - return ref; - } - - private synchronized void refreshPackedRefs() { - final long currTime = packedRefsFile.lastModified(); - final long currLen = currTime == 0 ? 0 : packedRefsFile.length(); - if (currTime == packedRefsLastModified && currLen == packedRefsLength) - return; - if (currTime == 0) { - packedRefsLastModified = 0; - packedRefsLength = 0; - packedRefs = new HashMap(); - return; - } - - final Map newPackedRefs = new HashMap(); - try { - final BufferedReader b = openReader(packedRefsFile); - try { - String p; - Ref last = null; - while ((p = b.readLine()) != null) { - if (p.charAt(0) == '#') - continue; - - if (p.charAt(0) == '^') { - if (last == null) - throw new IOException("Peeled line before ref."); - - final ObjectId id = ObjectId.fromString(p.substring(1)); - last = new Ref(Ref.Storage.PACKED, last.getName(), last - .getName(), last.getObjectId(), id, true); - newPackedRefs.put(last.getName(), last); - continue; - } - - final int sp = p.indexOf(' '); - final ObjectId id = ObjectId.fromString(p.substring(0, sp)); - final String name = copy(p, sp + 1, p.length()); - last = new Ref(Ref.Storage.PACKED, name, name, id); - newPackedRefs.put(last.getName(), last); - } - } finally { - b.close(); - } - packedRefsLastModified = currTime; - packedRefsLength = currLen; - packedRefs = newPackedRefs; - setModified(); - } catch (FileNotFoundException noPackedRefs) { - // Ignore it and leave the new map empty. - // - packedRefsLastModified = 0; - packedRefsLength = 0; - packedRefs = newPackedRefs; - } catch (IOException e) { - throw new RuntimeException("Cannot read packed refs", e); - } - } - - private static String copy(final String src, final int off, final int end) { - return new StringBuilder(end - off).append(src, off, end).toString(); - } - - private void lockAndWriteFile(File file, byte[] content) throws IOException { - String name = file.getName(); - final LockFile lck = new LockFile(file); - if (!lck.lock()) - throw new ObjectWritingException("Unable to lock " + name); - try { - lck.write(content); - } catch (IOException ioe) { - throw new ObjectWritingException("Unable to write " + name, ioe); - } - if (!lck.commit()) - throw new ObjectWritingException("Unable to write " + name); - } - - synchronized void removePackedRef(String name) throws IOException { - packedRefs.remove(name); - writePackedRefs(); - } - - private void writePackedRefs() throws IOException { - new RefWriter(packedRefs.values()) { - @Override - protected void writeFile(String name, byte[] content) throws IOException { - lockAndWriteFile(new File(db.getDirectory(), name), content); - } - }.writePackedRefs(); - } - - private static String readLine(final File file) - throws FileNotFoundException, IOException { - final byte[] buf = IO.readFully(file, 4096); - int n = buf.length; - - // remove trailing whitespaces - while (n > 0 && Character.isWhitespace(buf[n - 1])) - n--; - - if (n == 0) - return null; - return RawParseUtils.decode(buf, 0, n); - } - - private static BufferedReader openReader(final File fileLocation) - throws FileNotFoundException { - return new BufferedReader(new InputStreamReader(new FileInputStream( - fileLocation), Constants.CHARSET)); - } + public abstract Ref peel(Ref ref) throws IOException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectory.java new file mode 100644 index 000000000..90ac0bf47 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectory.java @@ -0,0 +1,1006 @@ +/* + * Copyright (C) 2007, Dave Watson + * Copyright (C) 2009-2010, Google Inc. + * Copyright (C) 2007, Robin Rosenberg + * Copyright (C) 2006, Shawn O. Pearce + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.lib; + +import static org.eclipse.jgit.lib.Constants.CHARSET; +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.lib.Constants.LOGS; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH; +import static org.eclipse.jgit.lib.Constants.PACKED_REFS; +import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.eclipse.jgit.lib.Constants.R_REFS; +import static org.eclipse.jgit.lib.Constants.R_REMOTES; +import static org.eclipse.jgit.lib.Constants.R_TAGS; +import static org.eclipse.jgit.lib.Constants.encode; +import static org.eclipse.jgit.lib.Ref.Storage.LOOSE; +import static org.eclipse.jgit.lib.Ref.Storage.NEW; +import static org.eclipse.jgit.lib.Ref.Storage.PACKED; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jgit.errors.ObjectWritingException; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.IO; +import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.RefList; +import org.eclipse.jgit.util.RefMap; + +/** + * Traditional file system based {@link RefDatabase}. + *

+ * This is the classical reference database representation for a Git repository. + * References are stored in two formats: loose, and packed. + *

+ * Loose references are stored as individual files within the {@code refs/} + * directory. The file name matches the reference name and the file contents is + * the current {@link ObjectId} in string form. + *

+ * Packed references are stored in a single text file named {@code packed-refs}. + * In the packed format, each reference is stored on its own line. This file + * reduces the number of files needed for large reference spaces, reducing the + * overall size of a Git repository on disk. + */ +public class RefDirectory extends RefDatabase { + /** Magic string denoting the start of a symbolic reference file. */ + public static final String SYMREF = "ref: "; //$NON-NLS-1$ + + /** Magic string denoting the header of a packed-refs file. */ + public static final String PACKED_REFS_HEADER = "# pack-refs with:"; //$NON-NLS-1$ + + /** If in the header, denotes the file has peeled data. */ + public static final String PACKED_REFS_PEELED = " peeled"; //$NON-NLS-1$ + + private final Repository parent; + + private final File gitDir; + + private final File refsDir; + + private final File logsDir; + + private final File logsRefsDir; + + private final File packedRefsFile; + + /** + * Immutable sorted list of loose references. + *

+ * Symbolic references in this collection are stored unresolved, that is + * their target appears to be a new reference with no ObjectId. These are + * converted into resolved references during a get operation, ensuring the + * live value is always returned. + */ + private final AtomicReference> looseRefs = new AtomicReference>(); + + /** Immutable sorted list of packed references. */ + private final AtomicReference packedRefs = new AtomicReference(); + + /** + * Number of modifications made to this database. + *

+ * This counter is incremented when a change is made, or detected from the + * filesystem during a read operation. + */ + private final AtomicInteger modCnt = new AtomicInteger(); + + /** + * Last {@link #modCnt} that we sent to listeners. + *

+ * This value is compared to {@link #modCnt}, and a notification is sent to + * the listeners only when it differs. + */ + private final AtomicInteger lastNotifiedModCnt = new AtomicInteger(); + + RefDirectory(final Repository db) { + parent = db; + gitDir = db.getDirectory(); + refsDir = FS.resolve(gitDir, R_REFS); + logsDir = FS.resolve(gitDir, LOGS); + logsRefsDir = FS.resolve(gitDir, LOGS + '/' + R_REFS); + packedRefsFile = FS.resolve(gitDir, PACKED_REFS); + + looseRefs.set(RefList. emptyList()); + packedRefs.set(PackedRefList.NO_PACKED_REFS); + } + + Repository getRepository() { + return parent; + } + + public void create() throws IOException { + refsDir.mkdir(); + logsDir.mkdir(); + logsRefsDir.mkdir(); + + new File(refsDir, R_HEADS.substring(R_REFS.length())).mkdir(); + new File(refsDir, R_TAGS.substring(R_REFS.length())).mkdir(); + new File(logsRefsDir, R_HEADS.substring(R_REFS.length())).mkdir(); + } + + @Override + public void close() { + // We have no resources to close. + } + + void rescan() { + looseRefs.set(RefList. emptyList()); + packedRefs.set(PackedRefList.NO_PACKED_REFS); + } + + @Override + public boolean isNameConflicting(String name) throws IOException { + RefList packed = getPackedRefs(); + RefList loose = getLooseRefs(); + + // Cannot be nested within an existing reference. + int lastSlash = name.lastIndexOf('/'); + while (0 < lastSlash) { + String needle = name.substring(0, lastSlash); + if (loose.contains(needle) || packed.contains(needle)) + return true; + lastSlash = name.lastIndexOf('/', lastSlash - 1); + } + + // Cannot be the container of an existing reference. + String prefix = name + '/'; + int idx; + + idx = -(packed.find(prefix) + 1); + if (idx < packed.size() && packed.get(idx).getName().startsWith(prefix)) + return true; + + idx = -(loose.find(prefix) + 1); + if (idx < loose.size() && loose.get(idx).getName().startsWith(prefix)) + return true; + + return false; + } + + private RefList getLooseRefs() { + final RefList oldLoose = looseRefs.get(); + + LooseScanner scan = new LooseScanner(oldLoose); + scan.scan(ALL); + + RefList loose; + if (scan.newLoose != null) { + loose = scan.newLoose.toRefList(); + if (looseRefs.compareAndSet(oldLoose, loose)) + modCnt.incrementAndGet(); + } else + loose = oldLoose; + return loose; + } + + @Override + public Ref getRef(final String needle) throws IOException { + final RefList packed = getPackedRefs(); + Ref ref = null; + for (String prefix : SEARCH_PATH) { + ref = readRef(prefix + needle, packed); + if (ref != null) { + ref = resolve(ref, 0, null, null, packed); + break; + } + } + fireRefsChanged(); + return ref; + } + + @Override + public Map getRefs(String prefix) throws IOException { + final RefList packed = getPackedRefs(); + final RefList oldLoose = looseRefs.get(); + + LooseScanner scan = new LooseScanner(oldLoose); + scan.scan(prefix); + + RefList loose; + if (scan.newLoose != null) { + loose = scan.newLoose.toRefList(); + if (looseRefs.compareAndSet(oldLoose, loose)) + modCnt.incrementAndGet(); + } else + loose = oldLoose; + fireRefsChanged(); + + RefList.Builder symbolic = scan.symbolic; + for (int idx = 0; idx < symbolic.size();) { + Ref ref = symbolic.get(idx); + ref = resolve(ref, 0, prefix, loose, packed); + if (ref != null && ref.getObjectId() != null) { + symbolic.set(idx, ref); + idx++; + } else { + // A broken symbolic reference, we have to drop it from the + // collections the client is about to receive. Should be a + // rare occurrence so pay a copy penalty. + loose = loose.remove(idx); + symbolic.remove(idx); + } + } + + return new RefMap(prefix, packed, upcast(loose), symbolic.toRefList()); + } + + @SuppressWarnings("unchecked") + private RefList upcast(RefList loose) { + return (RefList) loose; + } + + private class LooseScanner { + private final RefList curLoose; + + private int curIdx; + + final RefList.Builder symbolic = new RefList.Builder(4); + + RefList.Builder newLoose; + + LooseScanner(final RefList curLoose) { + this.curLoose = curLoose; + } + + void scan(String prefix) { + if (ALL.equals(prefix)) { + scanOne(HEAD); + scanTree(R_REFS, refsDir); + + // If any entries remain, they are deleted, drop them. + if (newLoose == null && curIdx < curLoose.size()) + newLoose = curLoose.copy(curIdx); + + } else if (prefix.startsWith(R_REFS) && prefix.endsWith("/")) { + curIdx = -(curLoose.find(prefix) + 1); + File dir = new File(refsDir, prefix.substring(R_REFS.length())); + scanTree(prefix, dir); + + // Skip over entries still within the prefix; these have + // been removed from the directory. + while (curIdx < curLoose.size()) { + if (!curLoose.get(curIdx).getName().startsWith(prefix)) + break; + if (newLoose == null) + newLoose = curLoose.copy(curIdx); + curIdx++; + } + + // Keep any entries outside of the prefix space, we + // do not know anything about their status. + if (newLoose != null) { + while (curIdx < curLoose.size()) + newLoose.add(curLoose.get(curIdx++)); + } + } + } + + private void scanTree(String prefix, File dir) { + final String[] entries = dir.list(LockFile.FILTER); + if (entries != null && 0 < entries.length) { + Arrays.sort(entries); + for (String name : entries) { + File e = new File(dir, name); + if (e.isDirectory()) + scanTree(prefix + name + '/', e); + else + scanOne(prefix + name); + } + } + } + + private void scanOne(String name) { + LooseRef cur; + + if (curIdx < curLoose.size()) { + do { + cur = curLoose.get(curIdx); + int cmp = RefComparator.compareTo(cur, name); + if (cmp < 0) { + // Reference is not loose anymore, its been deleted. + // Skip the name in the new result list. + if (newLoose == null) + newLoose = curLoose.copy(curIdx); + curIdx++; + cur = null; + continue; + } + + if (cmp > 0) // Newly discovered loose reference. + cur = null; + break; + } while (curIdx < curLoose.size()); + } else + cur = null; // Newly discovered loose reference. + + LooseRef n; + try { + n = scanRef(cur, name); + } catch (IOException notValid) { + n = null; + } + + if (n != null) { + if (cur != n && newLoose == null) + newLoose = curLoose.copy(curIdx); + if (newLoose != null) + newLoose.add(n); + if (n.isSymbolic()) + symbolic.add(n); + } else if (cur != null) { + // Tragically, this file is no longer a loose reference. + // Kill our cached entry of it. + if (newLoose == null) + newLoose = curLoose.copy(curIdx); + } + + if (cur != null) + curIdx++; + } + } + + @Override + public Ref peel(final Ref ref) throws IOException { + final Ref leaf = ref.getLeaf(); + if (leaf.isPeeled() || leaf.getObjectId() == null) + return ref; + + RevWalk rw = new RevWalk(getRepository()); + RevObject obj = rw.parseAny(leaf.getObjectId()); + ObjectIdRef newLeaf; + if (obj instanceof RevTag) { + do { + obj = rw.parseAny(((RevTag) obj).getObject()); + } while (obj instanceof RevTag); + + newLeaf = new ObjectIdRef.PeeledTag(leaf.getStorage(), leaf + .getName(), leaf.getObjectId(), obj.copy()); + } else { + newLeaf = new ObjectIdRef.PeeledNonTag(leaf.getStorage(), leaf + .getName(), leaf.getObjectId()); + } + + // Try to remember this peeling in the cache, so we don't have to do + // it again in the future, but only if the reference is unchanged. + if (leaf.getStorage().isLoose()) { + RefList curList = looseRefs.get(); + int idx = curList.find(leaf.getName()); + if (0 <= idx && curList.get(idx) == leaf) { + LooseRef asPeeled = ((LooseRef) leaf).peel(newLeaf); + RefList newList = curList.set(idx, asPeeled); + looseRefs.compareAndSet(curList, newList); + } + } + + return recreate(ref, newLeaf); + } + + private static Ref recreate(final Ref old, final ObjectIdRef leaf) { + if (old.isSymbolic()) { + Ref dst = recreate(old.getTarget(), leaf); + return new SymbolicRef(old.getName(), dst); + } + return leaf; + } + + void storedSymbolicRef(RefDirectoryUpdate u, long modified, String target) { + putLooseRef(newSymbolicRef(modified, u.getRef().getName(), target)); + fireRefsChanged(); + } + + public RefDirectoryUpdate newUpdate(String name, boolean detach) + throws IOException { + final RefList packed = getPackedRefs(); + Ref ref = readRef(name, packed); + if (ref != null) + ref = resolve(ref, 0, null, null, packed); + if (ref == null) + ref = new ObjectIdRef.Unpeeled(NEW, name, null); + else if (detach && ref.isSymbolic()) + ref = new ObjectIdRef.Unpeeled(LOOSE, name, ref.getObjectId()); + return new RefDirectoryUpdate(this, ref); + } + + @Override + public RefDirectoryRename newRename(String fromName, String toName) + throws IOException { + RefDirectoryUpdate from = newUpdate(fromName, false); + RefDirectoryUpdate to = newUpdate(toName, false); + return new RefDirectoryRename(from, to); + } + + void stored(RefDirectoryUpdate update, long modified) { + final ObjectId target = update.getNewObjectId().copy(); + final Ref leaf = update.getRef().getLeaf(); + putLooseRef(new LooseUnpeeled(modified, leaf.getName(), target)); + } + + private void putLooseRef(LooseRef ref) { + RefList cList, nList; + do { + cList = looseRefs.get(); + nList = cList.put(ref); + } while (!looseRefs.compareAndSet(cList, nList)); + modCnt.incrementAndGet(); + fireRefsChanged(); + } + + void delete(RefDirectoryUpdate update) throws IOException { + Ref dst = update.getRef().getLeaf(); + String name = dst.getName(); + + // Write the packed-refs file using an atomic update. We might + // wind up reading it twice, before and after the lock, to ensure + // we don't miss an edit made externally. + final PackedRefList packed = getPackedRefs(); + if (packed.contains(name)) { + LockFile lck = new LockFile(packedRefsFile); + if (!lck.lock()) + throw new IOException("Cannot lock " + packedRefsFile); + try { + PackedRefList cur = readPackedRefs(0, 0); + int idx = cur.find(name); + if (0 <= idx) + commitPackedRefs(lck, cur.remove(idx), packed); + } finally { + lck.unlock(); + } + } + + RefList curLoose, newLoose; + do { + curLoose = looseRefs.get(); + int idx = curLoose.find(name); + if (idx < 0) + break; + newLoose = curLoose.remove(idx); + } while (!looseRefs.compareAndSet(curLoose, newLoose)); + + int levels = levelsIn(name) - 2; + delete(logFor(name), levels); + if (dst.getStorage().isLoose()) { + update.unlock(); + delete(fileFor(name), levels); + } + + modCnt.incrementAndGet(); + fireRefsChanged(); + } + + void log(final RefUpdate update, final String msg, final boolean deref) + throws IOException { + final ObjectId oldId = update.getOldObjectId(); + final ObjectId newId = update.getNewObjectId(); + final Ref ref = update.getRef(); + + PersonIdent ident = update.getRefLogIdent(); + if (ident == null) + ident = new PersonIdent(parent); + else + ident = new PersonIdent(ident); + + final StringBuilder r = new StringBuilder(); + r.append(ObjectId.toString(oldId)); + r.append(' '); + r.append(ObjectId.toString(newId)); + r.append(' '); + r.append(ident.toExternalString()); + r.append('\t'); + r.append(msg); + r.append('\n'); + final byte[] rec = encode(r.toString()); + + if (deref && ref.isSymbolic()) { + log(ref.getName(), rec); + log(ref.getLeaf().getName(), rec); + } else { + log(ref.getName(), rec); + } + } + + private void log(final String refName, final byte[] rec) throws IOException { + final File log = logFor(refName); + final boolean write; + if (isLogAllRefUpdates() && shouldAutoCreateLog(refName)) + write = true; + else if (log.isFile()) + write = true; + else + write = false; + + if (write) { + FileOutputStream out; + try { + out = new FileOutputStream(log, true); + } catch (FileNotFoundException err) { + final File dir = log.getParentFile(); + if (dir.exists()) + throw err; + if (!dir.mkdirs() && !dir.isDirectory()) + throw new IOException("Cannot create directory " + dir); + out = new FileOutputStream(log, true); + } + try { + out.write(rec); + } finally { + out.close(); + } + } + } + + private boolean isLogAllRefUpdates() { + return parent.getConfig().getCore().isLogAllRefUpdates(); + } + + private boolean shouldAutoCreateLog(final String refName) { + return refName.equals(HEAD) // + || refName.startsWith(R_HEADS) // + || refName.startsWith(R_REMOTES); + } + + private Ref resolve(final Ref ref, int depth, String prefix, + RefList loose, RefList packed) throws IOException { + if (ref.isSymbolic()) { + Ref dst = ref.getTarget(); + + if (MAX_SYMBOLIC_REF_DEPTH <= depth) + return null; // claim it doesn't exist + + // If the cached value can be assumed to be current due to a + // recent scan of the loose directory, use it. + if (loose != null && dst.getName().startsWith(prefix)) { + int idx; + if (0 <= (idx = loose.find(dst.getName()))) + dst = loose.get(idx); + else if (0 <= (idx = packed.find(dst.getName()))) + dst = packed.get(idx); + else + return ref; + } else { + dst = readRef(dst.getName(), packed); + if (dst == null) + return ref; + } + + dst = resolve(dst, depth + 1, prefix, loose, packed); + if (dst == null) + return null; + return new SymbolicRef(ref.getName(), dst); + } + return ref; + } + + private PackedRefList getPackedRefs() throws IOException { + long size = packedRefsFile.length(); + long mtime = size != 0 ? packedRefsFile.lastModified() : 0; + + final PackedRefList curList = packedRefs.get(); + if (size == curList.lastSize && mtime == curList.lastModified) + return curList; + + final PackedRefList newList = readPackedRefs(size, mtime); + if (packedRefs.compareAndSet(curList, newList)) + modCnt.incrementAndGet(); + return newList; + } + + private PackedRefList readPackedRefs(long size, long mtime) + throws IOException { + final BufferedReader br; + try { + br = new BufferedReader(new InputStreamReader(new FileInputStream( + packedRefsFile), CHARSET)); + } catch (FileNotFoundException noPackedRefs) { + // Ignore it and leave the new list empty. + return PackedRefList.NO_PACKED_REFS; + } + try { + return new PackedRefList(parsePackedRefs(br), size, mtime); + } finally { + br.close(); + } + } + + private RefList parsePackedRefs(final BufferedReader br) + throws IOException { + RefList.Builder all = new RefList.Builder(); + Ref last = null; + boolean peeled = false; + boolean needSort = false; + + String p; + while ((p = br.readLine()) != null) { + if (p.charAt(0) == '#') { + if (p.startsWith(PACKED_REFS_HEADER)) { + p = p.substring(PACKED_REFS_HEADER.length()); + peeled = p.contains(PACKED_REFS_PEELED); + } + continue; + } + + if (p.charAt(0) == '^') { + if (last == null) + throw new IOException("Peeled line before ref."); + + ObjectId id = ObjectId.fromString(p.substring(1)); + last = new ObjectIdRef.PeeledTag(PACKED, last.getName(), last + .getObjectId(), id); + all.set(all.size() - 1, last); + continue; + } + + int sp = p.indexOf(' '); + ObjectId id = ObjectId.fromString(p.substring(0, sp)); + String name = copy(p, sp + 1, p.length()); + ObjectIdRef cur; + if (peeled) + cur = new ObjectIdRef.PeeledNonTag(PACKED, name, id); + else + cur = new ObjectIdRef.Unpeeled(PACKED, name, id); + if (last != null && RefComparator.compareTo(last, cur) > 0) + needSort = true; + all.add(cur); + last = cur; + } + + if (needSort) + all.sort(); + return all.toRefList(); + } + + private static String copy(final String src, final int off, final int end) { + // Don't use substring since it could leave a reference to the much + // larger existing string. Force construction of a full new object. + return new StringBuilder(end - off).append(src, off, end).toString(); + } + + private void commitPackedRefs(final LockFile lck, final RefList refs, + final PackedRefList oldPackedList) throws IOException { + new RefWriter(refs) { + @Override + protected void writeFile(String name, byte[] content) + throws IOException { + lck.setNeedStatInformation(true); + try { + lck.write(content); + } catch (IOException ioe) { + throw new ObjectWritingException("Unable to write " + name, + ioe); + } + try { + lck.waitForStatChange(); + } catch (InterruptedException e) { + lck.unlock(); + throw new ObjectWritingException("Interrupted writing " + + name); + } + if (!lck.commit()) + throw new ObjectWritingException("Unable to write " + name); + + packedRefs.compareAndSet(oldPackedList, new PackedRefList(refs, + content.length, lck.getCommitLastModified())); + } + }.writePackedRefs(); + } + + private Ref readRef(String name, RefList packed) throws IOException { + final RefList curList = looseRefs.get(); + final int idx = curList.find(name); + if (0 <= idx) { + final LooseRef o = curList.get(idx); + final LooseRef n = scanRef(o, name); + if (n == null) { + if (looseRefs.compareAndSet(curList, curList.remove(idx))) + modCnt.incrementAndGet(); + return packed.get(name); + } + + if (o == n) + return n; + if (looseRefs.compareAndSet(curList, curList.set(idx, n))) + modCnt.incrementAndGet(); + return n; + } + + final LooseRef n = scanRef(null, name); + if (n == null) + return packed.get(name); + if (looseRefs.compareAndSet(curList, curList.add(idx, n))) + modCnt.incrementAndGet(); + return n; + } + + private LooseRef scanRef(LooseRef ref, String name) throws IOException { + final File path = fileFor(name); + final long modified = path.lastModified(); + + if (ref != null) { + if (ref.getLastModified() == modified) + return ref; + name = ref.getName(); + } else if (modified == 0) + return null; + + final byte[] buf; + try { + buf = IO.readFully(path, 4096); + } catch (FileNotFoundException noFile) { + return null; // doesn't exist; not a reference. + } + + int n = buf.length; + if (n == 0) + return null; // empty file; not a reference. + + if (isSymRef(buf, n)) { + // trim trailing whitespace + while (0 < n && Character.isWhitespace(buf[n - 1])) + n--; + if (n < 6) { + String content = RawParseUtils.decode(buf, 0, n); + throw new IOException("Not a ref: " + name + ": " + content); + } + final String target = RawParseUtils.decode(buf, 5, n); + return newSymbolicRef(modified, name, target); + } + + if (n < OBJECT_ID_STRING_LENGTH) + return null; // impossibly short object identifier; not a reference. + + final ObjectId id; + try { + id = ObjectId.fromString(buf, 0); + } catch (IllegalArgumentException notRef) { + while (0 < n && Character.isWhitespace(buf[n - 1])) + n--; + String content = RawParseUtils.decode(buf, 0, n); + throw new IOException("Not a ref: " + name + ": " + content); + } + return new LooseUnpeeled(modified, name, id); + } + + private static boolean isSymRef(final byte[] buf, int n) { + if (n < 6) + return false; + return /**/buf[0] == 'r' // + && buf[1] == 'e' // + && buf[2] == 'f' // + && buf[3] == ':' // + && buf[4] == ' '; + } + + /** If the parent should fire listeners, fires them. */ + private void fireRefsChanged() { + final int last = lastNotifiedModCnt.get(); + final int curr = modCnt.get(); + if (last != curr && lastNotifiedModCnt.compareAndSet(last, curr)) + parent.fireRefsChanged(); + } + + /** + * Create a reference update to write a temporary reference. + * + * @return an update for a new temporary reference. + * @throws IOException + * a temporary name cannot be allocated. + */ + RefDirectoryUpdate newTemporaryUpdate() throws IOException { + File tmp = File.createTempFile("renamed_", "_ref", refsDir); + String name = Constants.R_REFS + tmp.getName(); + Ref ref = new ObjectIdRef.Unpeeled(NEW, name, null); + return new RefDirectoryUpdate(this, ref); + } + + /** + * Locate the file on disk for a single reference name. + * + * @param name + * name of the ref, relative to the Git repository top level + * directory (so typically starts with refs/). + * @return the loose file location. + */ + File fileFor(String name) { + if (name.startsWith(R_REFS)) { + name = name.substring(R_REFS.length()); + return new File(refsDir, name); + } + return new File(gitDir, name); + } + + /** + * Locate the log file on disk for a single reference name. + * + * @param name + * name of the ref, relative to the Git repository top level + * directory (so typically starts with refs/). + * @return the log file location. + */ + File logFor(String name) { + if (name.startsWith(R_REFS)) { + name = name.substring(R_REFS.length()); + return new File(logsRefsDir, name); + } + return new File(logsDir, name); + } + + static int levelsIn(final String name) { + int count = 0; + for (int p = name.indexOf('/'); p >= 0; p = name.indexOf('/', p + 1)) + count++; + return count; + } + + static void delete(final File file, final int depth) throws IOException { + if (!file.delete() && file.isFile()) + throw new IOException("File cannot be deleted: " + file); + + File dir = file.getParentFile(); + for (int i = 0; i < depth; ++i) { + if (!dir.delete()) + break; // ignore problem here + dir = dir.getParentFile(); + } + } + + private static class PackedRefList extends RefList { + static final PackedRefList NO_PACKED_REFS = new PackedRefList(RefList + .emptyList(), 0, 0); + + /** Last length of the packed-refs file when we read it. */ + final long lastSize; + + /** Last modified time of the packed-refs file when we read it. */ + final long lastModified; + + PackedRefList(RefList src, long size, long mtime) { + super(src); + lastSize = size; + lastModified = mtime; + } + } + + private static LooseSymbolicRef newSymbolicRef(long lastModified, + String name, String target) { + Ref dst = new ObjectIdRef.Unpeeled(NEW, target, null); + return new LooseSymbolicRef(lastModified, name, dst); + } + + private static interface LooseRef extends Ref { + long getLastModified(); + + LooseRef peel(ObjectIdRef newLeaf); + } + + private final static class LoosePeeledTag extends ObjectIdRef.PeeledTag + implements LooseRef { + private final long lastModified; + + LoosePeeledTag(long mtime, String refName, ObjectId id, ObjectId p) { + super(LOOSE, refName, id, p); + this.lastModified = mtime; + } + + public long getLastModified() { + return lastModified; + } + + public LooseRef peel(ObjectIdRef newLeaf) { + return this; + } + } + + private final static class LooseNonTag extends ObjectIdRef.PeeledNonTag + implements LooseRef { + private final long lastModified; + + LooseNonTag(long mtime, String refName, ObjectId id) { + super(LOOSE, refName, id); + this.lastModified = mtime; + } + + public long getLastModified() { + return lastModified; + } + + public LooseRef peel(ObjectIdRef newLeaf) { + return this; + } + } + + private final static class LooseUnpeeled extends ObjectIdRef.Unpeeled + implements LooseRef { + private final long lastModified; + + LooseUnpeeled(long mtime, String refName, ObjectId id) { + super(LOOSE, refName, id); + this.lastModified = mtime; + } + + public long getLastModified() { + return lastModified; + } + + public LooseRef peel(ObjectIdRef newLeaf) { + if (newLeaf.getPeeledObjectId() != null) + return new LoosePeeledTag(lastModified, getName(), + getObjectId(), newLeaf.getPeeledObjectId()); + else + return new LooseNonTag(lastModified, getName(), getObjectId()); + } + } + + private final static class LooseSymbolicRef extends SymbolicRef implements + LooseRef { + private final long lastModified; + + LooseSymbolicRef(long mtime, String refName, Ref target) { + super(refName, target); + this.lastModified = mtime; + } + + public long getLastModified() { + return lastModified; + } + + public LooseRef peel(ObjectIdRef newLeaf) { + // We should never try to peel the symbolic references. + throw new UnsupportedOperationException(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryRename.java new file mode 100644 index 000000000..fec00d932 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryRename.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2010, Google Inc. + * Copyright (C) 2009, Robin Rosenberg + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Rename any reference stored by {@link RefDirectory}. + *

+ * This class works by first renaming the source reference to a temporary name, + * then renaming the temporary name to the final destination reference. + *

+ * This strategy permits switching a reference like {@code refs/heads/foo}, + * which is a file, to {@code refs/heads/foo/bar}, which is stored inside a + * directory that happens to match the source name. + */ +class RefDirectoryRename extends RefRename { + private final RefDirectory refdb; + + /** + * The value of the source reference at the start of the rename. + *

+ * At the end of the rename the destination reference must have this same + * value, otherwise we have a concurrent update and the rename must fail + * without making any changes. + */ + private ObjectId objId; + + /** True if HEAD must be moved to the destination reference. */ + private boolean updateHEAD; + + /** A reference we backup {@link #objId} into during the rename. */ + private RefDirectoryUpdate tmp; + + RefDirectoryRename(RefDirectoryUpdate src, RefDirectoryUpdate dst) { + super(src, dst); + refdb = src.getRefDatabase(); + } + + @Override + protected Result doRename() throws IOException { + if (source.getRef().isSymbolic()) + return Result.IO_FAILURE; // not supported + + final RevWalk rw = new RevWalk(refdb.getRepository()); + objId = source.getOldObjectId(); + updateHEAD = needToUpdateHEAD(); + tmp = refdb.newTemporaryUpdate(); + try { + // First backup the source so its never unreachable. + tmp.setNewObjectId(objId); + tmp.setForceUpdate(true); + tmp.disableRefLog(); + switch (tmp.update(rw)) { + case NEW: + case FORCED: + case NO_CHANGE: + break; + default: + return tmp.getResult(); + } + + // Save the source's log under the temporary name, we must do + // this before we delete the source, otherwise we lose the log. + if (!renameLog(source, tmp)) + return Result.IO_FAILURE; + + // If HEAD has to be updated, link it now to destination. + // We have to link before we delete, otherwise the delete + // fails because its the current branch. + RefUpdate dst = destination; + if (updateHEAD) { + if (!linkHEAD(destination)) { + renameLog(tmp, source); + return Result.LOCK_FAILURE; + } + + // Replace the update operation so HEAD will log the rename. + dst = refdb.newUpdate(Constants.HEAD, false); + dst.setRefLogIdent(destination.getRefLogIdent()); + dst.setRefLogMessage(destination.getRefLogMessage(), false); + } + + // Delete the source name so its path is free for replacement. + source.setExpectedOldObjectId(objId); + source.setForceUpdate(true); + source.disableRefLog(); + if (source.delete(rw) != Result.FORCED) { + renameLog(tmp, source); + if (updateHEAD) + linkHEAD(source); + return source.getResult(); + } + + // Move the log to the destination. + if (!renameLog(tmp, destination)) { + renameLog(tmp, source); + source.setExpectedOldObjectId(ObjectId.zeroId()); + source.setNewObjectId(objId); + source.update(rw); + if (updateHEAD) + linkHEAD(source); + return Result.IO_FAILURE; + } + + // Create the destination, logging the rename during the creation. + dst.setExpectedOldObjectId(ObjectId.zeroId()); + dst.setNewObjectId(objId); + if (dst.update(rw) != Result.NEW) { + // If we didn't create the destination we have to undo + // our work. Put the log back and restore source. + if (renameLog(destination, tmp)) + renameLog(tmp, source); + source.setExpectedOldObjectId(ObjectId.zeroId()); + source.setNewObjectId(objId); + source.update(rw); + if (updateHEAD) + linkHEAD(source); + return dst.getResult(); + } + + return Result.RENAMED; + } finally { + // Always try to free the temporary name. + try { + refdb.delete(tmp); + } catch (IOException err) { + refdb.fileFor(tmp.getName()).delete(); + } + } + } + + private boolean renameLog(RefUpdate src, RefUpdate dst) { + File srcLog = refdb.logFor(src.getName()); + File dstLog = refdb.logFor(dst.getName()); + + if (!srcLog.exists()) + return true; + + if (!rename(srcLog, dstLog)) + return false; + + try { + final int levels = RefDirectory.levelsIn(src.getName()) - 2; + RefDirectory.delete(srcLog, levels); + return true; + } catch (IOException e) { + rename(dstLog, srcLog); + return false; + } + } + + private static boolean rename(File src, File dst) { + if (src.renameTo(dst)) + return true; + + File dir = dst.getParentFile(); + if ((dir.exists() || !dir.mkdirs()) && !dir.isDirectory()) + return false; + return src.renameTo(dst); + } + + private boolean linkHEAD(RefUpdate target) { + try { + RefUpdate u = refdb.newUpdate(Constants.HEAD, false); + u.disableRefLog(); + switch (u.link(target.getName())) { + case NEW: + case FORCED: + case NO_CHANGE: + return true; + default: + return false; + } + } catch (IOException e) { + return false; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryUpdate.java new file mode 100644 index 000000000..447be104a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryUpdate.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2009-2010, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.lib; + +import static org.eclipse.jgit.lib.Constants.encode; + +import java.io.IOException; + +/** Updates any reference stored by {@link RefDirectory}. */ +class RefDirectoryUpdate extends RefUpdate { + private final RefDirectory database; + + private LockFile lock; + + RefDirectoryUpdate(final RefDirectory r, final Ref ref) { + super(ref); + database = r; + } + + @Override + protected RefDirectory getRefDatabase() { + return database; + } + + @Override + protected Repository getRepository() { + return database.getRepository(); + } + + @Override + protected boolean tryLock(boolean deref) throws IOException { + Ref dst = getRef(); + if (deref) + dst = dst.getLeaf(); + String name = dst.getName(); + lock = new LockFile(database.fileFor(name)); + if (lock.lock()) { + dst = database.getRef(name); + setOldObjectId(dst != null ? dst.getObjectId() : null); + return true; + } else { + return false; + } + } + + @Override + protected void unlock() { + if (lock != null) { + lock.unlock(); + lock = null; + } + } + + @Override + protected Result doUpdate(final Result status) throws IOException { + lock.setNeedStatInformation(true); + lock.write(getNewObjectId()); + + String msg = getRefLogMessage(); + if (msg != null) { + if (isRefLogIncludingResult()) { + String strResult = toResultString(status); + if (strResult != null) { + if (msg.length() > 0) + msg = msg + ": " + strResult; + else + msg = strResult; + } + } + database.log(this, msg, true); + } + if (!lock.commit()) + return Result.LOCK_FAILURE; + database.stored(this, lock.getCommitLastModified()); + return status; + } + + private String toResultString(final Result status) { + switch (status) { + case FORCED: + return "forced-update"; + case FAST_FORWARD: + return "fast forward"; + case NEW: + return "created"; + default: + return null; + } + } + + @Override + protected Result doDelete(final Result status) throws IOException { + if (getRef().getLeaf().getStorage() != Ref.Storage.NEW) + database.delete(this); + return status; + } + + @Override + protected Result doLink(final String target) throws IOException { + lock.setNeedStatInformation(true); + lock.write(encode(RefDirectory.SYMREF + target + '\n')); + + String msg = getRefLogMessage(); + if (msg != null) + database.log(this, msg, false); + if (!lock.commit()) + return Result.LOCK_FAILURE; + database.storedSymbolicRef(this, lock.getCommitLastModified(), target); + + if (getRef().getStorage() == Ref.Storage.NEW) + return Result.NEW; + return Result.FORCED; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefLogWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefLogWriter.java deleted file mode 100644 index 304968557..000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefLogWriter.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (C) 2009, Christian Halstrick - * Copyright (C) 2007, Dave Watson - * Copyright (C) 2009, Google Inc. - * Copyright (C) 2007-2009, Robin Rosenberg - * Copyright (C) 2006, Shawn O. Pearce - * and other copyright owners as documented in the project's IP log. - * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.eclipse.jgit.lib; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; - -/** - * Utility class to work with reflog files - * - * @author Dave Watson - */ -public class RefLogWriter { - static void append(final RefUpdate u, final String msg) throws IOException { - final ObjectId oldId = u.getOldObjectId(); - final ObjectId newId = u.getNewObjectId(); - final Repository db = u.getRepository(); - final PersonIdent ident = u.getRefLogIdent(); - - appendOneRecord(oldId, newId, ident, msg, db, u.getName()); - if (!u.getName().equals(u.getOrigName())) - appendOneRecord(oldId, newId, ident, msg, db, u.getOrigName()); - } - - static void append(RefRename refRename, String logName, String msg) throws IOException { - final ObjectId id = refRename.getObjectId(); - final Repository db = refRename.getRepository(); - final PersonIdent ident = refRename.getRefLogIdent(); - appendOneRecord(id, id, ident, msg, db, logName); - } - - static void renameTo(final Repository db, final RefUpdate from, - final RefUpdate to) throws IOException { - final File logdir = new File(db.getDirectory(), Constants.LOGS); - final File reflogFrom = new File(logdir, from.getName()); - if (!reflogFrom.exists()) - return; - final File reflogTo = new File(logdir, to.getName()); - final File reflogToDir = reflogTo.getParentFile(); - File tmp = new File(logdir, "tmp-renamed-log.." + Thread.currentThread().getId()); - if (!reflogFrom.renameTo(tmp)) { - throw new IOException("Cannot rename " + reflogFrom + " to (" + tmp - + ")" + reflogTo); - } - RefUpdate.deleteEmptyDir(reflogFrom, RefUpdate.count(from.getName(), - '/')); - if (!reflogToDir.exists() && !reflogToDir.mkdirs()) { - throw new IOException("Cannot create directory " + reflogToDir); - } - if (!tmp.renameTo(reflogTo)) { - throw new IOException("Cannot rename (" + tmp + ")" + reflogFrom - + " to " + reflogTo); - } - } - - private static void appendOneRecord(final ObjectId oldId, - final ObjectId newId, PersonIdent ident, final String msg, - final Repository db, final String refName) throws IOException { - if (ident == null) - ident = new PersonIdent(db); - else - ident = new PersonIdent(ident); - - final StringBuilder r = new StringBuilder(); - r.append(ObjectId.toString(oldId)); - r.append(' '); - r.append(ObjectId.toString(newId)); - r.append(' '); - r.append(ident.toExternalString()); - r.append('\t'); - r.append(msg); - r.append('\n'); - - final byte[] rec = Constants.encode(r.toString()); - final File logdir = new File(db.getDirectory(), Constants.LOGS); - final File reflog = new File(logdir, refName); - if (reflog.exists() || db.getConfig().getCore().isLogAllRefUpdates()) { - final File refdir = reflog.getParentFile(); - - if (!refdir.exists() && !refdir.mkdirs()) - throw new IOException("Cannot create directory " + refdir); - - final FileOutputStream out = new FileOutputStream(reflog, true); - try { - out.write(rec); - } finally { - out.close(); - } - } - } - - /** - * Writes reflog entry for ref specified by refName - * - * @param repo - * repository to use - * @param oldCommit - * previous commit - * @param commit - * new commit - * @param message - * reflog message - * @param refName - * full ref name - * @throws IOException - * @deprecated rely upon {@link RefUpdate}'s automatic logging instead. - */ - public static void writeReflog(Repository repo, ObjectId oldCommit, - ObjectId commit, String message, String refName) throws IOException { - appendOneRecord(oldCommit, commit, null, message, repo, refName); - } -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefRename.java index ba7f208f3..2ff5c614b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefRename.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefRename.java @@ -1,6 +1,8 @@ /* + * Copyright (C) 2009-2010, Google Inc. * Copyright (C) 2009, Robin Rosenberg * Copyright (C) 2009, Robin Rosenberg + * Copyright (C) 2008, Shawn O. Pearce * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -49,25 +51,96 @@ import org.eclipse.jgit.lib.RefUpdate.Result; /** - * A RefUpdate combination for renaming a ref + * A RefUpdate combination for renaming a reference. + *

+ * If the source reference is currently pointed to by {@code HEAD}, then the + * HEAD symbolic reference is updated to point to the new destination. */ -public class RefRename { - private RefUpdate newToUpdate; +public abstract class RefRename { + /** Update operation to read and delete the source reference. */ + protected final RefUpdate source; - private RefUpdate oldFromDelete; + /** Update operation to create/overwrite the destination reference. */ + protected final RefUpdate destination; - private Result renameResult = Result.NOT_ATTEMPTED; + private Result result = Result.NOT_ATTEMPTED; - RefRename(final RefUpdate toUpdate, final RefUpdate fromUpdate) { - newToUpdate = toUpdate; - oldFromDelete = fromUpdate; + /** + * Initialize a new rename operation. + * + * @param src + * operation to read and delete the source. + * @param dst + * operation to create (or overwrite) the destination. + */ + protected RefRename(final RefUpdate src, final RefUpdate dst) { + source = src; + destination = dst; + + Repository repo = destination.getRepository(); + String cmd = ""; + if (source.getName().startsWith(Constants.R_HEADS) + && destination.getName().startsWith(Constants.R_HEADS)) + cmd = "Branch: "; + setRefLogMessage(cmd + "renamed " + + repo.shortenRefName(source.getName()) + " to " + + repo.shortenRefName(destination.getName())); + } + + /** @return identity of the user making the change in the reflog. */ + public PersonIdent getRefLogIdent() { + return destination.getRefLogIdent(); + } + + /** + * Set the identity of the user appearing in the reflog. + *

+ * The timestamp portion of the identity is ignored. A new identity with the + * current timestamp will be created automatically when the rename occurs + * and the log record is written. + * + * @param pi + * identity of the user. If null the identity will be + * automatically determined based on the repository + * configuration. + */ + public void setRefLogIdent(final PersonIdent pi) { + destination.setRefLogIdent(pi); + } + + /** + * Get the message to include in the reflog. + * + * @return message the caller wants to include in the reflog; null if the + * rename should not be logged. + */ + public String getRefLogMessage() { + return destination.getRefLogMessage(); + } + + /** + * Set the message to include in the reflog. + * + * @param msg + * the message to describe this change. + */ + public void setRefLogMessage(final String msg) { + if (msg == null) + disableRefLog(); + else + destination.setRefLogMessage(msg, false); + } + + /** Don't record this rename in the ref's associated reflog. */ + public void disableRefLog() { + destination.setRefLogMessage("", false); } /** * @return result of rename operation */ public Result getResult() { - return renameResult; + return result; } /** @@ -75,101 +148,33 @@ public Result getResult() { * @throws IOException */ public Result rename() throws IOException { - Ref oldRef = oldFromDelete.db.readRef(Constants.HEAD); - boolean renameHEADtoo = oldRef != null - && oldRef.getName().equals(oldFromDelete.getName()); - Repository db = oldFromDelete.getRepository(); try { - RefLogWriter.renameTo(db, oldFromDelete, - newToUpdate); - newToUpdate.setRefLogMessage(null, false); - String tmpRefName = "RENAMED-REF.." + Thread.currentThread().getId(); - RefUpdate tmpUpdateRef = db.updateRef(tmpRefName); - if (renameHEADtoo) { - try { - oldFromDelete.db.link(Constants.HEAD, tmpRefName); - } catch (IOException e) { - RefLogWriter.renameTo(db, - newToUpdate, oldFromDelete); - return renameResult = Result.LOCK_FAILURE; - } - } - tmpUpdateRef.setNewObjectId(oldFromDelete.getOldObjectId()); - tmpUpdateRef.setForceUpdate(true); - Result update = tmpUpdateRef.update(); - if (update != Result.FORCED && update != Result.NEW && update != Result.NO_CHANGE) { - RefLogWriter.renameTo(db, - newToUpdate, oldFromDelete); - if (renameHEADtoo) { - oldFromDelete.db.link(Constants.HEAD, oldFromDelete.getName()); - } - return renameResult = update; - } - - oldFromDelete.setExpectedOldObjectId(oldFromDelete.getOldObjectId()); - oldFromDelete.setForceUpdate(true); - Result delete = oldFromDelete.delete(); - if (delete != Result.FORCED) { - if (db.getRef( - oldFromDelete.getName()) != null) { - RefLogWriter.renameTo(db, - newToUpdate, oldFromDelete); - if (renameHEADtoo) { - oldFromDelete.db.link(Constants.HEAD, oldFromDelete - .getName()); - } - } - return renameResult = delete; - } - - newToUpdate.setNewObjectId(tmpUpdateRef.getNewObjectId()); - Result updateResult = newToUpdate.update(); - if (updateResult != Result.NEW) { - RefLogWriter.renameTo(db, newToUpdate, oldFromDelete); - if (renameHEADtoo) { - oldFromDelete.db.link(Constants.HEAD, oldFromDelete.getName()); - } - oldFromDelete.setExpectedOldObjectId(null); - oldFromDelete.setNewObjectId(oldFromDelete.getOldObjectId()); - oldFromDelete.setForceUpdate(true); - oldFromDelete.setRefLogMessage(null, false); - Result undelete = oldFromDelete.update(); - if (undelete != Result.NEW && undelete != Result.LOCK_FAILURE) - return renameResult = Result.IO_FAILURE; - return renameResult = Result.LOCK_FAILURE; - } - - if (renameHEADtoo) { - oldFromDelete.db.link(Constants.HEAD, newToUpdate.getName()); - } else { - db.fireRefsMaybeChanged(); - } - RefLogWriter.append(this, newToUpdate.getName(), "Branch: renamed " - + db.shortenRefName(oldFromDelete.getName()) + " to " - + db.shortenRefName(newToUpdate.getName())); - if (renameHEADtoo) - RefLogWriter.append(this, Constants.HEAD, "Branch: renamed " - + db.shortenRefName(oldFromDelete.getName()) + " to " - + db.shortenRefName(newToUpdate.getName())); - return renameResult = Result.RENAMED; - } catch (RuntimeException e) { - throw e; + result = doRename(); + return result; + } catch (IOException err) { + result = Result.IO_FAILURE; + throw err; } } - ObjectId getObjectId() { - return oldFromDelete.getOldObjectId(); - } + /** + * @return the result of the rename operation. + * @throws IOException + */ + protected abstract Result doRename() throws IOException; - Repository getRepository() { - return oldFromDelete.getRepository(); - } - - PersonIdent getRefLogIdent() { - return newToUpdate.getRefLogIdent(); - } - - String getToName() { - return newToUpdate.getName(); + /** + * @return true if the {@code Constants#HEAD} reference needs to be linked + * to the new destination name. + * @throws IOException + * the current value of {@code HEAD} cannot be read. + */ + protected boolean needToUpdateHEAD() throws IOException { + Ref head = source.getRefDatabase().getRef(Constants.HEAD); + if (head.isSymbolic()) { + head = head.getTarget(); + return head.getName().equals(source.getName()); + } + return false; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java index 856eb1519..553266284 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java @@ -1,6 +1,5 @@ /* - * Copyright (C) 2008, Charles O'Farrell - * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008-2010, Google Inc. * Copyright (C) 2008, Shawn O. Pearce * and other copyright owners as documented in the project's IP log. * @@ -45,19 +44,17 @@ package org.eclipse.jgit.lib; -import java.io.File; import java.io.IOException; import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.lib.Ref.Storage; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevWalk; /** - * Updates any locally stored ref. + * Creates, updates or deletes any reference. */ -public class RefUpdate { +public abstract class RefUpdate { /** Status of an update request. */ public static enum Result { /** The ref update/delete has not been attempted by the caller. */ @@ -142,12 +139,6 @@ public static enum Result { RENAMED } - /** Repository the ref is stored in. */ - final RefDatabase db; - - /** Location of the loose file holding the value of this ref. */ - final File looseFile; - /** New value the caller wants this ref to have. */ private ObjectId newValue; @@ -170,22 +161,63 @@ public static enum Result { private ObjectId expValue; /** Result of the update operation. */ - Result result = Result.NOT_ATTEMPTED; + private Result result = Result.NOT_ATTEMPTED; private final Ref ref; - RefUpdate(final RefDatabase r, final Ref ref, final File f) { - db = r; + RefUpdate(final Ref ref) { this.ref = ref; oldValue = ref.getObjectId(); - looseFile = f; refLogMessage = ""; } - /** @return the repository the updated ref resides in */ - public Repository getRepository() { - return db.getRepository(); - } + /** @return the reference database this update modifies. */ + protected abstract RefDatabase getRefDatabase(); + + /** @return the repository storing the database's objects. */ + protected abstract Repository getRepository(); + + /** + * Try to acquire the lock on the reference. + *

+ * If the locking was successful the implementor must set the current + * identity value by calling {@link #setOldObjectId(ObjectId)}. + * + * @param deref + * true if the lock should be taken against the leaf level + * reference; false if it should be taken exactly against the + * current reference. + * @return true if the lock was acquired and the reference is likely + * protected from concurrent modification; false if it failed. + * @throws IOException + * the lock couldn't be taken due to an unexpected storage + * failure, and not because of a concurrent update. + */ + protected abstract boolean tryLock(boolean deref) throws IOException; + + /** Releases the lock taken by {@link #tryLock} if it succeeded. */ + protected abstract void unlock(); + + /** + * @param desiredResult + * @return {@code result} + * @throws IOException + */ + protected abstract Result doUpdate(Result desiredResult) throws IOException; + + /** + * @param desiredResult + * @return {@code result} + * @throws IOException + */ + protected abstract Result doDelete(Result desiredResult) throws IOException; + + /** + * @param target + * @return {@link Result#NEW} on success. + * @throws IOException + */ + protected abstract Result doLink(String target) throws IOException; /** * Get the name of the ref this update will operate on. @@ -193,16 +225,12 @@ public Repository getRepository() { * @return name of underlying ref. */ public String getName() { - return ref.getName(); + return getRef().getName(); } - /** - * Get the requested name of the ref thit update will operate on - * - * @return original (requested) name of the underlying ref. - */ - public String getOrigName() { - return ref.getOrigName(); + /** @return the reference this update will create or modify. */ + public Ref getRef() { + return ref; } /** @@ -295,12 +323,17 @@ public String getRefLogMessage() { return refLogMessage; } + /** @return {@code true} if the ref log message should show the result. */ + protected boolean isRefLogIncludingResult() { + return refLogIncludeResult; + } + /** * Set the message to include in the reflog. * * @param msg - * the message to describe this change. It may be null - * if appendStatus is null in order not to append to the reflog + * the message to describe this change. It may be null if + * appendStatus is null in order not to append to the reflog * @param appendStatus * true if the status of the ref change (fast-forward or * forced-update) should be appended to the user supplied @@ -339,6 +372,16 @@ public ObjectId getOldObjectId() { return oldValue; } + /** + * Set the old value of the ref. + * + * @param old + * the old value. + */ + protected void setOldObjectId(ObjectId old) { + oldValue = old; + } + /** * Get the status of this update. *

@@ -378,7 +421,7 @@ public Result forceUpdate() throws IOException { * This is the same as: * *

-	 * return update(new RevWalk(repository));
+	 * return update(new RevWalk(getRepository()));
 	 * 
* * @return the result status of the update. @@ -386,7 +429,7 @@ public Result forceUpdate() throws IOException { * an unexpected IO error occurred while writing changes. */ public Result update() throws IOException { - return update(new RevWalk(db.getRepository())); + return update(new RevWalk(getRepository())); } /** @@ -404,7 +447,14 @@ public Result update() throws IOException { public Result update(final RevWalk walk) throws IOException { requireCanDoUpdate(); try { - return result = updateImpl(walk, new UpdateStore()); + return result = updateImpl(walk, new Store() { + @Override + Result execute(Result status) throws IOException { + if (status == Result.NO_CHANGE) + return status; + return doUpdate(status); + } + }); } catch (IOException x) { result = Result.IO_FAILURE; throw x; @@ -417,14 +467,14 @@ public Result update(final RevWalk walk) throws IOException { * This is the same as: * *
-	 * return delete(new RevWalk(repository));
+	 * return delete(new RevWalk(getRepository()));
 	 * 
* * @return the result status of the delete. * @throws IOException */ public Result delete() throws IOException { - return delete(new RevWalk(db.getRepository())); + return delete(new RevWalk(getRepository())); } /** @@ -437,33 +487,83 @@ public Result delete() throws IOException { * @throws IOException */ public Result delete(final RevWalk walk) throws IOException { - if (getName().startsWith(Constants.R_HEADS)) { - final Ref head = db.readRef(Constants.HEAD); - if (head != null && getName().equals(head.getName())) - return result = Result.REJECTED_CURRENT_BRANCH; + final String myName = getRef().getLeaf().getName(); + if (myName.startsWith(Constants.R_HEADS)) { + Ref head = getRefDatabase().getRef(Constants.HEAD); + while (head.isSymbolic()) { + head = head.getTarget(); + if (myName.equals(head.getName())) + return result = Result.REJECTED_CURRENT_BRANCH; + } } try { - return result = updateImpl(walk, new DeleteStore()); + return result = updateImpl(walk, new Store() { + @Override + Result execute(Result status) throws IOException { + return doDelete(status); + } + }); } catch (IOException x) { result = Result.IO_FAILURE; throw x; } } + /** + * Replace this reference with a symbolic reference to another reference. + *

+ * This exact reference (not its traversed leaf) is replaced with a symbolic + * reference to the requested name. + * + * @param target + * name of the new target for this reference. The new target name + * must be absolute, so it must begin with {@code refs/}. + * @return {@link Result#NEW} or {@link Result#FORCED} on success. + * @throws IOException + */ + public Result link(String target) throws IOException { + if (!target.startsWith(Constants.R_REFS)) + throw new IllegalArgumentException("Not " + Constants.R_REFS); + if (getRefDatabase().isNameConflicting(getName())) + return Result.LOCK_FAILURE; + try { + if (!tryLock(false)) + return Result.LOCK_FAILURE; + + final Ref old = getRefDatabase().getRef(getName()); + if (old != null && old.isSymbolic()) { + final Ref dst = old.getTarget(); + if (target.equals(dst.getName())) + return result = Result.NO_CHANGE; + } + + if (old != null && old.getObjectId() != null) + setOldObjectId(old.getObjectId()); + + final Ref dst = getRefDatabase().getRef(target); + if (dst != null && dst.getObjectId() != null) + setNewObjectId(dst.getObjectId()); + + return result = doLink(target); + } catch (IOException x) { + result = Result.IO_FAILURE; + throw x; + } finally { + unlock(); + } + } + private Result updateImpl(final RevWalk walk, final Store store) throws IOException { - final LockFile lock; RevObject newObj; RevObject oldObj; - if (isNameConflicting()) - return Result.LOCK_FAILURE; - lock = new LockFile(looseFile); - if (!lock.lock()) + if (getRefDatabase().isNameConflicting(getName())) return Result.LOCK_FAILURE; try { - oldValue = db.idOf(getName()); + if (!tryLock(true)) + return Result.LOCK_FAILURE; if (expValue != null) { final ObjectId o; o = oldValue != null ? oldValue : ObjectId.zeroId(); @@ -471,41 +571,26 @@ private Result updateImpl(final RevWalk walk, final Store store) return Result.LOCK_FAILURE; } if (oldValue == null) - return store.store(lock, Result.NEW); + return store.execute(Result.NEW); newObj = safeParse(walk, newValue); oldObj = safeParse(walk, oldValue); if (newObj == oldObj) - return store.store(lock, Result.NO_CHANGE); + return store.execute(Result.NO_CHANGE); if (newObj instanceof RevCommit && oldObj instanceof RevCommit) { if (walk.isMergedInto((RevCommit) oldObj, (RevCommit) newObj)) - return store.store(lock, Result.FAST_FORWARD); + return store.execute(Result.FAST_FORWARD); } if (isForceUpdate()) - return store.store(lock, Result.FORCED); + return store.execute(Result.FORCED); return Result.REJECTED; } finally { - lock.unlock(); + unlock(); } } - private boolean isNameConflicting() throws IOException { - final String myName = getName(); - final int lastSlash = myName.lastIndexOf('/'); - if (lastSlash > 0) - if (db.getRepository().getRef(myName.substring(0, lastSlash)) != null) - return true; - - final String rName = myName + "/"; - for (Ref r : db.getAllRefs().values()) { - if (r.getName().startsWith(rName)) - return true; - } - return false; - } - private static RevObject safeParse(final RevWalk rw, final AnyObjectId id) throws IOException { try { @@ -520,123 +605,11 @@ private static RevObject safeParse(final RevWalk rw, final AnyObjectId id) } } - private Result updateStore(final LockFile lock, final Result status) - throws IOException { - if (status == Result.NO_CHANGE) - return status; - lock.setNeedStatInformation(true); - lock.write(newValue); - String msg = getRefLogMessage(); - if (msg != null) { - if (refLogIncludeResult) { - String strResult = toResultString(status); - if (strResult != null) { - if (msg.length() > 0) - msg = msg + ": " + strResult; - else - msg = strResult; - } - } - RefLogWriter.append(this, msg); - } - if (!lock.commit()) - return Result.LOCK_FAILURE; - db.stored(this.ref.getOrigName(), ref.getName(), newValue, lock.getCommitLastModified()); - return status; - } - - private static String toResultString(final Result status) { - switch (status) { - case FORCED: - return "forced-update"; - case FAST_FORWARD: - return "fast forward"; - case NEW: - return "created"; - default: - return null; - } - } - /** * Handle the abstraction of storing a ref update. This is because both * updating and deleting of a ref have merge testing in common. */ private abstract class Store { - abstract Result store(final LockFile lock, final Result status) - throws IOException; - } - - class UpdateStore extends Store { - - @Override - Result store(final LockFile lock, final Result status) - throws IOException { - return updateStore(lock, status); - } - } - - class DeleteStore extends Store { - - @Override - Result store(LockFile lock, Result status) throws IOException { - Storage storage = ref.getStorage(); - if (storage == Storage.NEW) - return status; - if (storage.isPacked()) - db.removePackedRef(ref.getName()); - - final int levels = count(ref.getName(), '/') - 2; - - // Delete logs _before_ unlocking - final File gitDir = db.getRepository().getDirectory(); - final File logDir = new File(gitDir, Constants.LOGS); - deleteFileAndEmptyDir(new File(logDir, ref.getName()), levels); - - // We have to unlock before (maybe) deleting the parent directories - lock.unlock(); - if (storage.isLoose()) - deleteFileAndEmptyDir(looseFile, levels); - db.uncacheRef(ref.getName()); - return status; - } - - private void deleteFileAndEmptyDir(final File file, final int depth) - throws IOException { - if (file.isFile()) { - if (!file.delete()) - throw new IOException("File cannot be deleted: " + file); - File dir = file.getParentFile(); - for (int i = 0; i < depth; ++i) { - if (!dir.delete()) - break; // ignore problem here - dir = dir.getParentFile(); - } - } - } - } - - UpdateStore newUpdateStore() { - return new UpdateStore(); - } - - DeleteStore newDeleteStore() { - return new DeleteStore(); - } - - static void deleteEmptyDir(File dir, int depth) { - for (; depth > 0 && dir != null; depth--) { - if (dir.exists() && !dir.delete()) - break; - dir = dir.getParentFile(); - } - } - - static int count(final String s, final char c) { - int count = 0; - for (int p = s.indexOf(c); p >= 0; p = s.indexOf(c, p + 1)) { - count++; - } - return count; + abstract Result execute(Result status) throws IOException; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java index c48449933..6b0c7b3a9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java @@ -49,6 +49,10 @@ import java.io.IOException; import java.io.StringWriter; import java.util.Collection; +import java.util.Map; + +import org.eclipse.jgit.util.RefList; +import org.eclipse.jgit.util.RefMap; /** * Writes out refs to the {@link Constants#INFO_REFS} and @@ -70,6 +74,22 @@ public RefWriter(Collection refs) { this.refs = RefComparator.sort(refs); } + /** + * @param refs + * the complete set of references. This should have been computed + * by applying updates to the advertised refs already discovered. + */ + public RefWriter(Map refs) { + if (refs instanceof RefMap) + this.refs = refs.values(); + else + this.refs = RefComparator.sort(refs.values()); + } + + RefWriter(RefList list) { + this.refs = list.asList(); + } + /** * Rebuild the {@link Constants#INFO_REFS}. *

@@ -85,7 +105,7 @@ public void writeInfoRefs() throws IOException { final StringWriter w = new StringWriter(); final char[] tmp = new char[Constants.OBJECT_ID_STRING_LENGTH]; for (final Ref r : refs) { - if (Constants.HEAD.equals(r.getOrigName())) { + if (Constants.HEAD.equals(r.getName())) { // Historically HEAD has never been published through // the INFO_REFS file. This is a mistake, but its the // way things are. @@ -121,19 +141,18 @@ public void writeInfoRefs() throws IOException { */ public void writePackedRefs() throws IOException { boolean peeled = false; - for (final Ref r : refs) { - if (r.getStorage() != Ref.Storage.PACKED) - continue; - if (r.getPeeledObjectId() != null) + if (r.getStorage().isPacked() && r.isPeeled()) { peeled = true; + break; + } } final StringWriter w = new StringWriter(); if (peeled) { - w.write("# pack-refs with:"); + w.write(RefDirectory.PACKED_REFS_HEADER); if (peeled) - w.write(" peeled"); + w.write(RefDirectory.PACKED_REFS_PEELED); w.write('\n'); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java index 91a97740e..45eda052a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2007, Dave Watson + * Copyright (C) 2008-2010, Google Inc. * Copyright (C) 2006-2010, Robin Rosenberg * Copyright (C) 2006-2008, Shawn O. Pearce * and other copyright owners as documented in the project's IP log. @@ -45,10 +46,7 @@ package org.eclipse.jgit.lib; -import java.io.BufferedReader; import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -223,7 +221,7 @@ public Repository(final File d, final File workTree, final File objectDir, } } - refs = new RefDatabase(this); + refs = new RefDirectory(this); if (objectDir != null) objectDatabase = new ObjectDirectory(FS.resolve(objectDir, ""), alternateObjectDir); @@ -278,8 +276,10 @@ public void create(boolean bare) throws IOException { objectDatabase.create(); new File(gitDir, "branches").mkdir(); - final String master = Constants.R_HEADS + Constants.MASTER; - refs.link(Constants.HEAD, master); + + RefUpdate head = updateRef(Constants.HEAD); + head.disableRefLog(); + head.link(Constants.R_HEADS + Constants.MASTER); cfg.setInt("core", null, "repositoryformatversion", 0); cfg.setBoolean("core", null, "filemode", true); @@ -310,6 +310,11 @@ public ObjectDatabase getObjectDatabase() { return objectDatabase; } + /** @return the reference database which stores the reference namespace. */ + public RefDatabase getRefDatabase() { + return refs; + } + /** * @return the configuration of this repository */ @@ -591,7 +596,7 @@ public Tag mapTag(final String refName, final ObjectId id) throws IOException { * to the base ref, as the symbolic ref could not be read. */ public RefUpdate updateRef(final String ref) throws IOException { - return refs.newUpdate(ref); + return updateRef(ref, false); } /** @@ -862,7 +867,7 @@ else if (item.equals("")) { private ObjectId resolveSimple(final String revstr) throws IOException { if (ObjectId.isId(revstr)) return ObjectId.fromString(revstr); - final Ref r = refs.readRef(revstr); + final Ref r = refs.getRef(revstr); return r != null ? r.getObjectId() : null; } @@ -875,8 +880,10 @@ public void incrementOpen() { * Close all resources used by this repository */ public void close() { - if (useCnt.decrementAndGet() == 0) + if (useCnt.decrementAndGet() == 0) { objectDatabase.close(); + refs.close(); + } } /** @@ -894,70 +901,53 @@ public void openPack(final File pack, final File idx) throws IOException { objectDatabase.openPack(pack, idx); } - /** - * Writes a symref (e.g. HEAD) to disk - * - * @param name symref name - * @param target pointed to ref - * @throws IOException - */ - public void writeSymref(final String name, final String target) - throws IOException { - refs.link(name, target); - } - public String toString() { return "Repository[" + getDirectory() + "]"; } /** - * @return name of current branch + * Get the name of the reference that {@code HEAD} points to. + *

+ * This is essentially the same as doing: + * + *

+	 * return getRef(Constants.HEAD).getTarget().getName()
+	 * 
+ * + * Except when HEAD is detached, in which case this method returns the + * current ObjectId in hexadecimal string format. + * + * @return name of current branch (for example {@code refs/heads/master}) or + * an ObjectId in hex format if the current branch is detached. * @throws IOException */ public String getFullBranch() throws IOException { - final File ptr = new File(getDirectory(),Constants.HEAD); - final BufferedReader br = new BufferedReader(new FileReader(ptr)); - String ref; - try { - ref = br.readLine(); - } finally { - br.close(); - } - if (ref.startsWith("ref: ")) - ref = ref.substring(5); - return ref; + Ref head = getRef(Constants.HEAD); + if (head == null) + return null; + if (head.isSymbolic()) + return head.getTarget().getName(); + if (head.getObjectId() != null) + return head.getObjectId().name(); + return null; } /** - * @return name of current branch. + * Get the short name of the current branch that {@code HEAD} points to. + *

+ * This is essentially the same as {@link #getFullBranch()}, except the + * leading prefix {@code refs/heads/} is removed from the reference before + * it is returned to the caller. + * + * @return name of current branch (for example {@code master}), or an + * ObjectId in hex format if the current branch is detached. * @throws IOException */ public String getBranch() throws IOException { - try { - final File ptr = new File(getDirectory(), Constants.HEAD); - final BufferedReader br = new BufferedReader(new FileReader(ptr)); - String ref; - try { - ref = br.readLine(); - } finally { - br.close(); - } - if (ref.startsWith("ref: ")) - ref = ref.substring(5); - if (ref.startsWith("refs/heads/")) - ref = ref.substring(11); - return ref; - } catch (FileNotFoundException e) { - final File ptr = new File(getDirectory(),"head-name"); - final BufferedReader br = new BufferedReader(new FileReader(ptr)); - String ref; - try { - ref = br.readLine(); - } finally { - br.close(); - } - return ref; - } + String name = getFullBranch(); + if (name != null) + return shortenRefName(name); + return name; } /** @@ -971,26 +961,35 @@ public String getBranch() throws IOException { * @throws IOException */ public Ref getRef(final String name) throws IOException { - return refs.readRef(name); + return refs.getRef(name); } /** - * @return all known refs (heads, tags, remotes). + * @return mutable map of all known refs (heads, tags, remotes). */ public Map getAllRefs() { - return refs.getAllRefs(); + try { + return refs.getRefs(RefDatabase.ALL); + } catch (IOException e) { + return new HashMap(); + } } /** - * @return all tags; key is short tag name ("v1.0") and value of the entry - * contains the ref with the full tag name ("refs/tags/v1.0"). + * @return mutable map of all tags; key is short tag name ("v1.0") and value + * of the entry contains the ref with the full tag name + * ("refs/tags/v1.0"). */ public Map getTags() { - return refs.getTags(); + try { + return refs.getRefs(Constants.R_TAGS); + } catch (IOException e) { + return new HashMap(); + } } /** - * Peel a possibly unpeeled ref and updates it. + * Peel a possibly unpeeled reference to an annotated tag. *

* If the ref cannot be peeled (as it does not refer to an annotated tag) * the peeled id stays null, but {@link Ref#isPeeled()} will be true. @@ -1003,7 +1002,14 @@ public Map getTags() { * (or null). */ public Ref peel(final Ref ref) { - return refs.peel(ref); + try { + return refs.peel(ref); + } catch (IOException e) { + // Historical accident; if the reference cannot be peeled due + // to some sort of repository access problem we claim that the + // same as if the reference was not an annotated tag. + return ref; + } } /** @@ -1013,8 +1019,7 @@ public Map> getAllRefsByPeeledObjectId() { Map allRefs = getAllRefs(); Map> ret = new HashMap>(allRefs.size()); for (Ref ref : allRefs.values()) { - if (!ref.isPeeled()) - ref = peel(ref); + ref = peel(ref); AnyObjectId target = ref.getPeeledObjectId(); if (target == null) target = ref.getObjectId(); @@ -1033,11 +1038,6 @@ public Map> getAllRefsByPeeledObjectId() { return ret; } - /** Clean up stale caches */ - public void refreshFromDisk() { - refs.clearCache(); - } - /** * @return a representation of the index associated with this repo * @throws IOException @@ -1115,7 +1115,7 @@ public static boolean isValidRefName(final String refName) { final int len = refName.length(); if (len == 0) return false; - if (refName.endsWith(".lock")) + if (refName.endsWith(LockFile.SUFFIX)) return false; int components = 1; @@ -1233,20 +1233,17 @@ public static void removeAnyRepositoryChangedListener(final RepositoryListener l allListeners.remove(l); } - void fireRefsMaybeChanged() { - if (refs.lastRefModification != refs.lastNotifiedRefModification) { - refs.lastNotifiedRefModification = refs.lastRefModification; - final RefsChangedEvent event = new RefsChangedEvent(this); - List all; - synchronized (listeners) { - all = new ArrayList(listeners); - } - synchronized (allListeners) { - all.addAll(allListeners); - } - for (final RepositoryListener l : all) { - l.refsChanged(event); - } + void fireRefsChanged() { + final RefsChangedEvent event = new RefsChangedEvent(this); + List all; + synchronized (listeners) { + all = new ArrayList(listeners); + } + synchronized (allListeners) { + all.addAll(allListeners); + } + for (final RepositoryListener l : all) { + l.refsChanged(event); } } @@ -1298,7 +1295,7 @@ public String shortenRefName(String refName) { public ReflogReader getReflogReader(String refName) throws IOException { Ref ref = getRef(refName); if (ref != null) - return new ReflogReader(this, ref.getOrigName()); + return new ReflogReader(this, ref.getName()); return null; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java new file mode 100644 index 000000000..d169d61f1 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.lib; + +/** + * A reference that indirectly points at another {@link Ref}. + *

+ * A symbolic reference always derives its current value from the target + * reference. + */ +public class SymbolicRef implements Ref { + private final String name; + + private final Ref target; + + /** + * Create a new ref pairing. + * + * @param refName + * name of this ref. + * @param target + * the ref we reference and derive our value from. + */ + public SymbolicRef(String refName, Ref target) { + this.name = refName; + this.target = target; + } + + public String getName() { + return name; + } + + public boolean isSymbolic() { + return true; + } + + public Ref getLeaf() { + Ref dst = getTarget(); + while (dst.isSymbolic()) + dst = dst.getTarget(); + return dst; + } + + public Ref getTarget() { + return target; + } + + public ObjectId getObjectId() { + return getLeaf().getObjectId(); + } + + public Storage getStorage() { + return Storage.LOOSE; + } + + public ObjectId getPeeledObjectId() { + return getLeaf().getPeeledObjectId(); + } + + public boolean isPeeled() { + return getLeaf().isPeeled(); + } + + @Override + public String toString() { + StringBuilder r = new StringBuilder(); + r.append("SymbolicRef["); + Ref cur = this; + while (cur.isSymbolic()) { + r.append(cur.getName()); + r.append(" -> "); + cur = cur.getTarget(); + } + r.append(cur.getName()); + r.append('='); + r.append(ObjectId.toString(cur.getObjectId())); + r.append("]"); + return r.toString(); + } +} 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 90634d207..74c27a7cd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java @@ -61,6 +61,7 @@ import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.errors.TransportException; 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.util.io.InterruptTimer; @@ -209,11 +210,11 @@ private void readAdvertisedRefsImpl() throws IOException { if (prior.getPeeledObjectId() != null) throw duplicateAdvertisement(name + "^{}"); - avail.put(name, new Ref(Ref.Storage.NETWORK, name, prior - .getObjectId(), id, true)); + avail.put(name, new ObjectIdRef.PeeledTag( + Ref.Storage.NETWORK, name, prior.getObjectId(), id)); } else { - final Ref prior; - prior = avail.put(name, new Ref(Ref.Storage.NETWORK, name, id)); + final Ref prior = avail.put(name, new ObjectIdRef.PeeledNonTag( + Ref.Storage.NETWORK, name, id)); if (prior != null) throw duplicateAdvertisement(name); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java index 681963546..c788244f7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2009, Constantine Plotnikov - * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008-2010, Google Inc. * Copyright (C) 2008-2009, Robin Rosenberg * Copyright (C) 2009, Sasa Zivkov * Copyright (C) 2008, Shawn O. Pearce @@ -65,6 +65,7 @@ import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.PackLock; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; @@ -140,8 +141,8 @@ private void readBundleV2() throws IOException { final String name = line.substring(41, line.length()); final ObjectId id = ObjectId.fromString(line.substring(0, 40)); - final Ref prior = avail.put(name, new Ref(Ref.Storage.NETWORK, - name, id)); + final Ref prior = avail.put(name, new ObjectIdRef.Unpeeled( + Ref.Storage.NETWORK, name, id)); if (prior != null) throw duplicateAdvertisement(name); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java index f85c22ddb..15bdf9618 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008-2010, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -52,7 +52,6 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -590,10 +589,10 @@ public void sendAdvertisedRefs(final RefAdvertiser adv) throws IOException { adv.advertiseCapability(CAPABILITY_REPORT_STATUS); if (allowOfsDelta) adv.advertiseCapability(CAPABILITY_OFS_DELTA); - refs = new HashMap(db.getAllRefs()); + refs = db.getAllRefs(); final Ref head = refs.remove(Constants.HEAD); - adv.send(refs.values()); - if (head != null && head.getName().equals(head.getOrigName())) + adv.send(refs); + if (!head.isSymbolic()) adv.advertiseHave(head.getObjectId()); adv.includeAdditionalHaves(); if (adv.isEmpty()) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java index 953fae918..694a2e0f1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008-2010, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -44,9 +44,10 @@ package org.eclipse.jgit.transport; import java.io.IOException; -import java.util.Collection; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; +import java.util.SortedMap; import org.eclipse.jgit.lib.AlternateRepositoryDatabase; import org.eclipse.jgit.lib.AnyObjectId; @@ -59,6 +60,7 @@ import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevTag; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.util.RefMap; /** Support for the start of {@link UploadPack} and {@link ReceivePack}. */ public abstract class RefAdvertiser { @@ -122,7 +124,7 @@ public void init(final RevWalk protoWalk, final RevFlag advertisedFlag) { *

* This method must be invoked prior to any of the following: *

    - *
  • {@link #send(Collection)} + *
  • {@link #send(Map)} *
  • {@link #advertiseHave(AnyObjectId)} *
  • {@link #includeAdditionalHaves()} *
@@ -140,7 +142,7 @@ public void setDerefTags(final boolean deref) { *

* This method must be invoked prior to any of the following: *

    - *
  • {@link #send(Collection)} + *
  • {@link #send(Map)} *
  • {@link #advertiseHave(AnyObjectId)} *
  • {@link #includeAdditionalHaves()} *
@@ -160,23 +162,30 @@ public void advertiseCapability(String name) { * * @param refs * zero or more refs to format for the client. The collection is - * copied and sorted before display and therefore may appear in - * any order. + * sorted before display if necessary, and therefore may appear + * in any order. * @throws IOException * the underlying output stream failed to write out an * advertisement record. */ - public void send(final Collection refs) throws IOException { - for (final Ref r : RefComparator.sort(refs)) { + public void send(final Map refs) throws IOException { + for (final Ref r : getSortedRefs(refs)) { final RevObject obj = parseAnyOrNull(r.getObjectId()); if (obj != null) { - advertiseAny(obj, r.getOrigName()); + advertiseAny(obj, r.getName()); if (derefTags && obj instanceof RevTag) - advertiseTag((RevTag) obj, r.getOrigName() + "^{}"); + advertiseTag((RevTag) obj, r.getName() + "^{}"); } } } + private Iterable getSortedRefs(Map all) { + if (all instanceof RefMap + || (all instanceof SortedMap && ((SortedMap) all).comparator() == null)) + return all.values(); + return RefComparator.sort(all.values()); + } + /** * Advertise one object is available using the magic {@code .have}. *

diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java index 6a1a17f60..a3fd1ceae 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java @@ -61,9 +61,11 @@ import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.SymbolicRef; import org.eclipse.jgit.lib.Ref.Storage; import org.eclipse.jgit.util.FS; @@ -305,16 +307,15 @@ private Ref readRef(final TreeMap avail, final String rn) if (r == null) r = readRef(avail, target); if (r == null) - return null; - r = new Ref(r.getStorage(), rn, r.getObjectId(), r - .getPeeledObjectId(), r.isPeeled()); + r = new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null); + r = new SymbolicRef(rn, r); avail.put(r.getName(), r); return r; } if (ObjectId.isId(s)) { - final Ref r = new Ref(loose(avail.get(rn)), rn, ObjectId - .fromString(s)); + final Ref r = new ObjectIdRef.Unpeeled(loose(avail.get(rn)), + rn, ObjectId.fromString(s)); avail.put(r.getName(), r); return r; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java index c521e8051..f041765b8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java @@ -80,9 +80,12 @@ import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDirectory; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.SymbolicRef; import org.eclipse.jgit.lib.Config.SectionParser; import org.eclipse.jgit.util.HttpSupport; import org.eclipse.jgit.util.IO; @@ -240,16 +243,17 @@ private FetchConnection newDumbConnection(InputStream in) br = toBufferedReader(openInputStream(conn)); try { String line = br.readLine(); - if (line != null && line.startsWith("ref: ")) { - Ref src = refs.get(line.substring(5)); - if (src != null) { - refs.put(Constants.HEAD, new Ref( - Ref.Storage.NETWORK, Constants.HEAD, src - .getName(), src.getObjectId())); - } + if (line != null && line.startsWith(RefDirectory.SYMREF)) { + String target = line.substring(RefDirectory.SYMREF.length()); + Ref r = refs.get(target); + if (r == null) + r = new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null); + r = new SymbolicRef(Constants.HEAD, r); + refs.put(r.getName(), r); } else if (line != null && ObjectId.isId(line)) { - refs.put(Constants.HEAD, new Ref(Ref.Storage.NETWORK, - Constants.HEAD, ObjectId.fromString(line))); + Ref r = new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, + Constants.HEAD, ObjectId.fromString(line)); + refs.put(r.getName(), r); } } finally { br.close(); @@ -527,10 +531,11 @@ Map readAdvertisedImpl(final BufferedReader br) if (prior.getPeeledObjectId() != null) throw duplicateAdvertisement(name + "^{}"); - avail.put(name, new Ref(Ref.Storage.NETWORK, name, prior - .getObjectId(), id, true)); + avail.put(name, new ObjectIdRef.PeeledTag( + Ref.Storage.NETWORK, name, + prior.getObjectId(), id)); } else { - final Ref prior = avail.put(name, new Ref( + Ref prior = avail.put(name, new ObjectIdRef.PeeledNonTag( Ref.Storage.NETWORK, name, id)); if (prior != null) throw duplicateAdvertisement(name); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java index 617cca890..2a196b51d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java @@ -59,9 +59,11 @@ import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.SymbolicRef; import org.eclipse.jgit.lib.Ref.Storage; import com.jcraft.jsch.Channel; @@ -389,21 +391,20 @@ private Ref readRef(final TreeMap avail, throw new TransportException("Empty ref: " + name); if (line.startsWith("ref: ")) { - final String p = line.substring("ref: ".length()); - Ref r = readRef(avail, ROOT_DIR + p, p); + final String target = line.substring("ref: ".length()); + Ref r = avail.get(target); if (r == null) - r = avail.get(p); - if (r != null) { - r = new Ref(loose(r), name, r.getObjectId(), r - .getPeeledObjectId(), true); - avail.put(name, r); - } + r = readRef(avail, ROOT_DIR + target, target); + if (r == null) + r = new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null); + r = new SymbolicRef(name, r); + avail.put(r.getName(), r); return r; } if (ObjectId.isId(line)) { - final Ref r = new Ref(loose(avail.get(name)), name, ObjectId - .fromString(line)); + final Ref r = new ObjectIdRef.Unpeeled(loose(avail.get(name)), + name, ObjectId.fromString(line)); avail.put(r.getName(), r); return r; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java index 6b81bc492..57bb2adbf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008-2010, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -330,7 +330,7 @@ public void sendAdvertisedRefs(final RefAdvertiser adv) throws IOException { adv.advertiseCapability(OPTION_NO_PROGRESS); adv.setDerefTags(true); refs = db.getAllRefs(); - adv.send(refs.values()); + adv.send(refs); adv.end(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java index 56f73c50b..88b7ca438 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java @@ -58,6 +58,7 @@ import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.PackWriter; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; @@ -324,8 +325,8 @@ private void deleteCommand(final RemoteRefUpdate u) { private void updateCommand(final RemoteRefUpdate u) { try { dest.writeRef(u.getRemoteName(), u.getNewObjectId()); - newRefs.put(u.getRemoteName(), new Ref(Storage.LOOSE, u - .getRemoteName(), u.getNewObjectId())); + newRefs.put(u.getRemoteName(), new ObjectIdRef.Unpeeled( + Storage.LOOSE, u.getRemoteName(), u.getNewObjectId())); u.setStatus(Status.OK); } catch (IOException e) { u.setStatus(Status.REJECTED_OTHER_REASON); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkRemoteObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkRemoteObjectDatabase.java index 6a010fb4e..2aa644ce8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkRemoteObjectDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkRemoteObjectDatabase.java @@ -57,8 +57,10 @@ import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDirectory; import org.eclipse.jgit.util.IO; /** @@ -433,18 +435,24 @@ protected void readPackedRefs(final Map avail) private void readPackedRefsImpl(final Map avail, final BufferedReader br) throws IOException { Ref last = null; + boolean peeled = false; for (;;) { String line = br.readLine(); if (line == null) break; - if (line.charAt(0) == '#') + if (line.charAt(0) == '#') { + if (line.startsWith(RefDirectory.PACKED_REFS_HEADER)) { + line = line.substring(RefDirectory.PACKED_REFS_HEADER.length()); + peeled = line.contains(RefDirectory.PACKED_REFS_PEELED); + } continue; + } if (line.charAt(0) == '^') { if (last == null) throw new TransportException("Peeled line before ref."); final ObjectId id = ObjectId.fromString(line.substring(1)); - last = new Ref(Ref.Storage.PACKED, last.getName(), last - .getObjectId(), id, true); + last = new ObjectIdRef.PeeledTag(Ref.Storage.PACKED, last + .getName(), last.getObjectId(), id); avail.put(last.getName(), last); continue; } @@ -454,7 +462,10 @@ private void readPackedRefsImpl(final Map avail, throw new TransportException("Unrecognized ref: " + line); final ObjectId id = ObjectId.fromString(line.substring(0, sp)); final String name = line.substring(sp + 1); - last = new Ref(Ref.Storage.PACKED, name, id); + if (peeled) + last = new ObjectIdRef.PeeledNonTag(Ref.Storage.PACKED, name, id); + else + last = new ObjectIdRef.Unpeeled(Ref.Storage.PACKED, name, id); avail.put(last.getName(), last); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java new file mode 100644 index 000000000..45b065999 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java @@ -0,0 +1,438 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.util; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefComparator; + +/** + * Specialized variant of an ArrayList to support a {@code RefDatabase}. + *

+ * This list is a hybrid of a Map<String,Ref> and of a List<Ref>. It + * tracks reference instances by name by keeping them sorted and performing + * binary search to locate an entry. Lookup time is O(log N), but addition and + * removal is O(N + log N) due to the list expansion or contraction costs. + *

+ * This list type is copy-on-write. Mutation methods return a new copy of the + * list, leaving {@code this} unmodified. As a result we cannot easily implement + * the {@link java.util.List} interface contract. + * + * @param + * the type of reference being stored in the collection. + */ +public class RefList implements Iterable { + private static final RefList EMPTY = new RefList(new Ref[0], 0); + + /** + * @return an empty unmodifiable reference list. + * @param + * the type of reference being stored in the collection. + */ + @SuppressWarnings("unchecked") + public static RefList emptyList() { + return (RefList) EMPTY; + } + + private final Ref[] list; + + private final int cnt; + + RefList(Ref[] list, int cnt) { + this.list = list; + this.cnt = cnt; + } + + /** + * Initialize this list to use the same backing array as another list. + * + * @param src + * the source list. + */ + protected RefList(RefList src) { + this.list = src.list; + this.cnt = src.cnt; + } + + public Iterator iterator() { + return new Iterator() { + private int idx; + + public boolean hasNext() { + return idx < cnt; + } + + public Ref next() { + if (idx < cnt) + return list[idx++]; + throw new NoSuchElementException(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + /** @return this cast as an immutable, standard {@link java.util.List}. */ + public final List asList() { + final List r = Arrays.asList(list).subList(0, cnt); + return Collections.unmodifiableList(r); + } + + /** @return number of items in this list. */ + public final int size() { + return cnt; + } + + /** @return true if the size of this list is 0. */ + public final boolean isEmpty() { + return cnt == 0; + } + + /** + * Locate an entry by name. + * + * @param name + * the name of the reference to find. + * @return the index the reference is at. If the entry is not present + * returns a negative value. The insertion position for the given + * name can be computed from {@code -(index + 1)}. + */ + public final int find(String name) { + int high = cnt; + if (high == 0) + return -1; + int low = 0; + do { + final int mid = (low + high) >>> 1; + final int cmp = RefComparator.compareTo(list[mid], name); + if (cmp < 0) + low = mid + 1; + else if (cmp == 0) + return mid; + else + high = mid; + } while (low < high); + return -(low + 1); + } + + /** + * Determine if a reference is present. + * + * @param name + * name of the reference to find. + * @return true if the reference is present; false if it is not. + */ + public final boolean contains(String name) { + return 0 <= find(name); + } + + /** + * Get a reference object by name. + * + * @param name + * the name of the reference. + * @return the reference object; null if it does not exist in this list. + */ + public final T get(String name) { + int idx = find(name); + return 0 <= idx ? get(idx) : null; + } + + /** + * Get the reference at a particular index. + * + * @param idx + * the index to obtain. Must be {@code 0 <= idx < size()}. + * @return the reference value, never null. + */ + @SuppressWarnings("unchecked") + public final T get(int idx) { + return (T) list[idx]; + } + + /** + * Obtain a builder initialized with the first {@code n} elements. + *

+ * Copies the first {@code n} elements from this list into a new builder, + * which can be used by the caller to add additional elements. + * + * @param n + * the number of elements to copy. + * @return a new builder with the first {@code n} elements already added. + */ + public final Builder copy(int n) { + Builder r = new Builder(Math.max(16, n)); + r.addAll(list, 0, n); + return r; + } + + /** + * Obtain a new copy of the list after changing one element. + *

+ * This list instance is not affected by the replacement. Because this + * method copies the entire list, it runs in O(N) time. + * + * @param idx + * index of the element to change. + * @param ref + * the new value, must not be null. + * @return copy of this list, after replacing {@code idx} with {@code ref} . + */ + public final RefList set(int idx, T ref) { + Ref[] newList = new Ref[cnt]; + System.arraycopy(list, 0, newList, 0, cnt); + newList[idx] = ref; + return new RefList(newList, cnt); + } + + /** + * Add an item at a specific index. + *

+ * This list instance is not affected by the addition. Because this method + * copies the entire list, it runs in O(N) time. + * + * @param idx + * position to add the item at. If negative the method assumes it + * was a direct return value from {@link #find(String)} and will + * adjust it to the correct position. + * @param ref + * the new reference to insert. + * @return copy of this list, after making space for and adding {@code ref}. + */ + public final RefList add(int idx, T ref) { + if (idx < 0) + idx = -(idx + 1); + + Ref[] newList = new Ref[cnt + 1]; + if (0 < idx) + System.arraycopy(list, 0, newList, 0, idx); + newList[idx] = ref; + if (idx < cnt) + System.arraycopy(list, idx, newList, idx + 1, cnt - idx); + return new RefList(newList, cnt + 1); + } + + /** + * Remove an item at a specific index. + *

+ * This list instance is not affected by the addition. Because this method + * copies the entire list, it runs in O(N) time. + * + * @param idx + * position to remove the item from. + * @return copy of this list, after making removing the item at {@code idx}. + */ + public final RefList remove(int idx) { + if (cnt == 1) + return emptyList(); + Ref[] newList = new Ref[cnt - 1]; + if (0 < idx) + System.arraycopy(list, 0, newList, 0, idx); + if (idx + 1 < cnt) + System.arraycopy(list, idx + 1, newList, idx, cnt - (idx + 1)); + return new RefList(newList, cnt - 1); + } + + /** + * Store a reference, adding or replacing as necessary. + *

+ * This list instance is not affected by the store. The correct position is + * determined, and the item is added if missing, or replaced if existing. + * Because this method copies the entire list, it runs in O(N + log N) time. + * + * @param ref + * the reference to store. + * @return copy of this list, after performing the addition or replacement. + */ + public final RefList put(T ref) { + int idx = find(ref.getName()); + if (0 <= idx) + return set(idx, ref); + return add(idx, ref); + } + + @Override + public String toString() { + StringBuilder r = new StringBuilder(); + r.append('['); + if (cnt > 0) { + r.append(list[0]); + for (int i = 1; i < cnt; i++) { + r.append(", "); + r.append(list[i]); + } + } + r.append(']'); + return r.toString(); + } + + /** + * Builder to facilitate fast construction of an immutable RefList. + * + * @param + * type of reference being stored. + */ + public static class Builder { + private Ref[] list; + + private int size; + + /** Create an empty list ready for items to be added. */ + public Builder() { + this(16); + } + + /** + * Create an empty list with at least the specified capacity. + * + * @param capacity + * the new capacity. + */ + public Builder(int capacity) { + list = new Ref[capacity]; + } + + /** @return number of items in this builder's internal collection. */ + public int size() { + return size; + } + + /** + * Get the reference at a particular index. + * + * @param idx + * the index to obtain. Must be {@code 0 <= idx < size()}. + * @return the reference value, never null. + */ + @SuppressWarnings("unchecked") + public T get(int idx) { + return (T) list[idx]; + } + + /** + * Remove an item at a specific index. + * + * @param idx + * position to remove the item from. + */ + public void remove(int idx) { + System.arraycopy(list, idx + 1, list, idx, size - (idx + 1)); + size--; + } + + /** + * Add the reference to the end of the array. + *

+ * References must be added in sort order, or the array must be sorted + * after additions are complete using {@link #sort()}. + * + * @param ref + */ + public void add(T ref) { + if (list.length == size) { + Ref[] n = new Ref[size * 2]; + System.arraycopy(list, 0, n, 0, size); + list = n; + } + list[size++] = ref; + } + + /** + * Add all items from a source array. + *

+ * References must be added in sort order, or the array must be sorted + * after additions are complete using {@link #sort()}. + * + * @param src + * the source array. + * @param off + * position within {@code src} to start copying from. + * @param cnt + * number of items to copy from {@code src}. + */ + public void addAll(Ref[] src, int off, int cnt) { + if (list.length < size + cnt) { + Ref[] n = new Ref[Math.max(size * 2, size + cnt)]; + System.arraycopy(list, 0, n, 0, size); + list = n; + } + System.arraycopy(src, off, list, size, cnt); + size += cnt; + } + + /** + * Replace a single existing element. + * + * @param idx + * index, must have already been added previously. + * @param ref + * the new reference. + */ + public void set(int idx, T ref) { + list[idx] = ref; + } + + /** Sort the list's backing array in-place. */ + public void sort() { + Arrays.sort(list, 0, size, RefComparator.INSTANCE); + } + + /** @return an unmodifiable list using this collection's backing array. */ + public RefList toRefList() { + return new RefList(list, size); + } + + @Override + public String toString() { + return toRefList().toString(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java new file mode 100644 index 000000000..8a21cff73 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.util; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefComparator; + +/** + * Specialized Map to present a {@code RefDatabase} namespace. + *

+ * Although not declared as a {@link java.util.SortedMap}, iterators from this + * map's projections always return references in {@link RefComparator} ordering. + * The map's internal representation is a sorted array of {@link Ref} objects, + * which means lookup and replacement is O(log N), while insertion and removal + * can be as expensive as O(N + log N) while the list expands or contracts. + * Since this is not a general map implementation, all entries must be keyed by + * the reference name. + *

+ * This class is really intended as a helper for {@code RefDatabase}, which + * needs to perform a merge-join of three sorted {@link RefList}s in order to + * present the unified namespace of the packed-refs file, the loose refs/ + * directory tree, and the resolved form of any symbolic references. + */ +public class RefMap extends AbstractMap { + /** + * Prefix denoting the reference subspace this map contains. + *

+ * All reference names in this map must start with this prefix. If the + * prefix is not the empty string, it must end with a '/'. + */ + private final String prefix; + + /** Immutable collection of the packed references at construction time. */ + private RefList packed; + + /** + * Immutable collection of the loose references at construction time. + *

+ * If an entry appears here and in {@link #packed}, this entry must take + * precedence, as its more current. Symbolic references in this collection + * are typically unresolved, so they only tell us who their target is, but + * not the current value of the target. + */ + private RefList loose; + + /** + * Immutable collection of resolved symbolic references. + *

+ * This collection contains only the symbolic references we were able to + * resolve at map construction time. Other loose references must be read + * from {@link #loose}. Every entry in this list must be matched by an entry + * in {@code loose}, otherwise it might be omitted by the map. + */ + private RefList resolved; + + private int size; + + private boolean sizeIsValid; + + private Set> entrySet; + + /** Construct an empty map with a small initial capacity. */ + public RefMap() { + prefix = ""; + packed = RefList.emptyList(); + loose = RefList.emptyList(); + resolved = RefList.emptyList(); + } + + /** + * Construct a map to merge 3 collections together. + * + * @param prefix + * prefix used to slice the lists down. Only references whose + * names start with this prefix will appear to reside in the map. + * Must not be null, use {@code ""} (the empty string) to select + * all list items. + * @param packed + * items from the packed reference list, this is the last list + * searched. + * @param loose + * items from the loose reference list, this list overrides + * {@code packed} if a name appears in both. + * @param resolved + * resolved symbolic references. This list overrides the prior + * list {@code loose}, if an item appears in both. Items in this + * list must also appear in {@code loose}. + */ + public RefMap(String prefix, RefList packed, RefList loose, + RefList resolved) { + this.prefix = prefix; + this.packed = packed; + this.loose = loose; + this.resolved = resolved; + } + + @Override + public boolean containsKey(Object name) { + return get(name) != null; + } + + @Override + public Ref get(Object key) { + String name = toRefName((String) key); + Ref ref = resolved.get(name); + if (ref == null) + ref = loose.get(name); + if (ref == null) + ref = packed.get(name); + return ref; + } + + @Override + public Ref put(final String keyName, Ref value) { + String name = toRefName(keyName); + + if (!name.equals(value.getName())) + throw new IllegalArgumentException(); + + if (!resolved.isEmpty()) { + // Collapse the resolved list into the loose list so we + // can discard it and stop joining the two together. + for (Ref ref : resolved) + loose = loose.put(ref); + resolved = RefList.emptyList(); + } + + int idx = loose.find(name); + if (0 <= idx) { + Ref prior = loose.get(name); + loose = loose.set(idx, value); + return prior; + } else { + Ref prior = get(keyName); + loose = loose.add(idx, value); + sizeIsValid = false; + return prior; + } + } + + @Override + public Ref remove(Object key) { + String name = toRefName((String) key); + Ref res = null; + int idx; + if (0 <= (idx = packed.find(name))) { + res = packed.get(name); + packed = packed.remove(idx); + sizeIsValid = false; + } + if (0 <= (idx = loose.find(name))) { + res = loose.get(name); + loose = loose.remove(idx); + sizeIsValid = false; + } + if (0 <= (idx = resolved.find(name))) { + res = resolved.get(name); + resolved = resolved.remove(idx); + sizeIsValid = false; + } + return res; + } + + @Override + public boolean isEmpty() { + return entrySet().isEmpty(); + } + + @Override + public Set> entrySet() { + if (entrySet == null) { + entrySet = new AbstractSet>() { + @Override + public Iterator> iterator() { + return new SetIterator(); + } + + @Override + public int size() { + if (!sizeIsValid) { + size = 0; + Iterator i = entrySet().iterator(); + for (; i.hasNext(); i.next()) + size++; + sizeIsValid = true; + } + return size; + } + + @Override + public boolean isEmpty() { + if (sizeIsValid) + return 0 == size; + return !iterator().hasNext(); + } + + @Override + public void clear() { + packed = RefList.emptyList(); + loose = RefList.emptyList(); + resolved = RefList.emptyList(); + size = 0; + sizeIsValid = true; + } + }; + } + return entrySet; + } + + @Override + public String toString() { + StringBuilder r = new StringBuilder(); + boolean first = true; + r.append('['); + for (Ref ref : values()) { + if (first) + first = false; + else + r.append(", "); + r.append(ref); + } + r.append(']'); + return r.toString(); + } + + private String toRefName(String name) { + if (0 < prefix.length()) + name = prefix + name; + return name; + } + + private String toMapKey(Ref ref) { + String name = ref.getName(); + if (0 < prefix.length()) + name = name.substring(prefix.length()); + return name; + } + + private class SetIterator implements Iterator> { + private int packedIdx; + + private int looseIdx; + + private int resolvedIdx; + + private Entry next; + + SetIterator() { + if (0 < prefix.length()) { + packedIdx = -(packed.find(prefix) + 1); + looseIdx = -(loose.find(prefix) + 1); + resolvedIdx = -(resolved.find(prefix) + 1); + } + } + + public boolean hasNext() { + if (next == null) + next = peek(); + return next != null; + } + + public Entry next() { + if (hasNext()) { + Entry r = next; + next = peek(); + return r; + } + throw new NoSuchElementException(); + } + + public Entry peek() { + if (packedIdx < packed.size() && looseIdx < loose.size()) { + Ref p = packed.get(packedIdx); + Ref l = loose.get(looseIdx); + int cmp = RefComparator.compareTo(p, l); + if (cmp < 0) { + packedIdx++; + return toEntry(p); + } + + if (cmp == 0) + packedIdx++; + looseIdx++; + return toEntry(resolveLoose(l)); + } + + if (looseIdx < loose.size()) + return toEntry(resolveLoose(loose.get(looseIdx++))); + if (packedIdx < packed.size()) + return toEntry(packed.get(packedIdx++)); + return null; + } + + private Ref resolveLoose(final Ref l) { + if (resolvedIdx < resolved.size()) { + Ref r = resolved.get(resolvedIdx); + int cmp = RefComparator.compareTo(l, r); + if (cmp == 0) { + resolvedIdx++; + return r; + } else if (cmp > 0) { + // WTF, we have a symbolic entry but no match + // in the loose collection. That's an error. + throw new IllegalStateException(); + } + } + return l; + } + + private Ent toEntry(Ref p) { + if (p.getName().startsWith(prefix)) + return new Ent(p); + packedIdx = packed.size(); + looseIdx = loose.size(); + resolvedIdx = resolved.size(); + return null; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private class Ent implements Entry { + private Ref ref; + + Ent(Ref ref) { + this.ref = ref; + } + + public String getKey() { + return toMapKey(ref); + } + + public Ref getValue() { + return ref; + } + + public Ref setValue(Ref value) { + Ref prior = put(getKey(), value); + ref = value; + return prior; + } + + @Override + public int hashCode() { + return getKey().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Map.Entry) { + final Object key = ((Map.Entry) obj).getKey(); + final Object val = ((Map.Entry) obj).getValue(); + if (key instanceof String && val instanceof Ref) { + final Ref r = (Ref) val; + if (r.getName().equals(ref.getName())) { + final ObjectId a = r.getObjectId(); + final ObjectId b = ref.getObjectId(); + if (a != null && b != null && AnyObjectId.equals(a, b)) + return true; + } + } + } + return false; + } + + @Override + public String toString() { + return ref.toString(); + } + } +}