diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index 8d8508431..0a87fd323 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -26,6 +26,7 @@ Import-Package: junit.framework;version="[4.0.0,5.0.0)", org.eclipse.jgit.lib;version="[0.10.0,0.11.0)", org.eclipse.jgit.merge;version="[0.10.0,0.11.0)", org.eclipse.jgit.nls;version="[0.10.0,0.11.0)", + org.eclipse.jgit.notes;version="[0.10.0,0.11.0)", org.eclipse.jgit.patch;version="[0.10.0,0.11.0)", org.eclipse.jgit.pgm;version="[0.10.0,0.11.0)", org.eclipse.jgit.revplot;version="[0.10.0,0.11.0)", 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 new file mode 100644 index 000000000..786f1b94a --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/NoteMapTest.java @@ -0,0 +1,201 @@ +/* + * 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 org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryTestCase; +import org.eclipse.jgit.revwalk.RevBlob; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.util.RawParseUtils; + +public class NoteMapTest extends RepositoryTestCase { + private TestRepository tr; + + private ObjectReader reader; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + tr = new TestRepository(db); + reader = db.newObjectReader(); + } + + @Override + protected void tearDown() throws Exception { + reader.release(); + super.tearDown(); + } + + public void testReadFlatTwoNotes() 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); + assertNotNull("have map", map); + + assertTrue("has note for a", map.contains(a)); + assertTrue("has note for b", map.contains(b)); + assertEquals(data1, map.get(a)); + assertEquals(data2, map.get(b)); + + assertFalse("no note for data1", map.contains(data1)); + assertNull("no note for data1", map.get(data1)); + } + + public void testReadFanout2_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); + assertNotNull("have map", map); + + assertTrue("has note for a", map.contains(a)); + assertTrue("has note for b", map.contains(b)); + assertEquals(data1, map.get(a)); + assertEquals(data2, map.get(b)); + + assertFalse("no note for data1", map.contains(data1)); + assertNull("no note for data1", map.get(data1)); + } + + public void testReadFanout2_2_36() 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(4, a.name()), data1) // + .add(fanout(4, b.name()), data2) // + .create(); + tr.parseBody(r); + + NoteMap map = NoteMap.read(reader, r); + assertNotNull("have map", map); + + assertTrue("has note for a", map.contains(a)); + assertTrue("has note for b", map.contains(b)); + assertEquals(data1, map.get(a)); + assertEquals(data2, map.get(b)); + + assertFalse("no note for data1", map.contains(data1)); + assertNull("no note for data1", map.get(data1)); + } + + public void testReadFullyFannedOut() 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(38, a.name()), data1) // + .add(fanout(38, b.name()), data2) // + .create(); + tr.parseBody(r); + + NoteMap map = NoteMap.read(reader, r); + assertNotNull("have map", map); + + assertTrue("has note for a", map.contains(a)); + assertTrue("has note for b", map.contains(b)); + assertEquals(data1, map.get(a)); + assertEquals(data2, map.get(b)); + + assertFalse("no note for data1", map.contains(data1)); + assertNull("no note for data1", map.get(data1)); + } + + public void testGetCachedBytes() throws Exception { + final String exp = "this is test data"; + RevBlob a = tr.blob("a"); + RevBlob data = tr.blob(exp); + + RevCommit r = tr.commit() // + .add(a.name(), data) // + .create(); + tr.parseBody(r); + + NoteMap map = NoteMap.read(reader, r); + byte[] act = map.getCachedBytes(a, exp.length() * 4); + assertNotNull("has data for a", act); + assertEquals(exp, RawParseUtils.decode(act)); + } + + private static String fanout(int prefix, String name) { + StringBuilder r = new StringBuilder(); + int i = 0; + for (; i < prefix && i < name.length(); i += 2) { + if (i != 0) + r.append('/'); + r.append(name.charAt(i + 0)); + r.append(name.charAt(i + 1)); + } + if (i < name.length()) { + if (i != 0) + r.append('/'); + r.append(name.substring(i)); + } + return r.toString(); + } +} diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index 9e7362fd9..ba09e5883 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -17,6 +17,7 @@ Export-Package: org.eclipse.jgit;version="0.10.0", org.eclipse.jgit.lib;version="0.10.0", org.eclipse.jgit.merge;version="0.10.0", org.eclipse.jgit.nls;version="0.10.0", + org.eclipse.jgit.notes;version="0.10.0", org.eclipse.jgit.patch;version="0.10.0", org.eclipse.jgit.revplot;version="0.10.0", org.eclipse.jgit.revwalk;version="0.10.0", diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/Note.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/Note.java new file mode 100644 index 000000000..d365f9bfe --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/Note.java @@ -0,0 +1,78 @@ +/* + * 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 org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; + +/** In-memory representation of a single note attached to one object. */ +class Note extends ObjectId { + private ObjectId data; + + /** + * A Git note about the object referenced by {@code noteOn}. + * + * @param noteOn + * the object that has a note attached to it. + * @param noteData + * the actual note data contained in this note + */ + Note(AnyObjectId noteOn, ObjectId noteData) { + super(noteOn); + data = noteData; + } + + ObjectId getData() { + return data; + } + + void setData(ObjectId newData) { + data = newData; + } + + @Override + public String toString() { + return "Note[" + name() + " -> " + data.name() + "]"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java new file mode 100644 index 000000000..90e0480a8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java @@ -0,0 +1,275 @@ +/* + * 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 org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.LargeObjectException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdSubclassMap; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * Index of notes from a note branch. + * + * This class is not thread-safe, and relies on an {@link ObjectReader} that it + * borrows/shares with the caller. + * + * The current implementation is not very efficient. During load it recursively + * scans the entire note branch and indexes all annotated objects. If there are + * more than 256 notes in the branch, and not all of them will be accessed by + * the caller, this aggressive up-front loading probably takes too much time. + */ +public class NoteMap { + /** + * Load a collection of notes from a branch. + * + * @param reader + * reader to scan the note branch with. This reader may be + * retained by the NoteMap for the life of the map in order to + * support lazy loading of entries. + * @param commit + * the revision of the note branch to read. + * @return the note map read from the commit. + * @throws IOException + * the repository cannot be accessed through the reader. + * @throws CorruptObjectException + * a tree object is corrupt and cannot be read. + * @throws IncorrectObjectTypeException + * a tree object wasn't actually a tree. + * @throws MissingObjectException + * a reference tree object doesn't exist. + */ + public static NoteMap read(ObjectReader reader, RevCommit commit) + throws MissingObjectException, IncorrectObjectTypeException, + CorruptObjectException, IOException { + return read(reader, commit.getTree()); + } + + /** + * Load a collection of notes from a tree. + * + * @param reader + * reader to scan the note branch with. This reader may be + * retained by the NoteMap for the life of the map in order to + * support lazy loading of entries. + * @param tree + * the note tree to read. + * @return the note map read from the tree. + * @throws IOException + * the repository cannot be accessed through the reader. + * @throws CorruptObjectException + * a tree object is corrupt and cannot be read. + * @throws IncorrectObjectTypeException + * a tree object wasn't actually a tree. + * @throws MissingObjectException + * a reference tree object doesn't exist. + */ + public static NoteMap read(ObjectReader reader, RevTree tree) + throws MissingObjectException, IncorrectObjectTypeException, + CorruptObjectException, IOException { + return readTree(reader, tree); + } + + /** + * Load a collection of notes from a tree. + * + * @param reader + * reader to scan the note branch with. This reader may be + * retained by the NoteMap for the life of the map in order to + * support lazy loading of entries. + * @param treeId + * the note tree to read. + * @return the note map read from the tree. + * @throws IOException + * the repository cannot be accessed through the reader. + * @throws CorruptObjectException + * a tree object is corrupt and cannot be read. + * @throws IncorrectObjectTypeException + * a tree object wasn't actually a tree. + * @throws MissingObjectException + * a reference tree object doesn't exist. + */ + public static NoteMap readTree(ObjectReader reader, ObjectId treeId) + throws MissingObjectException, IncorrectObjectTypeException, + CorruptObjectException, IOException { + NoteMap map = new NoteMap(reader); + map.load(treeId); + return map; + } + + /** Borrowed reader to access the repository. */ + private final ObjectReader reader; + + /** All of the notes that have been loaded. */ + private ObjectIdSubclassMap notes = new ObjectIdSubclassMap(); + + private NoteMap(ObjectReader reader) { + this.reader = reader; + } + + /** + * Lookup a note for a specific ObjectId. + * + * @param id + * the object to look for. + * @return the note's blob ObjectId, or null if no note exists. + */ + public ObjectId get(AnyObjectId id) { + Note note = notes.get(id); + return note != null ? note.getData() : null; + } + + /** + * Determine if a note exists for the specified ObjectId. + * + * @param id + * the object to look for. + * @return true if a note exists; false if there is no note. + */ + public boolean contains(AnyObjectId id) { + return get(id) != null; + } + + /** + * Open and return the content of an object's note. + * + * This method assumes the note is fairly small and can be accessed + * efficiently. Larger notes should be accessed by streaming: + * + *
+	 * ObjectId dataId = thisMap.get(id);
+	 * if (dataId != null)
+	 * 	reader.open(dataId).openStream();
+	 * 
+ * + * @param id + * object to lookup the note of. + * @param sizeLimit + * maximum number of bytes to return. If the note data size is + * larger than this limit, LargeObjectException will be thrown. + * @return if a note is defined for {@code id}, the note content. If no note + * is defined, null. + * @throws LargeObjectException + * the note data is larger than {@code sizeLimit}. + * @throws MissingObjectException + * the note's blob does not exist in the repository. + * @throws IOException + * the note's blob cannot be read from the repository + */ + public byte[] getCachedBytes(AnyObjectId id, int sizeLimit) + throws LargeObjectException, MissingObjectException, IOException { + ObjectId dataId = get(id); + if (dataId != null) + return reader.open(dataId).getCachedBytes(sizeLimit); + else + return null; + } + + private void load(ObjectId treeId) throws MissingObjectException, + IncorrectObjectTypeException, CorruptObjectException, IOException { + try { + byte[] idBuf = new byte[Constants.OBJECT_ID_LENGTH]; + TreeWalk tw = new TreeWalk(reader); + tw.reset(); + tw.setRecursive(true); + tw.addTree(treeId); + + notes.clear(); + + while (tw.next()) { + ObjectId pathId = pathToId(idBuf, tw.getRawPath()); + if (pathId != null) + notes.add(new Note(pathId, tw.getObjectId(0))); + } + } finally { + reader.release(); + } + } + + private static ObjectId pathToId(byte[] rawIdBuf, byte[] path) { + int r = 0; + + for (int i = 0; i < path.length; i++) { + byte c = path[i]; + + // Skip embedded '/' in paths, these aren't part of a name. + if (c == '/') + continue; + + // We need at least 2 digits to consume in this cycle. + if (path.length < i + 2) + return null; + + // We cannot exceed an ObjectId raw length. + if (r == Constants.OBJECT_ID_STRING_LENGTH) + return null; + + int high, low; + try { + high = RawParseUtils.parseHexInt4(c); + low = RawParseUtils.parseHexInt4(path[++i]); + } catch (ArrayIndexOutOfBoundsException notHex) { + return null; + } + + rawIdBuf[r++] = (byte) ((high << 4) | low); + } + + // We need exactly the right number of input bytes. + if (r == Constants.OBJECT_ID_LENGTH) + return ObjectId.fromRaw(rawIdBuf); + else + return null; + } +}