Merge "Add in-memory updating support to NoteMap"
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..e1a6d9b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/NoteMapTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/NoteMapTest.java
@@ -44,6 +44,8 @@
 package org.eclipse.jgit.notes;
 
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.MutableObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryTestCase;
@@ -56,17 +58,21 @@ public class NoteMapTest extends RepositoryTestCase {
 
 	private ObjectReader reader;
 
+	private ObjectInserter inserter;
+
 	@Override
 	protected void setUp() throws Exception {
 		super.setUp();
 
 		tr = new TestRepository<Repository>(db);
 		reader = db.newObjectReader();
+		inserter = db.newObjectInserter();
 	}
 
 	@Override
 	protected void tearDown() throws Exception {
 		reader.release();
+		inserter.release();
 		super.tearDown();
 	}
 
@@ -182,6 +188,99 @@ public void testGetCachedBytes() throws Exception {
 		assertEquals(exp, RawParseUtils.decode(act));
 	}
 
+	public void testCreateFromEmpty() throws Exception {
+		RevBlob a = tr.blob("a");
+		RevBlob b = tr.blob("b");
+		RevBlob data1 = tr.blob("data1");
+		RevBlob data2 = tr.blob("data2");
+
+		NoteMap map = NoteMap.newEmptyMap();
+		assertFalse("no a", map.contains(a));
+		assertFalse("no b", map.contains(b));
+
+		map.set(a, data1);
+		map.set(b, data2);
+
+		assertEquals(data1, map.get(a));
+		assertEquals(data2, map.get(b));
+
+		map.remove(a);
+		map.remove(b);
+
+		assertFalse("no a", map.contains(a));
+		assertFalse("no b", map.contains(b));
+
+		map.set(a, "data1", inserter);
+		assertEquals(data1, map.get(a));
+
+		map.set(a, null, inserter);
+		assertFalse("no a", map.contains(a));
+	}
+
+	public void testEditFlat() throws Exception {
+		RevBlob a = tr.blob("a");
+		RevBlob b = tr.blob("b");
+		RevBlob data1 = tr.blob("data1");
+		RevBlob data2 = tr.blob("data2");
+
+		RevCommit r = tr.commit() //
+				.add(a.name(), data1) //
+				.add(b.name(), data2) //
+				.create();
+		tr.parseBody(r);
+
+		NoteMap map = NoteMap.read(reader, r);
+		map.set(a, data2);
+		map.set(b, null);
+		map.set(data1, b);
+		map.set(data2, null);
+
+		assertEquals(data2, map.get(a));
+		assertEquals(b, map.get(data1));
+		assertFalse("no b", map.contains(b));
+		assertFalse("no data2", map.contains(data2));
+
+		MutableObjectId id = new MutableObjectId();
+		for (int p = 42; p > 0; p--) {
+			id.setByte(1, p);
+			map.set(id, data1);
+		}
+
+		for (int p = 42; p > 0; p--) {
+			id.setByte(1, p);
+			assertTrue("contains " + id, map.contains(id));
+		}
+	}
+
+	public void testEditFanout2_38() throws Exception {
+		RevBlob a = tr.blob("a");
+		RevBlob b = tr.blob("b");
+		RevBlob data1 = tr.blob("data1");
+		RevBlob data2 = tr.blob("data2");
+
+		RevCommit r = tr.commit() //
+				.add(fanout(2, a.name()), data1) //
+				.add(fanout(2, b.name()), data2) //
+				.create();
+		tr.parseBody(r);
+
+		NoteMap map = NoteMap.read(reader, r);
+		map.set(a, data2);
+		map.set(b, null);
+		map.set(data1, b);
+		map.set(data2, null);
+
+		assertEquals(data2, map.get(a));
+		assertEquals(b, map.get(data1));
+		assertFalse("no b", map.contains(b));
+		assertFalse("no data2", map.contains(data2));
+
+		map.set(a, null);
+		map.set(data1, null);
+		assertFalse("no a", map.contains(a));
+		assertFalse("no data1", map.contains(data1));
+	}
+
 	private static String fanout(int prefix, String name) {
 		StringBuilder r = new StringBuilder();
 		int i = 0;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java
index 85337c8..7605286 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java
@@ -84,6 +84,9 @@ class FanoutBucket extends InMemoryNoteBucket {
 	 */
 	private final NoteBucket[] table;
 
+	/** Number of non-null slots in {@link #table}. */
+	private int cnt;
+
 	FanoutBucket(int prefixLen) {
 		super(prefixLen);
 		table = new NoteBucket[256];
@@ -91,6 +94,7 @@ class FanoutBucket extends InMemoryNoteBucket {
 
 	void parseOneEntry(int cell, ObjectId id) {
 		table[cell] = new LazyNoteBucket(id);
+		cnt++;
 	}
 
 	@Override
@@ -99,6 +103,39 @@ ObjectId get(AnyObjectId objId, ObjectReader or) throws IOException {
 		return b != null ? b.get(objId, or) : null;
 	}
 
+	@Override
+	InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
+			ObjectReader or) throws IOException {
+		int cell = cell(noteOn);
+		NoteBucket b = table[cell];
+
+		if (b == null) {
+			if (noteData == null)
+				return this;
+
+			LeafBucket n = new LeafBucket(prefixLen + 2);
+			table[cell] = n.set(noteOn, noteData, or);
+			cnt++;
+			return this;
+
+		} else {
+			NoteBucket n = b.set(noteOn, noteData, or);
+			if (n == null) {
+				table[cell] = null;
+				cnt--;
+
+				if (cnt == 0)
+					return null;
+
+				return this;
+
+			} else if (n != b) {
+				table[cell] = n;
+			}
+			return this;
+		}
+	}
+
 	private int cell(AnyObjectId id) {
 		return id.getByte(prefixLen >> 1);
 	}
@@ -115,6 +152,12 @@ ObjectId get(AnyObjectId objId, ObjectReader or) throws IOException {
 			return load(objId, or).get(objId, or);
 		}
 
+		@Override
+		InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
+				ObjectReader or) throws IOException {
+			return load(noteOn, or).set(noteOn, noteData, or);
+		}
+
 		private NoteBucket load(AnyObjectId objId, ObjectReader or)
 				throws IOException {
 			AbbreviatedObjectId p = objId.abbreviate(prefixLen + 2);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java
index 66d773a..ce4feae 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java
@@ -43,6 +43,8 @@
 
 package org.eclipse.jgit.notes;
 
+import java.io.IOException;
+
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -96,6 +98,34 @@ ObjectId get(AnyObjectId objId, ObjectReader or) {
 		return 0 <= idx ? notes[idx].getData() : null;
 	}
 
+	InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
+			ObjectReader or) throws IOException {
+		int p = search(noteOn);
+		if (0 <= p) {
+			if (noteData != null) {
+				notes[p].setData(noteData.copy());
+				return this;
+
+			} else {
+				System.arraycopy(notes, p + 1, notes, p, cnt - p - 1);
+				cnt--;
+				return 0 < cnt ? this : null;
+			}
+
+		} else if (noteData != null) {
+			growIfFull();
+			p = -(p + 1);
+			if (p < cnt)
+				System.arraycopy(notes, p, notes, p + 1, cnt - p);
+			notes[p] = new Note(noteOn, noteData.copy());
+			cnt++;
+			return this;
+
+		} else {
+			return this;
+		}
+	}
+
 	void parseOneEntry(AnyObjectId noteOn, AnyObjectId noteData) {
 		growIfFull();
 		notes[cnt++] = new Note(noteOn, noteData.copy());
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteBucket.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteBucket.java
index 286f140..f75fc1f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteBucket.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteBucket.java
@@ -58,4 +58,7 @@
 abstract class NoteBucket {
 	abstract ObjectId get(AnyObjectId objId, ObjectReader reader)
 			throws IOException;
+
+	abstract InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
+			ObjectReader reader) throws IOException;
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java
index d2f0727..88ab481 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,89 @@ 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);
+	}
+
 	private void load(ObjectId rootTree) throws MissingObjectException,
 			IncorrectObjectTypeException, CorruptObjectException, IOException {
 		AbbreviatedObjectId none = AbbreviatedObjectId.fromString("");