diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/LeafBucketTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/LeafBucketTest.java new file mode 100644 index 000000000..68b0e2b5e --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/LeafBucketTest.java @@ -0,0 +1,226 @@ +/* + * 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.notes; + +import java.io.IOException; + +import junit.framework.TestCase; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.MutableObjectId; + +public class LeafBucketTest extends TestCase { + public void testEmpty() { + LeafBucket b = new LeafBucket(0); + assertNull(b.get(id(0x00), null)); + assertNull(b.get(id(0x01), null)); + assertNull(b.get(id(0xfe), null)); + } + + public void testParseFive() { + LeafBucket b = new LeafBucket(0); + + b.parseOneEntry(id(0x11), id(0x81)); + b.parseOneEntry(id(0x22), id(0x82)); + b.parseOneEntry(id(0x33), id(0x83)); + b.parseOneEntry(id(0x44), id(0x84)); + b.parseOneEntry(id(0x55), id(0x85)); + + assertNull(b.get(id(0x01), null)); + assertEquals(id(0x81), b.get(id(0x11), null)); + assertEquals(id(0x82), b.get(id(0x22), null)); + assertEquals(id(0x83), b.get(id(0x33), null)); + assertEquals(id(0x84), b.get(id(0x44), null)); + assertEquals(id(0x85), b.get(id(0x55), null)); + assertNull(b.get(id(0x66), null)); + } + + public void testSetFive_InOrder() throws IOException { + LeafBucket b = new LeafBucket(0); + + assertSame(b, b.set(id(0x11), id(0x81), null)); + assertSame(b, b.set(id(0x22), id(0x82), null)); + assertSame(b, b.set(id(0x33), id(0x83), null)); + assertSame(b, b.set(id(0x44), id(0x84), null)); + assertSame(b, b.set(id(0x55), id(0x85), null)); + + assertNull(b.get(id(0x01), null)); + assertEquals(id(0x81), b.get(id(0x11), null)); + assertEquals(id(0x82), b.get(id(0x22), null)); + assertEquals(id(0x83), b.get(id(0x33), null)); + assertEquals(id(0x84), b.get(id(0x44), null)); + assertEquals(id(0x85), b.get(id(0x55), null)); + assertNull(b.get(id(0x66), null)); + } + + public void testSetFive_ReverseOrder() throws IOException { + LeafBucket b = new LeafBucket(0); + + assertSame(b, b.set(id(0x55), id(0x85), null)); + assertSame(b, b.set(id(0x44), id(0x84), null)); + assertSame(b, b.set(id(0x33), id(0x83), null)); + assertSame(b, b.set(id(0x22), id(0x82), null)); + assertSame(b, b.set(id(0x11), id(0x81), null)); + + assertNull(b.get(id(0x01), null)); + assertEquals(id(0x81), b.get(id(0x11), null)); + assertEquals(id(0x82), b.get(id(0x22), null)); + assertEquals(id(0x83), b.get(id(0x33), null)); + assertEquals(id(0x84), b.get(id(0x44), null)); + assertEquals(id(0x85), b.get(id(0x55), null)); + assertNull(b.get(id(0x66), null)); + } + + public void testSetFive_MixedOrder() throws IOException { + LeafBucket b = new LeafBucket(0); + + assertSame(b, b.set(id(0x11), id(0x81), null)); + assertSame(b, b.set(id(0x33), id(0x83), null)); + assertSame(b, b.set(id(0x55), id(0x85), null)); + + assertSame(b, b.set(id(0x22), id(0x82), null)); + assertSame(b, b.set(id(0x44), id(0x84), null)); + + assertNull(b.get(id(0x01), null)); + assertEquals(id(0x81), b.get(id(0x11), null)); + assertEquals(id(0x82), b.get(id(0x22), null)); + assertEquals(id(0x83), b.get(id(0x33), null)); + assertEquals(id(0x84), b.get(id(0x44), null)); + assertEquals(id(0x85), b.get(id(0x55), null)); + assertNull(b.get(id(0x66), null)); + } + + public void testSet_Replace() throws IOException { + LeafBucket b = new LeafBucket(0); + + assertSame(b, b.set(id(0x11), id(0x81), null)); + assertEquals(id(0x81), b.get(id(0x11), null)); + + assertSame(b, b.set(id(0x11), id(0x01), null)); + assertEquals(id(0x01), b.get(id(0x11), null)); + } + + public void testRemoveMissingNote() throws IOException { + LeafBucket b = new LeafBucket(0); + assertNull(b.get(id(0x11), null)); + assertSame(b, b.set(id(0x11), null, null)); + assertNull(b.get(id(0x11), null)); + } + + public void testRemoveFirst() throws IOException { + LeafBucket b = new LeafBucket(0); + + assertSame(b, b.set(id(0x11), id(0x81), null)); + assertSame(b, b.set(id(0x22), id(0x82), null)); + assertSame(b, b.set(id(0x33), id(0x83), null)); + assertSame(b, b.set(id(0x44), id(0x84), null)); + assertSame(b, b.set(id(0x55), id(0x85), null)); + + assertSame(b, b.set(id(0x11), null, null)); + + assertNull(b.get(id(0x01), null)); + assertNull(b.get(id(0x11), null)); + assertEquals(id(0x82), b.get(id(0x22), null)); + assertEquals(id(0x83), b.get(id(0x33), null)); + assertEquals(id(0x84), b.get(id(0x44), null)); + assertEquals(id(0x85), b.get(id(0x55), null)); + assertNull(b.get(id(0x66), null)); + } + + public void testRemoveMiddle() throws IOException { + LeafBucket b = new LeafBucket(0); + + assertSame(b, b.set(id(0x11), id(0x81), null)); + assertSame(b, b.set(id(0x22), id(0x82), null)); + assertSame(b, b.set(id(0x33), id(0x83), null)); + assertSame(b, b.set(id(0x44), id(0x84), null)); + assertSame(b, b.set(id(0x55), id(0x85), null)); + + assertSame(b, b.set(id(0x33), null, null)); + + assertNull(b.get(id(0x01), null)); + assertEquals(id(0x81), b.get(id(0x11), null)); + assertEquals(id(0x82), b.get(id(0x22), null)); + assertNull(b.get(id(0x33), null)); + assertEquals(id(0x84), b.get(id(0x44), null)); + assertEquals(id(0x85), b.get(id(0x55), null)); + assertNull(b.get(id(0x66), null)); + } + + public void testRemoveLast() throws IOException { + LeafBucket b = new LeafBucket(0); + + assertSame(b, b.set(id(0x11), id(0x81), null)); + assertSame(b, b.set(id(0x22), id(0x82), null)); + assertSame(b, b.set(id(0x33), id(0x83), null)); + assertSame(b, b.set(id(0x44), id(0x84), null)); + assertSame(b, b.set(id(0x55), id(0x85), null)); + + assertSame(b, b.set(id(0x55), null, null)); + + assertNull(b.get(id(0x01), null)); + assertEquals(id(0x81), b.get(id(0x11), null)); + assertEquals(id(0x82), b.get(id(0x22), null)); + assertEquals(id(0x83), b.get(id(0x33), null)); + assertEquals(id(0x84), b.get(id(0x44), null)); + assertNull(b.get(id(0x55), null)); + assertNull(b.get(id(0x66), null)); + } + + public void testRemoveMakesEmpty() throws IOException { + LeafBucket b = new LeafBucket(0); + + assertSame(b, b.set(id(0x11), id(0x81), null)); + assertEquals(id(0x81), b.get(id(0x11), null)); + + assertNull(b.set(id(0x11), null, null)); + assertNull(b.get(id(0x11), null)); + } + + private static AnyObjectId id(int first) { + MutableObjectId id = new MutableObjectId(); + id.setByte(1, first); + return id; + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/NoteMapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/NoteMapTest.java index 786f1b94a..e1a6d9bf8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/NoteMapTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/NoteMapTest.java @@ -44,6 +44,8 @@ package org.eclipse.jgit.notes; import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryTestCase; @@ -56,17 +58,21 @@ public class NoteMapTest extends RepositoryTestCase { private ObjectReader reader; + private ObjectInserter inserter; + @Override protected void setUp() throws Exception { super.setUp(); tr = new TestRepository(db); reader = db.newObjectReader(); + inserter = db.newObjectInserter(); } @Override protected void tearDown() throws Exception { reader.release(); + inserter.release(); super.tearDown(); } @@ -182,6 +188,99 @@ public class NoteMapTest extends RepositoryTestCase { assertEquals(exp, RawParseUtils.decode(act)); } + public void testCreateFromEmpty() throws Exception { + RevBlob a = tr.blob("a"); + RevBlob b = tr.blob("b"); + RevBlob data1 = tr.blob("data1"); + RevBlob data2 = tr.blob("data2"); + + NoteMap map = NoteMap.newEmptyMap(); + assertFalse("no a", map.contains(a)); + assertFalse("no b", map.contains(b)); + + map.set(a, data1); + map.set(b, data2); + + assertEquals(data1, map.get(a)); + assertEquals(data2, map.get(b)); + + map.remove(a); + map.remove(b); + + assertFalse("no a", map.contains(a)); + assertFalse("no b", map.contains(b)); + + map.set(a, "data1", inserter); + assertEquals(data1, map.get(a)); + + map.set(a, null, inserter); + assertFalse("no a", map.contains(a)); + } + + public void testEditFlat() throws Exception { + RevBlob a = tr.blob("a"); + RevBlob b = tr.blob("b"); + RevBlob data1 = tr.blob("data1"); + RevBlob data2 = tr.blob("data2"); + + RevCommit r = tr.commit() // + .add(a.name(), data1) // + .add(b.name(), data2) // + .create(); + tr.parseBody(r); + + NoteMap map = NoteMap.read(reader, r); + map.set(a, data2); + map.set(b, null); + map.set(data1, b); + map.set(data2, null); + + assertEquals(data2, map.get(a)); + assertEquals(b, map.get(data1)); + assertFalse("no b", map.contains(b)); + assertFalse("no data2", map.contains(data2)); + + MutableObjectId id = new MutableObjectId(); + for (int p = 42; p > 0; p--) { + id.setByte(1, p); + map.set(id, data1); + } + + for (int p = 42; p > 0; p--) { + id.setByte(1, p); + assertTrue("contains " + id, map.contains(id)); + } + } + + public void testEditFanout2_38() throws Exception { + RevBlob a = tr.blob("a"); + RevBlob b = tr.blob("b"); + RevBlob data1 = tr.blob("data1"); + RevBlob data2 = tr.blob("data2"); + + RevCommit r = tr.commit() // + .add(fanout(2, a.name()), data1) // + .add(fanout(2, b.name()), data2) // + .create(); + tr.parseBody(r); + + NoteMap map = NoteMap.read(reader, r); + map.set(a, data2); + map.set(b, null); + map.set(data1, b); + map.set(data2, null); + + assertEquals(data2, map.get(a)); + assertEquals(b, map.get(data1)); + assertFalse("no b", map.contains(b)); + assertFalse("no data2", map.contains(data2)); + + map.set(a, null); + map.set(data1, null); + assertFalse("no a", map.contains(a)); + assertFalse("no data1", map.contains(data1)); + } + private static String fanout(int prefix, String name) { StringBuilder r = new StringBuilder(); int i = 0; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java index 85337c8d1..760528618 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java @@ -84,6 +84,9 @@ class FanoutBucket extends InMemoryNoteBucket { */ private final NoteBucket[] table; + /** Number of non-null slots in {@link #table}. */ + private int cnt; + FanoutBucket(int prefixLen) { super(prefixLen); table = new NoteBucket[256]; @@ -91,6 +94,7 @@ class FanoutBucket extends InMemoryNoteBucket { void parseOneEntry(int cell, ObjectId id) { table[cell] = new LazyNoteBucket(id); + cnt++; } @Override @@ -99,6 +103,39 @@ class FanoutBucket extends InMemoryNoteBucket { return b != null ? b.get(objId, or) : null; } + @Override + InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData, + ObjectReader or) throws IOException { + int cell = cell(noteOn); + NoteBucket b = table[cell]; + + if (b == null) { + if (noteData == null) + return this; + + LeafBucket n = new LeafBucket(prefixLen + 2); + table[cell] = n.set(noteOn, noteData, or); + cnt++; + return this; + + } else { + NoteBucket n = b.set(noteOn, noteData, or); + if (n == null) { + table[cell] = null; + cnt--; + + if (cnt == 0) + return null; + + return this; + + } else if (n != b) { + table[cell] = n; + } + return this; + } + } + private int cell(AnyObjectId id) { return id.getByte(prefixLen >> 1); } @@ -115,6 +152,12 @@ class FanoutBucket extends InMemoryNoteBucket { return load(objId, or).get(objId, or); } + @Override + InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData, + ObjectReader or) throws IOException { + return load(noteOn, or).set(noteOn, noteData, or); + } + private NoteBucket load(AnyObjectId objId, ObjectReader or) throws IOException { AbbreviatedObjectId p = objId.abbreviate(prefixLen + 2); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java index 66d773a05..ce4feae25 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java @@ -43,6 +43,8 @@ package org.eclipse.jgit.notes; +import java.io.IOException; + import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; @@ -96,6 +98,34 @@ class LeafBucket extends InMemoryNoteBucket { return 0 <= idx ? notes[idx].getData() : null; } + InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData, + ObjectReader or) throws IOException { + int p = search(noteOn); + if (0 <= p) { + if (noteData != null) { + notes[p].setData(noteData.copy()); + return this; + + } else { + System.arraycopy(notes, p + 1, notes, p, cnt - p - 1); + cnt--; + return 0 < cnt ? this : null; + } + + } else if (noteData != null) { + growIfFull(); + p = -(p + 1); + if (p < cnt) + System.arraycopy(notes, p, notes, p + 1, cnt - p); + notes[p] = new Note(noteOn, noteData.copy()); + cnt++; + return this; + + } else { + return this; + } + } + void parseOneEntry(AnyObjectId noteOn, AnyObjectId noteData) { growIfFull(); notes[cnt++] = new Note(noteOn, noteData.copy()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteBucket.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteBucket.java index 286f14084..f75fc1f94 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteBucket.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteBucket.java @@ -58,4 +58,7 @@ import org.eclipse.jgit.lib.ObjectReader; abstract class NoteBucket { abstract ObjectId get(AnyObjectId objId, ObjectReader reader) throws IOException; + + abstract InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData, + ObjectReader reader) throws IOException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java index d2f07279e..88ab48179 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java @@ -51,7 +51,9 @@ import org.eclipse.jgit.errors.LargeObjectException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; @@ -65,6 +67,17 @@ import org.eclipse.jgit.revwalk.RevTree; * shared {@code ObjectReader} at the proper times. */ public class NoteMap { + /** + * Construct a new empty note map. + * + * @return an empty note map. + */ + public static NoteMap newEmptyMap() { + NoteMap r = new NoteMap(null /* no reader */); + r.root = new LeafBucket(0); + return r; + } + /** * Load a collection of notes from a branch. * @@ -213,6 +226,89 @@ public class NoteMap { return null; } + /** + * Attach (or remove) a note on an object. + * + * If no note exists, a new note is stored. If a note already exists for the + * given object, it is replaced (or removed). + * + * This method only updates the map in memory. + * + * If the caller wants to attach a UTF-8 encoded string message to an + * object, {@link #set(AnyObjectId, String, ObjectInserter)} is a convenient + * way to encode and update a note in one step. + * + * @param noteOn + * the object to attach the note to. This same ObjectId can later + * be used as an argument to {@link #get(AnyObjectId)} or + * {@link #getCachedBytes(AnyObjectId, int)} to read back the + * {@code noteData}. + * @param noteData + * data to associate with the note. This must be the ObjectId of + * a blob that already exists in the repository. If null the note + * will be deleted, if present. + * @throws IOException + * a portion of the note space is not accessible. + */ + public void set(AnyObjectId noteOn, ObjectId noteData) throws IOException { + InMemoryNoteBucket newRoot = root.set(noteOn, noteData, reader); + if (newRoot == null) { + newRoot = new LeafBucket(0); + newRoot.nonNotes = root.nonNotes; + } + root = newRoot; + } + + /** + * Attach a note to an object. + * + * If no note exists, a new note is stored. If a note already exists for the + * given object, it is replaced (or removed). + * + * @param noteOn + * the object to attach the note to. This same ObjectId can later + * be used as an argument to {@link #get(AnyObjectId)} or + * {@link #getCachedBytes(AnyObjectId, int)} to read back the + * {@code noteData}. + * @param noteData + * text to store in the note. The text will be UTF-8 encoded when + * stored in the repository. If null the note will be deleted, if + * the empty string a note with the empty string will be stored. + * @param ins + * inserter to write the encoded {@code noteData} out as a blob. + * The caller must ensure the inserter is flushed before the + * updated note map is made available for reading. + * @throws IOException + * the note data could not be stored in the repository. + */ + public void set(AnyObjectId noteOn, String noteData, ObjectInserter ins) + throws IOException { + ObjectId dataId; + if (noteData != null) { + byte[] dataUTF8 = Constants.encode(noteData); + dataId = ins.insert(Constants.OBJ_BLOB, dataUTF8); + } else { + dataId = null; + } + set(noteOn, dataId); + } + + /** + * Remove a note from an object. + * + * If no note exists, no action is performed. + * + * This method only updates the map in memory. + * + * @param noteOn + * the object to remove the note from. + * @throws IOException + * a portion of the note space is not accessible. + */ + public void remove(AnyObjectId noteOn) throws IOException { + set(noteOn, null); + } + private void load(ObjectId rootTree) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { AbbreviatedObjectId none = AbbreviatedObjectId.fromString("");