From ab697ff18b09e5d49e028b7a32844d408b02ccf2 Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Fri, 22 Jan 2010 16:27:03 -0800 Subject: [PATCH 1/5] Create new RefList and RefMap utility types These types can be used by RefDatabase implementations to manage the collection. A RefList stores items sorted by their name, and is an immutable type using copy-on-write semantics to perform modifications to the collection. Binary search is used to locate an existing item by name, or to locate the proper insertion position if an item does not exist. A RefMap can merge up to 3 RefList collections at once during its entry iteration, allowing items in the resolved or loose RefList to override items by the same name in the packed RefList. The RefMap's goal is O(log N) lookup time, and O(N) iteration time, which is suitable for returning from a RefDatabase. By relying on the immutable RefList we might be able to make map construction nearly constant, making Repository.getAllRefs() an inexpensive operation if the caches are current. Since modification is not common, changes require up to O(N + log N) time to copy the internal list and collapse or expand the list's array. As most changes are made to the loose collection and not the packed collection, in practice most changes would require less than the full O(N) time, due to a significantly smaller N in the loose list. Almost complete test coverage is included in the corresponding unit tests. A handful of methods on RefMap are not tested in this change, as writing the proper test depends on a future refactoring of how the Ref class represents symbolic reference names. Change-Id: Ic2095274000336556f719edd75a5c5dd6dd1d857 Signed-off-by: Shawn O. Pearce --- .../org/eclipse/jgit/util/RefListTest.java | 431 +++++++++++++++++ .../tst/org/eclipse/jgit/util/RefMapTest.java | 382 +++++++++++++++ .../org/eclipse/jgit/lib/RefComparator.java | 32 +- .../src/org/eclipse/jgit/util/RefList.java | 438 ++++++++++++++++++ .../src/org/eclipse/jgit/util/RefMap.java | 423 +++++++++++++++++ 5 files changed, 1703 insertions(+), 3 deletions(-) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefListTest.java create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java 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..c9522341b --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefListTest.java @@ -0,0 +1,431 @@ +/* + * 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.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 Ref(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..6ce206e01 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java @@ -0,0 +1,382 @@ +/* + * 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.Ref; + +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 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 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, ObjectId id) { + return new Ref(Ref.Storage.LOOSE, name, id); + } +} 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/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(); + } + } +} From 01b5392cdbc12ce2e21fd1d1afbd61fdf97e1c38 Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Fri, 22 Jan 2010 14:54:40 -0800 Subject: [PATCH 2/5] Rewrite reference handling to be abstract and accurate This commit actually does three major changes to the way references are handled within JGit. Unfortunately they were easier to do as a single massive commit than to break them up into smaller units. Disambiguate symbolic references: --------------------------------- Reporting a symbolic reference such as HEAD as though it were any other normal reference like refs/heads/master causes subtle programming errors. We have been bitten by this error on several occasions, as have some downstream applications written by myself. Instead of reporting HEAD as a reference whose name differs from its "original name", report it as an actual SymbolicRef object that the application can test the type and examine the target of. With this change, Ref is now an abstract type with different subclasses for the different types. In the classical example of "HEAD" being a symbolic reference to branch "refs/heads/master", the Repository.getAllRefs() method will now return: Map all = repository.getAllRefs(); SymbolicRef HEAD = (SymbolicRef) all.get("HEAD"); ObjectIdRef master = (ObjectIdRef) all.get("refs/heads/master"); assertSame(master, HEAD.getTarget()); assertSame(master.getObjectId(), HEAD.getObjectId()); assertEquals("HEAD", HEAD.getName()); assertEquals("refs/heads/master", master.getName()); A nice side-effect of this change is the storage type of the symbolic reference is no longer ambiguous with the storge type of the underlying reference it targets. In the above example, if master was only available in the packed-refs file, then the following is also true: assertSame(Ref.Storage.LOOSE, HEAD.getStorage()); assertSame(Ref.Storage.PACKED, master.getStorage()); (Prior to this change we returned the ambiguous storage of LOOSE_PACKED for HEAD, which was confusing since it wasn't actually true on disk). Another nice side-effect of this change is all intermediate symbolic references are preserved, and are therefore visible to the application when they walk the target chain. We can now correctly inspect chains of symbolic references. As a result of this change the Ref.getOrigName() method has been removed from the API. Applications should identify a symbolic reference by testing for isSymbolic() and not by using an arcane string comparsion between properties. Abstract the RefDatabase storage: --------------------------------- RefDatabase is now abstract, similar to ObjectDatabase, and a new concrete implementation called RefDirectory is used for the traditional on-disk storage layout. In the future we plan to support additional implementations, such as a pure in-memory RefDatabase for unit testing purposes. Optimize RefDirectory: ---------------------- The implementation of the in-memory reference cache, reading, and update routines has been completely rewritten. Much of the code was heavily borrowed or cribbed from the prior implementation, so copyright notices have been left intact as much as possible. The RefDirectory cache no longer confuses symbolic references with normal references. This permits the cache to resolve the value of a symbolic reference as late as possible, ensuring it is always current, without needing to maintain reverse pointers. The cache is now 2 sorted RefLists, rather than 3 HashMaps. Using sorted lists allows the implementation to reduce the in-memory footprint when storing many refs. Using specialized types for the elements allows the code to avoid additional map lookups for auxiliary stat information. To improve scan time during getRefs(), the lists are returned via a copy-on-write contract. Most callers of getRefs() do not modify the returned collections, so the copy-on-write semantics improves access on repositories with a large number of packed references. Iterator traversals of the returned Map are performed using a simple merge-join of the two cache lists, ensuring we can perform the entire traversal in linear time as a function of the number of references: O(PackedRefs + LooseRefs). Scans of the loose reference space to update the cache run in O(LooseRefs log LooseRefs) time, as the directory contents are sorted before being merged against the in-memory cache. Since the majority of stable references are kept packed, there typically are only a handful of reference names to be sorted, so the sorting cost should not be very high. Locking is reduced during getRefs() by taking advantage of the copy-on-write semantics of the improved cache data structure. This permits concurrent readers to pull back references without blocking each other. If there is contention updating the cache during a scan, one or more updates are simply skipped and will get picked up again in a future scan. Writing to the $GIT_DIR/packed-refs during reference delete is now fully atomic. The file is locked, reparsed fresh, and written back out if a change is necessary. This avoids all race conditions with concurrent external updates of the packed-refs file. The RefLogWriter class has been fully folded into RefDirectory and is therefore deleted. Maintaining the reference's log is the responsiblity of the database implementation, and not all implementations will use java.io for access. Future work still remains to be done to abstract the ReflogReader class away from local disk IO. Change-Id: I26b9287c45a4b2d2be35ba2849daa316f5eec85d Signed-off-by: Shawn O. Pearce --- .../jgit/http/server/InfoRefsServlet.java | 3 +- .../src/org/eclipse/jgit/pgm/Branch.java | 2 +- .../src/org/eclipse/jgit/pgm/Log.java | 3 +- .../src/org/eclipse/jgit/pgm/ShowRef.java | 16 +- .../jgit/pgm/debug/RebuildCommitGraph.java | 6 +- .../org/eclipse/jgit/lib/ObjectIdRefTest.java | 115 ++ .../eclipse/jgit/lib/RefDirectoryTest.java | 1024 +++++++++++++++++ .../tst/org/eclipse/jgit/lib/RefTest.java | 37 +- .../org/eclipse/jgit/lib/RefUpdateTest.java | 78 +- .../eclipse/jgit/lib/ReflogConfigTest.java | 4 +- .../org/eclipse/jgit/lib/SymbolicRefTest.java | 129 +++ .../tst/org/eclipse/jgit/lib/T0003_Basic.java | 22 + .../jgit/transport/PushProcessTest.java | 31 +- .../eclipse/jgit/transport/RefSpecTest.java | 27 +- .../org/eclipse/jgit/util/RefListTest.java | 3 +- .../tst/org/eclipse/jgit/util/RefMapTest.java | 92 +- .../eclipse/jgit/awtui/AWTPlotRenderer.java | 3 +- .../src/org/eclipse/jgit/lib/LockFile.java | 36 +- .../src/org/eclipse/jgit/lib/ObjectIdRef.java | 188 +++ .../src/org/eclipse/jgit/lib/Ref.java | 184 +-- .../src/org/eclipse/jgit/lib/RefDatabase.java | 613 +++------- .../org/eclipse/jgit/lib/RefDirectory.java | 1015 ++++++++++++++++ .../eclipse/jgit/lib/RefDirectoryRename.java | 217 ++++ .../eclipse/jgit/lib/RefDirectoryUpdate.java | 135 +++ .../org/eclipse/jgit/lib/RefLogWriter.java | 158 --- .../src/org/eclipse/jgit/lib/RefRename.java | 205 ++-- .../src/org/eclipse/jgit/lib/RefUpdate.java | 276 ++--- .../src/org/eclipse/jgit/lib/RefWriter.java | 33 +- .../src/org/eclipse/jgit/lib/Repository.java | 159 +-- .../src/org/eclipse/jgit/lib/SymbolicRef.java | 121 ++ .../jgit/transport/BasePackConnection.java | 9 +- .../jgit/transport/BundleFetchConnection.java | 7 +- .../eclipse/jgit/transport/ReceivePack.java | 7 +- .../eclipse/jgit/transport/RefAdvertiser.java | 6 +- .../jgit/transport/TransportAmazonS3.java | 11 +- .../eclipse/jgit/transport/TransportHttp.java | 29 +- .../eclipse/jgit/transport/TransportSftp.java | 21 +- .../jgit/transport/WalkPushConnection.java | 5 +- .../transport/WalkRemoteObjectDatabase.java | 19 +- 39 files changed, 3787 insertions(+), 1262 deletions(-) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdRefTest.java create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDirectoryTest.java create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SymbolicRefTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectory.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryRename.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryUpdate.java delete mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/RefLogWriter.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java 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..3b615198a 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,7 +97,7 @@ 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()); 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..ffc28edc5 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 @@ -143,7 +143,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/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..42c8a92b9 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 @@ -75,7 +75,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"); @@ -87,7 +87,11 @@ public void testReadAllIncludingSymrefs() throws Exception { public void testReadSymRefToPacked() throws IOException { db.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 { @@ -100,7 +104,10 @@ public void testReadSymRefToLoosePacked() throws IOException { db.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 +136,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 +156,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..cb9111758 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 @@ -260,10 +260,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 +308,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")); @@ -337,7 +337,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 +414,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,7 +433,7 @@ 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"); @@ -440,11 +443,14 @@ public void testRefsCacheAfterUpdateLoosOnly() throws Exception { 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 +581,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"); @@ -598,8 +604,8 @@ public void testRenameCurrentBranch() throws IOException { db.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 +631,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"); @@ -657,7 +662,7 @@ public void tryRenameWhenLocked(String toLock, String fromName, db.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 +696,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 +738,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,12 +750,6 @@ 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"); @@ -767,7 +760,7 @@ public void testRenameRefNameColission1avoided() throws IOException { 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()); @@ -800,7 +793,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 +816,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); + } } 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 e29e9e721..d4231bfa6 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 index c9522341b..c6471ded9 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefListTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefListTest.java @@ -49,6 +49,7 @@ 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 { @@ -426,6 +427,6 @@ private RefList toList(Ref... refs) { } private static Ref newRef(final String name) { - return new Ref(Ref.Storage.LOOSE, name, ID); + 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 index 6ce206e01..c4c2383f5 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java @@ -50,7 +50,9 @@ 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 @@ -176,6 +178,49 @@ public void testIterator_FailsAtEnd() { } } + 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); @@ -297,6 +342,42 @@ public void testPut_WithPrefix() { 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); @@ -376,7 +457,16 @@ private RefList toList(Ref... refs) { 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 Ref(Ref.Storage.LOOSE, name, 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/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java index fb6a27db3..22d0c1ad3 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,169 @@ 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 { + /** + * 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. + */ + 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. - * @return an update command. The caller must finish populating this command - * and then invoke one of the update methods to actually make a - * change. + * proposed name. + * @return true if the name overlaps with an existing reference; false if + * using this name right now would be safe. * @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 database could not be read to check for conflicts. */ - RefUpdate newUpdate(final String name) throws IOException { - return newUpdate(name, false); - } + public abstract boolean isNameConflicting(String name) throws IOException; /** - * Create a command to update, create or delete a ref in this repository. + * Create a symbolic reference from one name to another. * * @param name - * name of the ref the caller wants to modify. - * @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. - * @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. - */ - 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(); - } - - /** - * An set of update operations for renaming a ref - * - * @param fromRef Old ref name - * @param toRef New ref name - * @return a RefUpdate operation to rename a ref - * @throws IOException - */ - 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); - } - - /** - * Writes a symref (e.g. HEAD) to disk - * - * @param name - * symref name + * the name of the reference. Should be {@link Constants#HEAD} or + * starting with {@link Constants#R_REFS}. * @param target - * pointed to ref + * the target of the reference. * @throws IOException + * the reference could not be created or overwritten. */ - 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 void link(String name, String target) throws IOException; /** - * @return all known refs (heads, tags, remotes). + * Create a new update command to create, modify or delete a reference. + * + * @param name + * the name of the reference. + * @param detach + * 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 + * the reference space cannot be accessed. */ - Map getAllRefs() { - return readRefs(); - } + public abstract RefUpdate newUpdate(String name, boolean detach) + 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"). + * Create a new update command to rename a reference. + * + * @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. */ - 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; - } + public abstract RefRename newRename(String fromName, String toName) + throws IOException; - 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; - } + /** + * 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 + * 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. + */ + public abstract Ref getRef(String name) throws IOException; - private synchronized void readPackedRefs(final Map avail) { - refreshPackedRefs(); - avail.putAll(packedRefs); - } + /** + * 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. + */ + public abstract Map getRefs(String prefix) throws IOException; - 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)); - } + /** + * 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. + */ + 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..9fdb48fe8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectory.java @@ -0,0 +1,1015 @@ +/* + * 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; + } + + @Override + public void link(String name, String target) throws IOException { + LockFile lck = new LockFile(fileFor(name)); + if (!lck.lock()) + throw new IOException("Cannot lock " + name); + lck.setNeedStatInformation(true); + try { + lck.write(encode(SYMREF + target + '\n')); + if (!lck.commit()) + throw new IOException("Cannot write " + name); + } finally { + lck.unlock(); + } + putLooseRef(newSymbolicRef(lck.getCommitLastModified(), name, target)); + fireRefsChanged(); + } + + @Override + 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) 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 (ref.isSymbolic()) + log(ref.getName(), rec); + log(ref.getLeaf().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..e43d2a561 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryRename.java @@ -0,0 +1,217 @@ +/* + * 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 { + refdb.link(Constants.HEAD, target.getName()); + return true; + } 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..113b74b44 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryUpdate.java @@ -0,0 +1,135 @@ +/* + * 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 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() throws IOException { + Ref dst = getRef().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); + } + 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; + } +} 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..33e12a110 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,52 @@ 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)}. + * + * @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() 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; /** * Get the name of the ref this update will operate on. @@ -193,16 +214,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 +312,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 +361,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 +410,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 +418,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 +436,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 +456,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,14 +476,23 @@ 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; @@ -453,17 +501,14 @@ public Result delete(final RevWalk walk) throws IOException { 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()) + return Result.LOCK_FAILURE; if (expValue != null) { final ObjectId o; o = oldValue != null ? oldValue : ObjectId.zeroId(); @@ -471,41 +516,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 +550,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 bbc5cc2a6..ecae243a4 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-2009, 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); @@ -310,6 +308,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 +594,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 +865,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 +878,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(); + } } /** @@ -911,53 +916,48 @@ public String toString() { } /** - * @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 +971,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 +1012,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 +1029,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 +1048,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 +1125,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 +1243,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 +1305,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..f8be827df 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())) + 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..77f30140c 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 @@ -170,9 +170,9 @@ public void send(final Collection refs) throws IOException { for (final Ref r : RefComparator.sort(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() + "^{}"); } } } 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/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); } } From 73b6efc9289d6f7b6c147f4c2e2c62d2134fd7f3 Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Sat, 9 Jan 2010 18:56:45 -0800 Subject: [PATCH 3/5] Replace writeSymref with RefUpdate.link By using RefUpdate for symbolic reference creation we can reuse the logic related to updating the reflog with the event, without needing to expose something such as the legacy ReflogWriter class (which we no longer have). Applications using writeSymref must update their code to use the new pattern of changing the reference through the updateRef method: String refName = "refs/heads/master"; RefUpdate u = repository.updateRef(Constants.HEAD); u.setRefLogMessage("checkout: moving to " + refName, false); switch (u.link(refName)) { case NEW: case FORCED: case NO_CHANGE: // A successful update of the reference break; default: // Handle the failure, e.g. for older behavior throw new IOException(u.getResult()); } Change-Id: I1093e1ec2970147978a786cfdd0a75d0aebf8010 Signed-off-by: Shawn O. Pearce --- .../src/org/eclipse/jgit/pgm/Clone.java | 9 ++- .../tst/org/eclipse/jgit/lib/RefTest.java | 22 +++++-- .../org/eclipse/jgit/lib/RefUpdateTest.java | 27 ++++++--- .../src/org/eclipse/jgit/lib/RefDatabase.java | 13 ---- .../org/eclipse/jgit/lib/RefDirectory.java | 27 +++------ .../eclipse/jgit/lib/RefDirectoryRename.java | 12 +++- .../eclipse/jgit/lib/RefDirectoryUpdate.java | 27 ++++++++- .../src/org/eclipse/jgit/lib/RefUpdate.java | 59 ++++++++++++++++++- .../src/org/eclipse/jgit/lib/Repository.java | 18 ++---- 9 files changed, 147 insertions(+), 67 deletions(-) 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 3fe50d668..571f34b05 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.test/tst/org/eclipse/jgit/lib/RefTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java index 42c8a92b9..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); @@ -85,7 +97,7 @@ 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, ref.getStorage()); assertTrue("is symref", ref.isSymbolic()); @@ -102,7 +114,7 @@ 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, ref.getStorage()); ref = ref.getTarget(); 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 cb9111758..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)); @@ -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); @@ -437,7 +450,7 @@ 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); @@ -601,7 +614,7 @@ 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)); writeReflog(db, rb, rb, "Just a message", "refs/heads/b"); @@ -659,7 +672,7 @@ 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); writeReflog(db, oldfromId, oldfromId, "Just a message", @@ -753,7 +766,7 @@ public void testRenameBranchCannotLockAFileHEADisOtherLockTo() 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); @@ -785,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); @@ -823,6 +836,6 @@ private void writeReflog(Repository db, ObjectId oldId, ObjectId newId, RefDirectoryUpdate update = refs.newUpdate(refName, true); update.setOldObjectId(oldId); update.setNewObjectId(newId); - refs.log(update, msg); + refs.log(update, msg, true); } } 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 22d0c1ad3..21e7041b3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java @@ -113,19 +113,6 @@ public abstract class RefDatabase { */ public abstract boolean isNameConflicting(String name) throws IOException; - /** - * Create a symbolic reference from one name to another. - * - * @param name - * the name of the reference. Should be {@link Constants#HEAD} or - * starting with {@link Constants#R_REFS}. - * @param target - * the target of the reference. - * @throws IOException - * the reference could not be created or overwritten. - */ - public abstract void link(String name, String target) throws IOException; - /** * Create a new update command to create, modify or delete a reference. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectory.java index 9fdb48fe8..90ac0bf47 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectory.java @@ -439,24 +439,11 @@ private static Ref recreate(final Ref old, final ObjectIdRef leaf) { return leaf; } - @Override - public void link(String name, String target) throws IOException { - LockFile lck = new LockFile(fileFor(name)); - if (!lck.lock()) - throw new IOException("Cannot lock " + name); - lck.setNeedStatInformation(true); - try { - lck.write(encode(SYMREF + target + '\n')); - if (!lck.commit()) - throw new IOException("Cannot write " + name); - } finally { - lck.unlock(); - } - putLooseRef(newSymbolicRef(lck.getCommitLastModified(), name, target)); + void storedSymbolicRef(RefDirectoryUpdate u, long modified, String target) { + putLooseRef(newSymbolicRef(modified, u.getRef().getName(), target)); fireRefsChanged(); } - @Override public RefDirectoryUpdate newUpdate(String name, boolean detach) throws IOException { final RefList packed = getPackedRefs(); @@ -536,7 +523,8 @@ void delete(RefDirectoryUpdate update) throws IOException { fireRefsChanged(); } - void log(final RefUpdate update, final String msg) throws IOException { + 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(); @@ -558,9 +546,12 @@ void log(final RefUpdate update, final String msg) throws IOException { r.append('\n'); final byte[] rec = encode(r.toString()); - if (ref.isSymbolic()) + if (deref && ref.isSymbolic()) { log(ref.getName(), rec); - log(ref.getLeaf().getName(), rec); + log(ref.getLeaf().getName(), rec); + } else { + log(ref.getName(), rec); + } } private void log(final String refName, final byte[] rec) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryRename.java index e43d2a561..fec00d932 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryRename.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryRename.java @@ -208,8 +208,16 @@ private static boolean rename(File src, File dst) { private boolean linkHEAD(RefUpdate target) { try { - refdb.link(Constants.HEAD, target.getName()); - return true; + 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 index 113b74b44..447be104a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDirectoryUpdate.java @@ -44,6 +44,8 @@ package org.eclipse.jgit.lib; +import static org.eclipse.jgit.lib.Constants.encode; + import java.io.IOException; /** Updates any reference stored by {@link RefDirectory}. */ @@ -68,8 +70,10 @@ protected Repository getRepository() { } @Override - protected boolean tryLock() throws IOException { - Ref dst = getRef().getLeaf(); + 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()) { @@ -105,7 +109,7 @@ protected Result doUpdate(final Result status) throws IOException { msg = strResult; } } - database.log(this, msg); + database.log(this, msg, true); } if (!lock.commit()) return Result.LOCK_FAILURE; @@ -132,4 +136,21 @@ protected Result doDelete(final Result status) throws IOException { 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/RefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java index 33e12a110..553266284 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java @@ -183,13 +183,17 @@ public static enum Result { * 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() throws IOException; + protected abstract boolean tryLock(boolean deref) throws IOException; /** Releases the lock taken by {@link #tryLock} if it succeeded. */ protected abstract void unlock(); @@ -208,6 +212,13 @@ public static enum Result { */ 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. * @@ -499,6 +510,50 @@ Result execute(Result status) throws IOException { } } + /** + * 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 { RevObject newObj; @@ -507,7 +562,7 @@ private Result updateImpl(final RevWalk walk, final Store store) if (getRefDatabase().isNameConflicting(getName())) return Result.LOCK_FAILURE; try { - if (!tryLock()) + if (!tryLock(true)) return Result.LOCK_FAILURE; if (expValue != null) { final ObjectId o; 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 ecae243a4..ca86e36a4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -276,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); @@ -899,18 +901,6 @@ 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() + "]"; } From 57f6f6a6bb50bf4916a32723c4f32bac616a1da6 Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Sat, 9 Jan 2010 19:22:47 -0800 Subject: [PATCH 4/5] branch: Add -m option to rename a branch Change-Id: I7cf8e43344eaf301592fba0c178e04daad930f9a Signed-off-by: Shawn O. Pearce --- .../src/org/eclipse/jgit/pgm/Branch.java | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) 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 ffc28edc5..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) From 36f05a9c27e6961b10df0b65014ffc869f4f8686 Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Fri, 22 Jan 2010 18:42:12 -0800 Subject: [PATCH 5/5] Optimize RefAdvertiser performance by avoiding sorting Don't copy and sort the set of references if they are passed through in a RefMap or a SortedMap using the key's natural sort ordering. Either map is already in the order we want to present the items to the client in, so copying and sorting is a waste of local CPU and memory. Change-Id: I49ada7c1220e0fc2a163b9752c2b77525d9c82c1 Signed-off-by: Shawn O. Pearce --- .../jgit/http/server/InfoRefsServlet.java | 2 +- .../eclipse/jgit/transport/ReceivePack.java | 2 +- .../eclipse/jgit/transport/RefAdvertiser.java | 23 +++++++++++++------ .../eclipse/jgit/transport/UploadPack.java | 4 ++-- 4 files changed, 20 insertions(+), 11 deletions(-) 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 3b615198a..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 @@ -99,7 +99,7 @@ protected void end() { 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/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java index f8be827df..15bdf9618 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java @@ -591,7 +591,7 @@ public void sendAdvertisedRefs(final RefAdvertiser adv) throws IOException { adv.advertiseCapability(CAPABILITY_OFS_DELTA); refs = db.getAllRefs(); final Ref head = refs.remove(Constants.HEAD); - adv.send(refs.values()); + adv.send(refs); if (!head.isSymbolic()) adv.advertiseHave(head.getObjectId()); adv.includeAdditionalHaves(); 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 77f30140c..694a2e0f1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java @@ -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,14 +162,14 @@ 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.getName()); @@ -177,6 +179,13 @@ public void send(final Collection refs) throws IOException { } } + 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/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(); }