Merge "Base64: Strip out code JGit doesn't use"
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 7a95ccd..e74af8c 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
@@ -184,6 +184,17 @@ public void tick(final int secDelta) {
 	}
 
 	/**
+	 * 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.
 	 *
 	 * @param content
@@ -815,8 +826,7 @@ public RevCommit create() throws Exception {
 
 				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/diff/SimilarityIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/SimilarityIndexTest.java
index 7e42e53..1da5828 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/SimilarityIndexTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/SimilarityIndexTest.java
@@ -48,10 +48,11 @@
 
 import junit.framework.TestCase;
 
+import org.eclipse.jgit.diff.SimilarityIndex.TableFullException;
 import org.eclipse.jgit.lib.Constants;
 
 public class SimilarityIndexTest extends TestCase {
-	public void testIndexingSmallObject() {
+	public void testIndexingSmallObject() throws TableFullException {
 		SimilarityIndex si = hash("" //
 				+ "A\n" //
 				+ "B\n" //
@@ -70,7 +71,8 @@ public void testIndexingSmallObject() {
 		assertEquals(2, si.count(si.findIndex(key_D)));
 	}
 
-	public void testIndexingLargeObject() throws IOException {
+	public void testIndexingLargeObject() throws IOException,
+			TableFullException {
 		byte[] in = ("" //
 				+ "A\n" //
 				+ "B\n" //
@@ -81,7 +83,7 @@ public void testIndexingLargeObject() throws IOException {
 		assertEquals(2, si.size());
 	}
 
-	public void testCommonScore_SameFiles() {
+	public void testCommonScore_SameFiles() throws TableFullException {
 		String text = "" //
 				+ "A\n" //
 				+ "B\n" //
@@ -96,21 +98,22 @@ public void testCommonScore_SameFiles() {
 		assertEquals(100, dst.score(src, 100));
 	}
 
-	public void testCommonScore_EmptyFiles() {
+	public void testCommonScore_EmptyFiles() throws TableFullException {
 		SimilarityIndex src = hash("");
 		SimilarityIndex dst = hash("");
 		assertEquals(0, src.common(dst));
 		assertEquals(0, dst.common(src));
 	}
 
-	public void testCommonScore_TotallyDifferentFiles() {
+	public void testCommonScore_TotallyDifferentFiles()
+			throws TableFullException {
 		SimilarityIndex src = hash("A\n");
 		SimilarityIndex dst = hash("D\n");
 		assertEquals(0, src.common(dst));
 		assertEquals(0, dst.common(src));
 	}
 
-	public void testCommonScore_SimiliarBy75() {
+	public void testCommonScore_SimiliarBy75() throws TableFullException {
 		SimilarityIndex src = hash("A\nB\nC\nD\n");
 		SimilarityIndex dst = hash("A\nB\nC\nQ\n");
 		assertEquals(6, src.common(dst));
@@ -120,10 +123,11 @@ public void testCommonScore_SimiliarBy75() {
 		assertEquals(75, dst.score(src, 100));
 	}
 
-	private static SimilarityIndex hash(String text) {
+	private static SimilarityIndex hash(String text) throws TableFullException {
 		SimilarityIndex src = new SimilarityIndex() {
 			@Override
-			void hash(byte[] raw, int ptr, final int end) {
+			void hash(byte[] raw, int ptr, final int end)
+					throws TableFullException {
 				while (ptr < end) {
 					int hash = raw[ptr] & 0xff;
 					int start = ptr;
@@ -143,7 +147,7 @@ void hash(byte[] raw, int ptr, final int end) {
 		return src;
 	}
 
-	private static int keyFor(String line) {
+	private static int keyFor(String line) throws TableFullException {
 		SimilarityIndex si = hash(line);
 		assertEquals("single line scored", 1, si.size());
 		return si.key(0);
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 0000000..68b0e2b
--- /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 786f1b9..d740ffa 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,12 +43,20 @@
 
 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.Constants;
+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;
 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 {
@@ -56,17 +64,21 @@ public class NoteMapTest extends RepositoryTestCase {
 
 	private ObjectReader reader;
 
+	private ObjectInserter inserter;
+
 	@Override
 	protected void setUp() throws Exception {
 		super.setUp();
 
 		tr = new TestRepository<Repository>(db);
 		reader = db.newObjectReader();
+		inserter = db.newObjectInserter();
 	}
 
 	@Override
 	protected void tearDown() throws Exception {
 		reader.release();
+		inserter.release();
 		super.tearDown();
 	}
 
@@ -182,6 +194,240 @@ public void testGetCachedBytes() throws Exception {
 		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");
+		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) //
+				.add(".gitignore", "") //
+				.add("zoo-animals.txt", b) //
+				.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));
+		}
+
+		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 {
+		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", b) //
+				.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));
+		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 testLeafSplitsWhenFull() throws Exception {
+		RevBlob data1 = tr.blob("data1");
+		MutableObjectId idBuf = new MutableObjectId();
+
+		RevCommit r = tr.commit() //
+				.add(data1.name(), data1) //
+				.create();
+		tr.parseBody(r);
+
+		NoteMap map = NoteMap.read(reader, r);
+		for (int i = 0; i < 254; i++) {
+			idBuf.setByte(Constants.OBJECT_ID_LENGTH - 1, i);
+			map.set(idBuf, data1);
+		}
+
+		RevCommit n = commitNoteMap(map);
+		TreeWalk tw = new TreeWalk(reader);
+		tw.reset(n.getTree());
+		while (tw.next())
+			assertFalse("no fan-out subtree", tw.isSubtree());
+
+		for (int i = 254; i < 256; i++) {
+			idBuf.setByte(Constants.OBJECT_ID_LENGTH - 1, i);
+			map.set(idBuf, data1);
+		}
+		idBuf.setByte(Constants.OBJECT_ID_LENGTH - 2, 1);
+		map.set(idBuf, data1);
+		n = commitNoteMap(map);
+
+		// The 00 bucket is fully split.
+		String path = fanout(38, idBuf.name());
+		tw = TreeWalk.forPath(reader, path, n.getTree());
+		assertNotNull("has " + path, tw);
+
+		// The other bucket is not.
+		path = fanout(2, data1.name());
+		tw = TreeWalk.forPath(reader, path, n.getTree());
+		assertNotNull("has " + path, tw);
+	}
+
+	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) {
 		StringBuilder r = new StringBuilder();
 		int i = 0;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
index 66218f6..dfaf588 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
@@ -57,6 +57,7 @@
 
 import org.eclipse.jgit.JGitText;
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.diff.SimilarityIndex.TableFullException;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -356,9 +357,17 @@ public List<DiffEntry> compute(ContentSource.Pair reader, ProgressMonitor pm)
 
 			if (pm == null)
 				pm = NullProgressMonitor.INSTANCE;
+
+			if (0 < breakScore)
 				breakModifies(reader, pm);
+
+			if (!added.isEmpty() && !deleted.isEmpty())
 				findExactRenames(pm);
+
+			if (!added.isEmpty() && !deleted.isEmpty())
 				findContentRenames(reader, pm);
+
+			if (0 < breakScore && !added.isEmpty() && !deleted.isEmpty())
 				rejoinModifies(pm);
 
 			entries.addAll(added);
@@ -382,9 +391,6 @@ public void reset() {
 
 	private void breakModifies(ContentSource.Pair reader, ProgressMonitor pm)
 			throws IOException {
-		if (breakScore <= 0)
-			return;
-
 		ArrayList<DiffEntry> newEntries = new ArrayList<DiffEntry>(entries.size());
 
 		pm.beginTask(JGitText.get().renamesBreakingModifies, entries.size());
@@ -445,29 +451,36 @@ private void rejoinModifies(ProgressMonitor pm) {
 
 	private int calculateModifyScore(ContentSource.Pair reader, DiffEntry d)
 			throws IOException {
-		SimilarityIndex src = new SimilarityIndex();
-		src.hash(reader.open(OLD, d));
-		src.sort();
+		try {
+			SimilarityIndex src = new SimilarityIndex();
+			src.hash(reader.open(OLD, d));
+			src.sort();
 
-		SimilarityIndex dst = new SimilarityIndex();
-		dst.hash(reader.open(NEW, d));
-		dst.sort();
-		return src.score(dst, 100);
+			SimilarityIndex dst = new SimilarityIndex();
+			dst.hash(reader.open(NEW, d));
+			dst.sort();
+			return src.score(dst, 100);
+		} catch (TableFullException tableFull) {
+			// If either table overflowed while being constructed, don't allow
+			// the pair to be broken. Returning 1 higher than breakScore will
+			// ensure its not similar, but not quite dissimilar enough to break.
+			//
+			overRenameLimit = true;
+			return breakScore + 1;
+		}
 	}
 
 	private void findContentRenames(ContentSource.Pair reader,
 			ProgressMonitor pm)
 			throws IOException {
 		int cnt = Math.max(added.size(), deleted.size());
-		if (cnt == 0)
-			return;
-
 		if (getRenameLimit() == 0 || cnt <= getRenameLimit()) {
 			SimilarityRenameDetector d;
 
 			d = new SimilarityRenameDetector(reader, deleted, added);
 			d.setRenameScore(getRenameScore());
 			d.compute(pm);
+			overRenameLimit |= d.isTableOverflow();
 			deleted = d.getLeftOverSources();
 			added = d.getLeftOverDestinations();
 			entries.addAll(d.getMatches());
@@ -478,9 +491,6 @@ private void findContentRenames(ContentSource.Pair reader,
 
 	@SuppressWarnings("unchecked")
 	private void findExactRenames(ProgressMonitor pm) {
-		if (added.isEmpty() || deleted.isEmpty())
-			return;
-
 		pm.beginTask(JGitText.get().renamesFindingExact, //
 				added.size() + added.size() + deleted.size()
 						+ added.size() * deleted.size());
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityIndex.java
index 6627268..17ccb97 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityIndex.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityIndex.java
@@ -65,8 +65,8 @@
  * file are discovered.
  */
 class SimilarityIndex {
-	/** The {@link #idHash} table stops growing at {@code 1 << MAX_HASH_BITS}. */
-	private static final int MAX_HASH_BITS = 17;
+	/** A special {@link TableFullException} used in place of OutOfMemoryError. */
+	private static final TableFullException TABLE_FULL_OUT_OF_MEMORY = new TableFullException();
 
 	/**
 	 * Shift to apply before storing a key.
@@ -76,20 +76,26 @@ class SimilarityIndex {
 	 */
 	private static final int KEY_SHIFT = 32;
 
+	/** Maximum value of the count field, also mask to extract the count. */
+	private static final long MAX_COUNT = (1L << KEY_SHIFT) - 1;
+
 	/** Total size of the file we hashed into the structure. */
 	private long fileSize;
 
 	/** Number of non-zero entries in {@link #idHash}. */
 	private int idSize;
 
+	/** {@link #idSize} that triggers {@link #idHash} to double in size. */
+	private int idGrowAt;
+
 	/**
 	 * Pairings of content keys and counters.
 	 * <p>
 	 * Slots in the table are actually two ints wedged into a single long. The
-	 * upper {@link #MAX_HASH_BITS} bits stores the content key, and the
-	 * remaining lower bits stores the number of bytes associated with that key.
-	 * Empty slots are denoted by 0, which cannot occur because the count cannot
-	 * be 0. Values can only be positive, which we enforce during key addition.
+	 * upper 32 bits stores the content key, and the remaining lower bits stores
+	 * the number of bytes associated with that key. Empty slots are denoted by
+	 * 0, which cannot occur because the count cannot be 0. Values can only be
+	 * positive, which we enforce during key addition.
 	 */
 	private long[] idHash;
 
@@ -99,6 +105,7 @@ class SimilarityIndex {
 	SimilarityIndex() {
 		idHashBits = 8;
 		idHash = new long[1 << idHashBits];
+		idGrowAt = growAt(idHashBits);
 	}
 
 	long getFileSize() {
@@ -109,7 +116,8 @@ void setFileSize(long size) {
 		fileSize = size;
 	}
 
-	void hash(ObjectLoader obj) throws MissingObjectException, IOException {
+	void hash(ObjectLoader obj) throws MissingObjectException, IOException,
+			TableFullException {
 		if (obj.isLarge()) {
 			ObjectStream in = obj.openStream();
 			try {
@@ -125,7 +133,7 @@ void hash(ObjectLoader obj) throws MissingObjectException, IOException {
 		}
 	}
 
-	void hash(byte[] raw, int ptr, final int end) {
+	void hash(byte[] raw, int ptr, final int end) throws TableFullException {
 		while (ptr < end) {
 			int hash = 5381;
 			int start = ptr;
@@ -141,7 +149,8 @@ void hash(byte[] raw, int ptr, final int end) {
 		}
 	}
 
-	void hash(InputStream in, long remaining) throws IOException {
+	void hash(InputStream in, long remaining) throws IOException,
+			TableFullException {
 		byte[] buf = new byte[4096];
 		int ptr = 0;
 		int cnt = 0;
@@ -190,11 +199,11 @@ int score(SimilarityIndex dst, int maxScore) {
 		return (int) ((common(dst) * maxScore) / max);
 	}
 
-	int common(SimilarityIndex dst) {
+	long common(SimilarityIndex dst) {
 		return common(this, dst);
 	}
 
-	private static int common(SimilarityIndex src, SimilarityIndex dst) {
+	private static long common(SimilarityIndex src, SimilarityIndex dst) {
 		int srcIdx = src.packedIndex(0);
 		int dstIdx = dst.packedIndex(0);
 		long[] srcHash = src.idHash;
@@ -202,12 +211,12 @@ private static int common(SimilarityIndex src, SimilarityIndex dst) {
 		return common(srcHash, srcIdx, dstHash, dstIdx);
 	}
 
-	private static int common(long[] srcHash, int srcIdx, //
+	private static long common(long[] srcHash, int srcIdx, //
 			long[] dstHash, int dstIdx) {
 		if (srcIdx == srcHash.length || dstIdx == dstHash.length)
 			return 0;
 
-		int common = 0;
+		long common = 0;
 		int srcKey = keyOf(srcHash[srcIdx]);
 		int dstKey = keyOf(dstHash[dstIdx]);
 
@@ -230,8 +239,8 @@ private static int common(long[] srcHash, int srcIdx, //
 					break;
 				srcKey = keyOf(srcHash[srcIdx]);
 
-			} else /* if (srcKey > dstKey) */{
-				// Regions of dst which do not appear in dst.
+			} else /* if (dstKey < srcKey) */{
+				// Regions of dst which do not appear in src.
 				if (++dstIdx == dstHash.length)
 					break;
 				dstKey = keyOf(dstHash[dstIdx]);
@@ -268,7 +277,7 @@ private int packedIndex(int idx) {
 		return (idHash.length - idSize) + idx;
 	}
 
-	void add(int key, int cnt) {
+	void add(int key, int cnt) throws TableFullException {
 		key = (key * 0x9e370001) >>> 1; // Mix bits and ensure not negative.
 
 		int j = slot(key);
@@ -276,18 +285,20 @@ void add(int key, int cnt) {
 			long v = idHash[j];
 			if (v == 0) {
 				// Empty slot in the table, store here.
-				if (shouldGrow()) {
+				if (idGrowAt <= idSize) {
 					grow();
 					j = slot(key);
 					continue;
 				}
-				idHash[j] = (((long) key) << KEY_SHIFT) | cnt;
+				idHash[j] = pair(key, cnt);
 				idSize++;
 				return;
 
 			} else if (keyOf(v) == key) {
-				// Same key, increment the counter.
-				idHash[j] = v + cnt;
+				// Same key, increment the counter. If it overflows, fail
+				// indexing to prevent the key from being impacted.
+				//
+				idHash[j] = pair(key, countOf(v) + cnt);
 				return;
 
 			} else if (++j >= idHash.length) {
@@ -296,6 +307,12 @@ void add(int key, int cnt) {
 		}
 	}
 
+	private static long pair(int key, long cnt) throws TableFullException {
+		if (MAX_COUNT < cnt)
+			throw new TableFullException();
+		return (((long) key) << KEY_SHIFT) | cnt;
+	}
+
 	private int slot(int key) {
 		// We use 31 - idHashBits because the upper bit was already forced
 		// to be 0 and we want the remaining high bits to be used as the
@@ -304,16 +321,26 @@ private int slot(int key) {
 		return key >>> (31 - idHashBits);
 	}
 
-	private boolean shouldGrow() {
-		return idHashBits < MAX_HASH_BITS && idHash.length <= idSize * 2;
+	private static int growAt(int idHashBits) {
+		return (1 << idHashBits) * (idHashBits - 3) / idHashBits;
 	}
 
-	private void grow() {
+	private void grow() throws TableFullException {
+		if (idHashBits == 30)
+			throw new TableFullException();
+
 		long[] oldHash = idHash;
 		int oldSize = idHash.length;
 
 		idHashBits++;
-		idHash = new long[1 << idHashBits];
+		idGrowAt = growAt(idHashBits);
+
+		try {
+			idHash = new long[1 << idHashBits];
+		} catch (OutOfMemoryError noMemory) {
+			throw TABLE_FULL_OUT_OF_MEMORY;
+		}
+
 		for (int i = 0; i < oldSize; i++) {
 			long v = oldHash[i];
 			if (v != 0) {
@@ -330,7 +357,11 @@ private static int keyOf(long v) {
 		return (int) (v >>> KEY_SHIFT);
 	}
 
-	private static int countOf(long v) {
-		return (int) v;
+	private static long countOf(long v) {
+		return v & MAX_COUNT;
+	}
+
+	static class TableFullException extends Exception {
+		private static final long serialVersionUID = 1L;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
index 3075c22..3a98475 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
@@ -49,10 +49,12 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.BitSet;
 import java.util.List;
 
 import org.eclipse.jgit.JGitText;
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.diff.SimilarityIndex.TableFullException;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ProgressMonitor;
@@ -110,6 +112,9 @@ class SimilarityRenameDetector {
 	/** Score a pair must exceed to be considered a rename. */
 	private int renameScore = 60;
 
+	/** Set if any {@link SimilarityIndex.TableFullException} occurs. */
+	private boolean tableOverflow;
+
 	private List<DiffEntry> out;
 
 	SimilarityRenameDetector(ContentSource.Pair reader, List<DiffEntry> srcs,
@@ -182,6 +187,10 @@ List<DiffEntry> getLeftOverDestinations() {
 		return dsts;
 	}
 
+	boolean isTableOverflow() {
+		return tableOverflow;
+	}
+
 	private static List<DiffEntry> compactSrcList(List<DiffEntry> in) {
 		ArrayList<DiffEntry> r = new ArrayList<DiffEntry>(in.size());
 		for (DiffEntry e : in) {
@@ -208,25 +217,22 @@ private int buildMatrix(ProgressMonitor pm) throws IOException {
 
 		long[] srcSizes = new long[srcs.size()];
 		long[] dstSizes = new long[dsts.size()];
-
-		// Init the size arrays to some value that indicates that we haven't
-		// calculated the size yet. Since sizes cannot be negative, -1 will work
-		Arrays.fill(srcSizes, -1);
-		Arrays.fill(dstSizes, -1);
+		BitSet dstTooLarge = null;
 
 		// Consider each pair of files, if the score is above the minimum
 		// threshold we need record that scoring in the matrix so we can
 		// later find the best matches.
 		//
 		int mNext = 0;
-		for (int srcIdx = 0; srcIdx < srcs.size(); srcIdx++) {
+		SRC: for (int srcIdx = 0; srcIdx < srcs.size(); srcIdx++) {
 			DiffEntry srcEnt = srcs.get(srcIdx);
 			if (!isFile(srcEnt.oldMode)) {
 				pm.update(dsts.size());
 				continue;
 			}
 
-			SimilarityIndex s = hash(OLD, srcEnt);
+			SimilarityIndex s = null;
+
 			for (int dstIdx = 0; dstIdx < dsts.size(); dstIdx++) {
 				DiffEntry dstEnt = dsts.get(dstIdx);
 
@@ -240,15 +246,20 @@ private int buildMatrix(ProgressMonitor pm) throws IOException {
 					continue;
 				}
 
+				if (dstTooLarge != null && dstTooLarge.get(dstIdx)) {
+					pm.update(1);
+					continue;
+				}
+
 				long srcSize = srcSizes[srcIdx];
-				if (srcSize < 0) {
-					srcSize = size(OLD, srcEnt);
+				if (srcSize == 0) {
+					srcSize = size(OLD, srcEnt) + 1;
 					srcSizes[srcIdx] = srcSize;
 				}
 
 				long dstSize = dstSizes[dstIdx];
-				if (dstSize < 0) {
-					dstSize = size(NEW, dstEnt);
+				if (dstSize == 0) {
+					dstSize = size(NEW, dstEnt) + 1;
 					dstSizes[dstIdx] = dstSize;
 				}
 
@@ -260,7 +271,27 @@ private int buildMatrix(ProgressMonitor pm) throws IOException {
 					continue;
 				}
 
-				SimilarityIndex d = hash(NEW, dstEnt);
+				if (s == null) {
+					try {
+						s = hash(OLD, srcEnt);
+					} catch (TableFullException tableFull) {
+						tableOverflow = true;
+						continue SRC;
+					}
+				}
+
+				SimilarityIndex d;
+				try {
+					d = hash(NEW, dstEnt);
+				} catch (TableFullException tableFull) {
+					if (dstTooLarge == null)
+						dstTooLarge = new BitSet(dsts.size());
+					dstTooLarge.set(dstIdx);
+					tableOverflow = true;
+					pm.update(1);
+					continue;
+				}
+
 				int contentScore = s.score(d, 10000);
 
 				// nameScore returns a value between 0 and 100, but we want it
@@ -336,7 +367,7 @@ static int nameScore(String a, String b) {
 	}
 
 	private SimilarityIndex hash(DiffEntry.Side side, DiffEntry ent)
-			throws IOException {
+			throws IOException, TableFullException {
 		SimilarityIndex r = new SimilarityIndex();
 		r.hash(reader.open(side, ent));
 		r.sort();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
index bc51aa5..d262972 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -144,8 +144,8 @@ public List<String> getRemoved() {
 	}
 
 	/**
-	 * Constructs a DirCacheCeckout for fast-forwarding from one tree to
-	 * another, merging it with the index
+	 * Constructs a DirCacheCeckout for merging and checking out two trees (HEAD
+	 * and mergeCommitTree) and the index.
 	 *
 	 * @param repo
 	 *            the repository in which we do the checkout
@@ -170,9 +170,9 @@ public DirCacheCheckout(Repository repo, ObjectId headCommitTree, DirCache dc,
 	}
 
 	/**
-	 * Constructs a DirCacheCeckout for checking out one tree, merging with the
-	 * index. As iterator over the working tree this constructor creates a
-	 * standard {@link FileTreeIterator}
+	 * Constructs a DirCacheCeckout for merging and checking out two trees (HEAD
+	 * and mergeCommitTree) and the index. As iterator over the working tree
+	 * this constructor creates a standard {@link FileTreeIterator}
 	 *
 	 * @param repo
 	 *            the repository in which we do the checkout
@@ -184,14 +184,54 @@ public DirCacheCheckout(Repository repo, ObjectId headCommitTree, DirCache dc,
 	 *            the id of the tree of the
 	 * @throws IOException
 	 */
-	public DirCacheCheckout(Repository repo, ObjectId headCommitTree, DirCache dc,
-			ObjectId mergeCommitTree) throws IOException {
+	public DirCacheCheckout(Repository repo, ObjectId headCommitTree,
+			DirCache dc, ObjectId mergeCommitTree) throws IOException {
 		this(repo, headCommitTree, dc, mergeCommitTree, new FileTreeIterator(
 				repo.getWorkTree(), repo.getFS(),
 				WorkingTreeOptions.createDefaultInstance()));
 	}
 
 	/**
+	 * Constructs a DirCacheCeckout for checking out one tree, merging with the
+	 * index.
+	 *
+	 * @param repo
+	 *            the repository in which we do the checkout
+	 * @param dc
+	 *            the (already locked) Dircache for this repo
+	 * @param mergeCommitTree
+	 *            the id of the tree we want to fast-forward to
+	 * @param workingTree
+	 *            an iterator over the repositories Working Tree
+	 * @throws IOException
+	 */
+	public DirCacheCheckout(Repository repo, DirCache dc,
+			ObjectId mergeCommitTree, WorkingTreeIterator workingTree)
+			throws IOException {
+		this(repo, null, dc, mergeCommitTree, workingTree);
+	}
+
+	/**
+	 * Constructs a DirCacheCeckout for checking out one tree, merging with the
+	 * index. As iterator over the working tree this constructor creates a
+	 * standard {@link FileTreeIterator}
+	 *
+	 * @param repo
+	 *            the repository in which we do the checkout
+	 * @param dc
+	 *            the (already locked) Dircache for this repo
+	 * @param mergeCommitTree
+	 *            the id of the tree of the
+	 * @throws IOException
+	 */
+	public DirCacheCheckout(Repository repo, DirCache dc,
+			ObjectId mergeCommitTree) throws IOException {
+		this(repo, null, dc, mergeCommitTree, new FileTreeIterator(
+				repo.getWorkTree(), repo.getFS(),
+				WorkingTreeOptions.createDefaultInstance()));
+	}
+
+	/**
 	 * Scan head, index and merge tree. Used during normal checkout or merge
 	 * operations.
 	 *
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 85337c8..e1b96ea 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,19 @@
 
 package org.eclipse.jgit.notes;
 
+import static org.eclipse.jgit.lib.FileMode.TREE;
+
 import java.io.IOException;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
 
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.MutableObjectId;
 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.
@@ -84,6 +91,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 +101,7 @@ class FanoutBucket extends InMemoryNoteBucket {
 
 	void parseOneEntry(int cell, ObjectId id) {
 		table[cell] = new LazyNoteBucket(id);
+		cnt++;
 	}
 
 	@Override
@@ -99,6 +110,178 @@ ObjectId get(AnyObjectId objId, ObjectReader or) throws IOException {
 		return b != null ? b.get(objId, or) : null;
 	}
 
+	@Override
+	Iterator<Note> iterator(AnyObjectId objId, final ObjectReader reader)
+			throws IOException {
+		final MutableObjectId id = new MutableObjectId();
+		id.fromObjectId(objId);
+
+		return new Iterator<Note>() {
+			private int cell;
+
+			private Iterator<Note> itr;
+
+			public boolean hasNext() {
+				if (itr != null && itr.hasNext())
+					return true;
+
+				for (; cell < table.length; cell++) {
+					NoteBucket b = table[cell];
+					if (b == null)
+						continue;
+
+					try {
+						id.setByte(prefixLen >> 1, cell);
+						itr = b.iterator(id, reader);
+					} catch (IOException err) {
+						throw new RuntimeException(err);
+					}
+
+					if (itr.hasNext()) {
+						cell++;
+						return true;
+					}
+				}
+				return false;
+			}
+
+			public Note next() {
+				if (hasNext())
+					return itr.next();
+				else
+					throw new NoSuchElementException();
+			}
+
+			public void remove() {
+				throw new UnsupportedOperationException();
+			}
+		};
+	}
+
+	@Override
+	int estimateSize(AnyObjectId noteOn, ObjectReader or) throws IOException {
+		// If most of this fan-out is full, estimate it should still be split.
+		if (LeafBucket.MAX_SIZE * 3 / 4 <= cnt)
+			return 1 + LeafBucket.MAX_SIZE;
+
+		// Due to the uniform distribution of ObjectIds, having less nodes full
+		// indicates a good chance the total number of children below here
+		// is less than the MAX_SIZE split point. Get a more accurate count.
+
+		MutableObjectId id = new MutableObjectId();
+		id.fromObjectId(noteOn);
+
+		int sz = 0;
+		for (int cell = 0; cell < 256; cell++) {
+			NoteBucket b = table[cell];
+			if (b == null)
+				continue;
+
+			id.setByte(prefixLen >> 1, cell);
+			sz += b.estimateSize(id, or);
+			if (LeafBucket.MAX_SIZE < sz)
+				break;
+		}
+		return sz;
+	}
+
+	@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;
+
+				if (estimateSize(noteOn, or) < LeafBucket.MAX_SIZE) {
+					// We are small enough to just contract to a single leaf.
+					InMemoryNoteBucket r = new LeafBucket(prefixLen);
+					for (Iterator<Note> i = iterator(noteOn, or); i.hasNext();)
+						r = r.append(i.next());
+					r.nonNotes = nonNotes;
+					return r;
+				}
+
+				return this;
+
+			} else if (n != b) {
+				table[cell] = n;
+			}
+			return this;
+		}
+	}
+
+	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;
+	}
+
+	@Override
+	InMemoryNoteBucket append(Note note) {
+		int cell = cell(note);
+		InMemoryNoteBucket b = (InMemoryNoteBucket) table[cell];
+
+		if (b == null) {
+			LeafBucket n = new LeafBucket(prefixLen + 2);
+			table[cell] = n.append(note);
+			cnt++;
+
+		} else {
+			InMemoryNoteBucket n = b.append(note);
+			if (n != b)
+				table[cell] = n;
+		}
+		return this;
+	}
+
 	private int cell(AnyObjectId id) {
 		return id.getByte(prefixLen >> 1);
 	}
@@ -115,6 +298,28 @@ ObjectId get(AnyObjectId objId, ObjectReader or) throws IOException {
 			return load(objId, or).get(objId, or);
 		}
 
+		@Override
+		Iterator<Note> iterator(AnyObjectId objId, ObjectReader reader)
+				throws IOException {
+			return load(objId, reader).iterator(objId, reader);
+		}
+
+		@Override
+		int estimateSize(AnyObjectId objId, ObjectReader or) throws IOException {
+			return load(objId, or).estimateSize(objId, or);
+		}
+
+		@Override
+		InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
+				ObjectReader or) throws IOException {
+			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/InMemoryNoteBucket.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/InMemoryNoteBucket.java
index 7d0df73..0f45f82 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/InMemoryNoteBucket.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/InMemoryNoteBucket.java
@@ -54,7 +54,20 @@ abstract class InMemoryNoteBucket extends NoteBucket {
 	 */
 	final int prefixLen;
 
+	/**
+	 * Chain of non-note tree entries found at this path in the tree.
+	 *
+	 * During parsing of a note tree into the in-memory representation,
+	 * {@link NoteParser} keeps track of all non-note tree entries and stores
+	 * them here as a sorted linked list. That list can be merged back with the
+	 * note data that is held by the subclass, allowing the tree to be
+	 * recreated.
+	 */
+	NonNoteEntry nonNotes;
+
 	InMemoryNoteBucket(int prefixLen) {
 		this.prefixLen = prefixLen;
 	}
+
+	abstract InMemoryNoteBucket append(Note note);
 }
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 66d773a..af6c6f4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java
@@ -43,9 +43,18 @@
 
 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 java.util.Iterator;
+import java.util.NoSuchElementException;
+
 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.
@@ -64,6 +73,8 @@
  * A LeafBucket must be parsed from a tree object by {@link NoteParser}.
  */
 class LeafBucket extends InMemoryNoteBucket {
+	static final int MAX_SIZE = 256;
+
 	/** All note blobs in this bucket, sorted sequentially. */
 	private Note[] notes;
 
@@ -96,11 +107,116 @@ ObjectId get(AnyObjectId objId, ObjectReader or) {
 		return 0 <= idx ? notes[idx].getData() : null;
 	}
 
+	@Override
+	Iterator<Note> iterator(AnyObjectId objId, ObjectReader reader) {
+		return new Iterator<Note>() {
+			private int idx;
+
+			public boolean hasNext() {
+				return idx < cnt;
+			}
+
+			public Note next() {
+				if (hasNext())
+					return notes[idx++];
+				else
+					throw new NoSuchElementException();
+			}
+
+			public void remove() {
+				throw new UnsupportedOperationException();
+			}
+		};
+	}
+
+	@Override
+	int estimateSize(AnyObjectId noteOn, ObjectReader or) throws IOException {
+		return cnt;
+	}
+
+	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) {
+			if (shouldSplit()) {
+				return split().set(noteOn, noteData, or);
+
+			} else {
+				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;
+		}
+	}
+
+	@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());
 	}
 
+	@Override
+	InMemoryNoteBucket append(Note note) {
+		if (shouldSplit()) {
+			return split().append(note);
+
+		} else {
+			growIfFull();
+			notes[cnt++] = note;
+			return this;
+		}
+	}
+
 	private void growIfFull() {
 		if (notes.length == cnt) {
 			Note[] n = new Note[notes.length * 2];
@@ -108,4 +224,16 @@ private void growIfFull() {
 			notes = n;
 		}
 	}
+
+	private boolean shouldSplit() {
+		return MAX_SIZE <= cnt && prefixLen + 2 < OBJECT_ID_STRING_LENGTH;
+	}
+
+	private InMemoryNoteBucket split() {
+		FanoutBucket n = new FanoutBucket(prefixLen);
+		for (int i = 0; i < cnt; i++)
+			n.append(notes[i]);
+		n.nonNotes = nonNotes;
+		return n;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java
new file mode 100644
index 0000000..6a2d44b
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java
@@ -0,0 +1,100 @@
+/*
+ * 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.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.TreeFormatter;
+
+/** A tree entry found in a note branch that isn't a valid note. */
+class NonNoteEntry extends ObjectId {
+	/** Name of the entry in the tree, in raw format. */
+	private final byte[] name;
+
+	/** Mode of the entry as parsed from the tree. */
+	private final FileMode mode;
+
+	/** The next non-note entry in the same tree, as defined by tree order. */
+	NonNoteEntry next;
+
+	NonNoteEntry(byte[] name, FileMode mode, AnyObjectId id) {
+		super(id);
+		this.name = name;
+		this.mode = mode;
+	}
+
+	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 286f140..defc37d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteBucket.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteBucket.java
@@ -44,9 +44,11 @@
 package org.eclipse.jgit.notes;
 
 import java.io.IOException;
+import java.util.Iterator;
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
 
 /**
@@ -58,4 +60,15 @@
 abstract class NoteBucket {
 	abstract ObjectId get(AnyObjectId objId, ObjectReader reader)
 			throws IOException;
+
+	abstract Iterator<Note> iterator(AnyObjectId objId, ObjectReader reader)
+			throws IOException;
+
+	abstract int estimateSize(AnyObjectId noteOn, ObjectReader or)
+			throws IOException;
+
+	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 d2f0727..6a7b5cf 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.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;
@@ -66,6 +68,17 @@
  */
 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.
 	 *
 	 * @param reader
@@ -213,6 +226,104 @@ public boolean contains(AnyObjectId id) throws IOException {
 			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);
+	}
+
+	/**
+	 * 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("");
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteParser.java
index 04e260a..d916e35 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteParser.java
@@ -52,6 +52,7 @@
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.MutableObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -87,13 +88,17 @@ final class NoteParser extends CanonicalTreeParser {
 	static InMemoryNoteBucket parse(AbbreviatedObjectId prefix,
 			final ObjectId treeId, final ObjectReader reader)
 			throws IOException {
-		return new NoteParser(prefix, reader, treeId).parseTree();
+		return new NoteParser(prefix, reader, treeId).parse();
 	}
 
 	private final AbbreviatedObjectId prefix;
 
 	private final int pathPadding;
 
+	private NonNoteEntry firstNonNote;
+
+	private NonNoteEntry lastNonNote;
+
 	private NoteParser(AbbreviatedObjectId p, ObjectReader r, ObjectId t)
 			throws IncorrectObjectTypeException, IOException {
 		super(encodeASCII(p.name()), r, t);
@@ -106,6 +111,12 @@ private NoteParser(AbbreviatedObjectId p, ObjectReader r, ObjectId t)
 			System.arraycopy(path, 0, path, pathPadding, prefix.length());
 	}
 
+	private InMemoryNoteBucket parse() {
+		InMemoryNoteBucket r = parseTree();
+		r.nonNotes = firstNonNote;
+		return r;
+	}
+
 	private InMemoryNoteBucket parseTree() {
 		for (; !eof(); next(1)) {
 			if (pathLen == pathPadding + OBJECT_ID_STRING_LENGTH && isHex())
@@ -113,6 +124,9 @@ private InMemoryNoteBucket parseTree() {
 
 			else if (getNameLength() == 2 && isHex() && isTree())
 				return parseFanoutTree();
+
+			else
+				storeNonNote();
 		}
 
 		// If we cannot determine the style used, assume its a leaf.
@@ -126,6 +140,8 @@ private LeafBucket parseLeafTree() {
 		for (; !eof(); next(1)) {
 			if (parseObjectId(idBuf))
 				leaf.parseOneEntry(idBuf, getEntryObjectId());
+			else
+				storeNonNote();
 		}
 
 		return leaf;
@@ -150,6 +166,8 @@ private FanoutBucket parseFanoutTree() {
 			final int cell = parseFanoutCell();
 			if (0 <= cell)
 				fanout.parseOneEntry(cell, getEntryObjectId());
+			else
+				storeNonNote();
 		}
 
 		return fanout;
@@ -168,6 +186,21 @@ private int parseFanoutCell() {
 		}
 	}
 
+	private void storeNonNote() {
+		ObjectId id = getEntryObjectId();
+		FileMode fileMode = getEntryFileMode();
+
+		byte[] name = new byte[getNameLength()];
+		getName(name, 0);
+
+		NonNoteEntry ent = new NonNoteEntry(name, fileMode, id);
+		if (firstNonNote == null)
+			firstNonNote = ent;
+		if (lastNonNote != null)
+			lastNonNote.next = ent;
+		lastNonNote = ent;
+	}
+
 	private boolean isTree() {
 		return TREE.equals(mode);
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/LockFile.java
index e8bc3e2..6199f4c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/LockFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/LockFile.java
@@ -44,7 +44,6 @@
 
 package org.eclipse.jgit.storage.file;
 
-import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -52,8 +51,9 @@
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.nio.channels.FileLock;
-import java.nio.channels.OverlappingFileLockException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
 import java.text.MessageFormat;
 
 import org.eclipse.jgit.JGitText;
@@ -85,14 +85,14 @@ public boolean accept(File dir, String name) {
 
 	private final File lck;
 
-	private FileLock fLck;
-
 	private boolean haveLck;
 
 	private FileOutputStream os;
 
 	private boolean needStatInformation;
 
+	private boolean fsync;
+
 	private long commitLastModified;
 
 	private final FS fs;
@@ -127,23 +127,6 @@ public boolean lock() throws IOException {
 			haveLck = true;
 			try {
 				os = new FileOutputStream(lck);
-				try {
-					fLck = os.getChannel().tryLock();
-					if (fLck == null)
-						throw new OverlappingFileLockException();
-				} catch (OverlappingFileLockException ofle) {
-					// We cannot use unlock() here as this file is not
-					// held by us, but we thought we created it. We must
-					// not delete it, as it belongs to some other process.
-					//
-					haveLck = false;
-					try {
-						os.close();
-					} catch (IOException ioe) {
-						// Fail by returning haveLck = false.
-					}
-					os = null;
-				}
 			} catch (IOException ioe) {
 				unlock();
 				throw ioe;
@@ -192,10 +175,21 @@ public void copyCurrentContent() throws IOException {
 		try {
 			final FileInputStream fis = new FileInputStream(ref);
 			try {
-				final byte[] buf = new byte[2048];
-				int r;
-				while ((r = fis.read(buf)) >= 0)
-					os.write(buf, 0, r);
+				if (fsync) {
+					FileChannel in = fis.getChannel();
+					long pos = 0;
+					long cnt = in.size();
+					while (0 < cnt) {
+						long r = os.getChannel().transferFrom(in, pos, cnt);
+						pos += r;
+						cnt -= r;
+					}
+				} else {
+					final byte[] buf = new byte[2048];
+					int r;
+					while ((r = fis.read(buf)) >= 0)
+						os.write(buf, 0, r);
+				}
 			} finally {
 				fis.close();
 			}
@@ -229,26 +223,10 @@ public void copyCurrentContent() throws IOException {
 	 *             before throwing the underlying exception to the caller.
 	 */
 	public void write(final ObjectId id) throws IOException {
-		requireLock();
-		try {
-			final BufferedOutputStream b;
-			b = new BufferedOutputStream(os, Constants.OBJECT_ID_STRING_LENGTH + 1);
-			id.copyTo(b);
-			b.write('\n');
-			b.flush();
-			fLck.release();
-			b.close();
-			os = null;
-		} catch (IOException ioe) {
-			unlock();
-			throw ioe;
-		} catch (RuntimeException ioe) {
-			unlock();
-			throw ioe;
-		} catch (Error ioe) {
-			unlock();
-			throw ioe;
-		}
+		byte[] buf = new byte[Constants.OBJECT_ID_STRING_LENGTH + 1];
+		id.copyTo(buf, 0);
+		buf[Constants.OBJECT_ID_STRING_LENGTH] = '\n';
+		write(buf);
 	}
 
 	/**
@@ -268,9 +246,15 @@ public void write(final ObjectId id) throws IOException {
 	public void write(final byte[] content) throws IOException {
 		requireLock();
 		try {
-			os.write(content);
-			os.flush();
-			fLck.release();
+			if (fsync) {
+				FileChannel fc = os.getChannel();
+				ByteBuffer buf = ByteBuffer.wrap(content);
+				while (0 < buf.remaining())
+					fc.write(buf);
+				fc.force(true);
+			} else {
+				os.write(content);
+			}
 			os.close();
 			os = null;
 		} catch (IOException ioe) {
@@ -296,34 +280,36 @@ public void write(final byte[] content) throws IOException {
 	 */
 	public OutputStream getOutputStream() {
 		requireLock();
+
+		final OutputStream out;
+		if (fsync)
+			out = Channels.newOutputStream(os.getChannel());
+		else
+			out = os;
+
 		return new OutputStream() {
 			@Override
 			public void write(final byte[] b, final int o, final int n)
 					throws IOException {
-				os.write(b, o, n);
+				out.write(b, o, n);
 			}
 
 			@Override
 			public void write(final byte[] b) throws IOException {
-				os.write(b);
+				out.write(b);
 			}
 
 			@Override
 			public void write(final int b) throws IOException {
-				os.write(b);
-			}
-
-			@Override
-			public void flush() throws IOException {
-				os.flush();
+				out.write(b);
 			}
 
 			@Override
 			public void close() throws IOException {
 				try {
-					os.flush();
-					fLck.release();
-					os.close();
+					if (fsync)
+						os.getChannel().force(true);
+					out.close();
 					os = null;
 				} catch (IOException ioe) {
 					unlock();
@@ -357,6 +343,16 @@ public void setNeedStatInformation(final boolean on) {
 	}
 
 	/**
+	 * Request that {@link #commit()} force dirty data to the drive.
+	 *
+	 * @param on
+	 *            true if dirty data should be forced to the drive.
+	 */
+	public void setFSync(final boolean on) {
+		fsync = on;
+	}
+
+	/**
 	 * Wait until the lock file information differs from the old file.
 	 * <p>
 	 * This method tests both the length and the last modification date. If both
@@ -447,14 +443,6 @@ public long getCommitLastModified() {
 	 */
 	public void unlock() {
 		if (os != null) {
-			if (fLck != null) {
-				try {
-					fLck.release();
-				} catch (IOException ioe) {
-					// Huh?
-				}
-				fLck = null;
-			}
 			try {
 				os.close();
 			} catch (IOException ioe) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/ObjectDirectoryInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/ObjectDirectoryInserter.java
index 16cb8aa..d922beb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/ObjectDirectoryInserter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/ObjectDirectoryInserter.java
@@ -52,6 +52,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.channels.Channels;
 import java.security.DigestOutputStream;
 import java.security.MessageDigest;
 import java.util.zip.Deflater;
@@ -60,7 +61,6 @@
 import org.eclipse.jgit.errors.ObjectWritingException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.CoreConfig;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 
@@ -68,13 +68,13 @@
 class ObjectDirectoryInserter extends ObjectInserter {
 	private final FileObjectDatabase db;
 
-	private final Config config;
+	private final WriteConfig config;
 
 	private Deflater deflate;
 
 	ObjectDirectoryInserter(final FileObjectDatabase dest, final Config cfg) {
 		db = dest;
-		config = cfg;
+		config = cfg.get(WriteConfig.KEY);
 	}
 
 	@Override
@@ -121,9 +121,13 @@ private File toTemp(final MessageDigest md, final int type, long len,
 		boolean delete = true;
 		File tmp = newTempFile();
 		try {
-			DigestOutputStream dOut = new DigestOutputStream(
-					compress(new FileOutputStream(tmp)), md);
+			FileOutputStream fOut = new FileOutputStream(tmp);
 			try {
+				OutputStream out = fOut;
+				if (config.getFSyncObjectFiles())
+					out = Channels.newOutputStream(fOut.getChannel());
+				DeflaterOutputStream cOut = compress(out);
+				DigestOutputStream dOut = new DigestOutputStream(cOut, md);
 				writeHeader(dOut, type, len);
 
 				final byte[] buf = buffer();
@@ -134,8 +138,12 @@ private File toTemp(final MessageDigest md, final int type, long len,
 					dOut.write(buf, 0, n);
 					len -= n;
 				}
+				dOut.flush();
+				cOut.finish();
 			} finally {
-				dOut.close();
+				if (config.getFSyncObjectFiles())
+					fOut.getChannel().force(true);
+				fOut.close();
 			}
 
 			delete = false;
@@ -160,7 +168,7 @@ File newTempFile() throws IOException {
 
 	DeflaterOutputStream compress(final OutputStream out) {
 		if (deflate == null)
-			deflate = new Deflater(config.get(CoreConfig.KEY).getCompression());
+			deflate = new Deflater(config.getCompression());
 		else
 			deflate.reset();
 		return new DeflaterOutputStream(out, deflate);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java
index 96c8361..2af7ca3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java
@@ -67,6 +67,8 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
 import java.text.MessageFormat;
 import java.util.Arrays;
 import java.util.LinkedList;
@@ -606,6 +608,7 @@ else if (log.isFile())
 			write = false;
 
 		if (write) {
+			WriteConfig wc = getRepository().getConfig().get(WriteConfig.KEY);
 			FileOutputStream out;
 			try {
 				out = new FileOutputStream(log, true);
@@ -618,7 +621,15 @@ else if (log.isFile())
 				out = new FileOutputStream(log, true);
 			}
 			try {
-				out.write(rec);
+				if (wc.getFSyncRefFiles()) {
+					FileChannel fc = out.getChannel();
+					ByteBuffer buf = ByteBuffer.wrap(rec);
+					while (0 < buf.remaining())
+						fc.write(buf);
+					fc.force(true);
+				} else {
+					out.write(rec);
+				}
 			} finally {
 				out.close();
 			}
@@ -757,6 +768,7 @@ private void commitPackedRefs(final LockFile lck, final RefList<Ref> refs,
 			@Override
 			protected void writeFile(String name, byte[] content)
 					throws IOException {
+				lck.setFSync(true);
 				lck.setNeedStatInformation(true);
 				try {
 					lck.write(content);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectoryUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectoryUpdate.java
index a9f0548..109960d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectoryUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectoryUpdate.java
@@ -99,6 +99,10 @@ protected void unlock() {
 
 	@Override
 	protected Result doUpdate(final Result status) throws IOException {
+		WriteConfig wc = database.getRepository().getConfig()
+				.get(WriteConfig.KEY);
+
+		lock.setFSync(wc.getFSyncRefFiles());
 		lock.setNeedStatInformation(true);
 		lock.write(getNewObjectId());
 
@@ -143,6 +147,10 @@ protected Result doDelete(final Result status) throws IOException {
 
 	@Override
 	protected Result doLink(final String target) throws IOException {
+		WriteConfig wc = database.getRepository().getConfig()
+				.get(WriteConfig.KEY);
+
+		lock.setFSync(wc.getFSyncRefFiles());
 		lock.setNeedStatInformation(true);
 		lock.write(encode(RefDirectory.SYMREF + target + '\n'));
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WriteConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WriteConfig.java
new file mode 100644
index 0000000..fd467a5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WriteConfig.java
@@ -0,0 +1,81 @@
+/*
+ * 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.storage.file;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Config.SectionParser;
+import org.eclipse.jgit.lib.CoreConfig;
+
+class WriteConfig {
+	/** Key for {@link Config#get(SectionParser)}. */
+	static final Config.SectionParser<WriteConfig> KEY = new SectionParser<WriteConfig>() {
+		public WriteConfig parse(final Config cfg) {
+			return new WriteConfig(cfg);
+		}
+	};
+
+	private final int compression;
+
+	private final boolean fsyncObjectFiles;
+
+	private final boolean fsyncRefFiles;
+
+	private WriteConfig(final Config rc) {
+		compression = rc.get(CoreConfig.KEY).getCompression();
+		fsyncObjectFiles = rc.getBoolean("core", "fsyncobjectfiles", false);
+		fsyncRefFiles = rc.getBoolean("core", "fsyncreffiles", false);
+	}
+
+	int getCompression() {
+		return compression;
+	}
+
+	boolean getFSyncObjectFiles() {
+		return fsyncObjectFiles;
+	}
+
+	boolean getFSyncRefFiles() {
+		return fsyncRefFiles;
+	}
+}