From 3e2b9b691ed30d53750cc08c2b89577b78e6d19a Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Thu, 4 Nov 2010 18:14:00 -0700 Subject: [PATCH] Allow writing a NoteMap back to the repository This is necessary to allow applications to wrap the note tree in a commit and update the note branch with the new state. Change-Id: Idbd7ead4a1b16ae2b64a30a4a01a29cfed548cdf Signed-off-by: Shawn O. Pearce --- .../eclipse/jgit/junit/TestRepository.java | 14 ++- .../org/eclipse/jgit/notes/NoteMapTest.java | 106 ++++++++++++++++++ .../org/eclipse/jgit/notes/FanoutBucket.java | 46 ++++++++ .../org/eclipse/jgit/notes/LeafBucket.java | 38 +++++++ .../org/eclipse/jgit/notes/NonNoteEntry.java | 29 +++++ .../org/eclipse/jgit/notes/NoteBucket.java | 3 + .../src/org/eclipse/jgit/notes/NoteMap.java | 15 +++ 7 files changed, 249 insertions(+), 2 deletions(-) diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java index 7a95ccd36..e74af8cf7 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java @@ -183,6 +183,17 @@ public class TestRepository { now += secDelta * 1000L; } + /** + * Set the author and committer using {@link #getClock()}. + * + * @param c + * the commit builder to store. + */ + public void setAuthorAndCommitter(org.eclipse.jgit.lib.CommitBuilder c) { + c.setAuthor(new PersonIdent(author, new Date(now))); + c.setCommitter(new PersonIdent(committer, new Date(now))); + } + /** * Create a new blob object in the repository. * @@ -815,8 +826,7 @@ public class TestRepository { c = new org.eclipse.jgit.lib.CommitBuilder(); c.setParentIds(parents); - c.setAuthor(new PersonIdent(author, new Date(now))); - c.setCommitter(new PersonIdent(committer, new Date(now))); + setAuthorAndCommitter(c); c.setMessage(message); ObjectId commitId; 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 e1a6d9bf8..f22b02098 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 @@ -43,7 +43,10 @@ package org.eclipse.jgit.notes; +import java.io.IOException; + import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectReader; @@ -51,6 +54,8 @@ 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.revwalk.RevTree; +import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.util.RawParseUtils; public class NoteMapTest extends RepositoryTestCase { @@ -188,6 +193,59 @@ public class NoteMapTest extends RepositoryTestCase { assertEquals(exp, RawParseUtils.decode(act)); } + public void testWriteUnchangedFlat() 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) // + .add(".gitignore", "") // + .add("zoo-animals.txt", "") // + .create(); + tr.parseBody(r); + + NoteMap map = NoteMap.read(reader, r); + assertTrue("has note for a", map.contains(a)); + assertTrue("has note for b", map.contains(b)); + + RevCommit n = commitNoteMap(map); + assertNotSame("is new commit", r, n); + assertSame("same tree", r.getTree(), n.getTree()); + } + + public void testWriteUnchangedFanout2_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) // + .add(".gitignore", "") // + .add("zoo-animals.txt", "") // + .create(); + tr.parseBody(r); + + NoteMap map = NoteMap.read(reader, r); + assertTrue("has note for a", map.contains(a)); + assertTrue("has note for b", map.contains(b)); + + // This is a non-lazy map, so we'll be looking at the leaf buckets. + RevCommit n = commitNoteMap(map); + assertNotSame("is new commit", r, n); + assertSame("same tree", r.getTree(), n.getTree()); + + // Use a lazy-map for the next round of the same test. + map = NoteMap.read(reader, r); + n = commitNoteMap(map); + assertNotSame("is new commit", r, n); + assertSame("same tree", r.getTree(), n.getTree()); + } + public void testCreateFromEmpty() throws Exception { RevBlob a = tr.blob("a"); RevBlob b = tr.blob("b"); @@ -226,6 +284,8 @@ public class NoteMapTest extends RepositoryTestCase { RevCommit r = tr.commit() // .add(a.name(), data1) // .add(b.name(), data2) // + .add(".gitignore", "") // + .add("zoo-animals.txt", b) // .create(); tr.parseBody(r); @@ -250,6 +310,15 @@ public class NoteMapTest extends RepositoryTestCase { id.setByte(1, p); assertTrue("contains " + id, map.contains(id)); } + + RevCommit n = commitNoteMap(map); + map = NoteMap.read(reader, n); + assertEquals(data2, map.get(a)); + assertEquals(b, map.get(data1)); + assertFalse("no b", map.contains(b)); + assertFalse("no data2", map.contains(data2)); + assertEquals(b, TreeWalk + .forPath(reader, "zoo-animals.txt", n.getTree()).getObjectId(0)); } public void testEditFanout2_38() throws Exception { @@ -261,6 +330,8 @@ public class NoteMapTest extends RepositoryTestCase { RevCommit r = tr.commit() // .add(fanout(2, a.name()), data1) // .add(fanout(2, b.name()), data2) // + .add(".gitignore", "") // + .add("zoo-animals.txt", b) // .create(); tr.parseBody(r); @@ -274,11 +345,46 @@ public class NoteMapTest extends RepositoryTestCase { assertEquals(b, map.get(data1)); assertFalse("no b", map.contains(b)); assertFalse("no data2", map.contains(data2)); + RevCommit n = commitNoteMap(map); map.set(a, null); map.set(data1, null); assertFalse("no a", map.contains(a)); assertFalse("no data1", map.contains(data1)); + + map = NoteMap.read(reader, n); + assertEquals(data2, map.get(a)); + assertEquals(b, map.get(data1)); + assertFalse("no b", map.contains(b)); + assertFalse("no data2", map.contains(data2)); + assertEquals(b, TreeWalk + .forPath(reader, "zoo-animals.txt", n.getTree()).getObjectId(0)); + } + + public void testRemoveDeletesTreeFanout2_38() throws Exception { + RevBlob a = tr.blob("a"); + RevBlob data1 = tr.blob("data1"); + RevTree empty = tr.tree(); + + RevCommit r = tr.commit() // + .add(fanout(2, a.name()), data1) // + .create(); + tr.parseBody(r); + + NoteMap map = NoteMap.read(reader, r); + map.set(a, null); + + RevCommit n = commitNoteMap(map); + assertEquals("empty tree", empty, n.getTree()); + } + + private RevCommit commitNoteMap(NoteMap map) throws IOException { + tr.tick(600); + + CommitBuilder builder = new CommitBuilder(); + builder.setTreeId(map.writeTree(inserter)); + tr.setAuthorAndCommitter(builder); + return tr.getRevWalk().parseCommit(inserter.insert(builder)); } private static String fanout(int prefix, String name) { 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 760528618..208567690 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java @@ -43,12 +43,16 @@ package org.eclipse.jgit.notes; +import static org.eclipse.jgit.lib.FileMode.TREE; + import java.io.IOException; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.TreeFormatter; /** * A note tree holding only note subtrees, each named using a 2 digit hex name. @@ -136,6 +140,43 @@ class FanoutBucket extends InMemoryNoteBucket { } } + private static final byte[] hexchar = { '0', '1', '2', '3', '4', '5', '6', + '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + @Override + ObjectId writeTree(ObjectInserter inserter) throws IOException { + byte[] nameBuf = new byte[2]; + TreeFormatter fmt = new TreeFormatter(treeSize()); + NonNoteEntry e = nonNotes; + + for (int cell = 0; cell < 256; cell++) { + NoteBucket b = table[cell]; + if (b == null) + continue; + + nameBuf[0] = hexchar[cell >>> 4]; + nameBuf[1] = hexchar[cell & 0x0f]; + + while (e != null && e.pathCompare(nameBuf, 0, 2, TREE) < 0) { + e.format(fmt); + e = e.next; + } + + fmt.append(nameBuf, 0, 2, TREE, b.writeTree(inserter)); + } + + for (; e != null; e = e.next) + e.format(fmt); + return fmt.insert(inserter); + } + + private int treeSize() { + int sz = cnt * TreeFormatter.entrySize(TREE, 2); + for (NonNoteEntry e = nonNotes; e != null; e = e.next) + sz += e.treeEntrySize(); + return sz; + } + private int cell(AnyObjectId id) { return id.getByte(prefixLen >> 1); } @@ -158,6 +199,11 @@ class FanoutBucket extends InMemoryNoteBucket { return load(noteOn, or).set(noteOn, noteData, or); } + @Override + ObjectId writeTree(ObjectInserter inserter) { + return treeId; + } + 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 ce4feae25..40be45f43 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java @@ -43,11 +43,16 @@ package org.eclipse.jgit.notes; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH; +import static org.eclipse.jgit.lib.FileMode.REGULAR_FILE; + import java.io.IOException; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.TreeFormatter; /** * A note tree holding only notes, with no subtrees. @@ -126,6 +131,39 @@ class LeafBucket extends InMemoryNoteBucket { } } + @Override + ObjectId writeTree(ObjectInserter inserter) throws IOException { + byte[] nameBuf = new byte[OBJECT_ID_STRING_LENGTH]; + int nameLen = OBJECT_ID_STRING_LENGTH - prefixLen; + TreeFormatter fmt = new TreeFormatter(treeSize(nameLen)); + NonNoteEntry e = nonNotes; + + for (int i = 0; i < cnt; i++) { + Note n = notes[i]; + + n.copyTo(nameBuf, 0); + + while (e != null + && e.pathCompare(nameBuf, prefixLen, nameLen, REGULAR_FILE) < 0) { + e.format(fmt); + e = e.next; + } + + fmt.append(nameBuf, prefixLen, nameLen, REGULAR_FILE, n.getData()); + } + + for (; e != null; e = e.next) + e.format(fmt); + return fmt.insert(inserter); + } + + private int treeSize(final int nameLen) { + int sz = cnt * TreeFormatter.entrySize(REGULAR_FILE, nameLen); + for (NonNoteEntry e = nonNotes; e != null; e = e.next) + sz += e.treeEntrySize(); + return sz; + } + 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/NonNoteEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java index 20fd3a88a..6a2d44bca 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java @@ -68,4 +68,33 @@ class NonNoteEntry extends ObjectId { void format(TreeFormatter fmt) { fmt.append(name, mode, this); } + + int treeEntrySize() { + return TreeFormatter.entrySize(mode, name.length); + } + + int pathCompare(byte[] bBuf, int bPos, int bLen, FileMode bMode) { + return pathCompare(name, 0, name.length, mode, // + bBuf, bPos, bLen, bMode); + } + + private static int pathCompare(final byte[] aBuf, int aPos, final int aEnd, + final FileMode aMode, final byte[] bBuf, int bPos, final int bEnd, + final FileMode bMode) { + while (aPos < aEnd && bPos < bEnd) { + int cmp = (aBuf[aPos++] & 0xff) - (bBuf[bPos++] & 0xff); + if (cmp != 0) + return cmp; + } + + if (aPos < aEnd) + return (aBuf[aPos] & 0xff) - lastPathChar(bMode); + if (bPos < bEnd) + return lastPathChar(aMode) - (bBuf[bPos] & 0xff); + return 0; + } + + private static int lastPathChar(final FileMode mode) { + return FileMode.TREE.equals(mode.getBits()) ? '/' : '\0'; + } } 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 f75fc1f94..f5465148a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteBucket.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteBucket.java @@ -47,6 +47,7 @@ import java.io.IOException; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectReader; /** @@ -61,4 +62,6 @@ abstract class NoteBucket { abstract InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData, ObjectReader reader) throws IOException; + + abstract ObjectId writeTree(ObjectInserter inserter) 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 88ab48179..6a7b5cffb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java @@ -309,6 +309,21 @@ public class NoteMap { set(noteOn, null); } + /** + * Write this note map as a tree. + * + * @param inserter + * inserter to use when writing trees to the object database. + * Caller is responsible for flushing the inserter before trying + * to read the objects, or exposing them through a reference. + * @return the top level tree. + * @throws IOException + * a tree could not be written. + */ + public ObjectId writeTree(ObjectInserter inserter) throws IOException { + return root.writeTree(inserter); + } + private void load(ObjectId rootTree) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { AbbreviatedObjectId none = AbbreviatedObjectId.fromString("");