]> source.dussan.org Git - jgit.git/commitdiff
Define NoteMap, a simple note tree reader 33/1833/5
authorShawn O. Pearce <spearce@spearce.org>
Fri, 5 Nov 2010 01:24:45 +0000 (18:24 -0700)
committerChris Aniszczyk <caniszczyk@gmail.com>
Thu, 11 Nov 2010 16:06:43 +0000 (10:06 -0600)
The NoteMap makes it easy to read a small notes tree as created by
the `git notes` command in C Git.  To make the initial implementation
simple a notes tree is read recursively into a map in memory.
This is reasonable if the application will need to access all notes,
or if there are less than 256 notes in the tree, but doesn't behave
well when the number of notes exceeds 256 and the application
doesn't need to access all of them.

We can later add support for lazily loading different subpaths,
thus fixing the large note tree problem described above.

Currently the implementation only supports reading.  Writing notes
is more complex because trees need to be expanded or collapsed at
the exact 256 entry cut-off in order to retain the same tree SHA-1
that C Git would use for the same content.  It also needs to retain
non-note tree entries such as ".gitignore" or ".gitattribute" files
that might randomly appear within a notes tree.  We can also add
writing support later.

Change-Id: I93704bd84ebf650d51de34da3f1577ef0f7a9144
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
Signed-off-by: Chris Aniszczyk <caniszczyk@gmail.com>
org.eclipse.jgit.test/META-INF/MANIFEST.MF
org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/NoteMapTest.java [new file with mode: 0644]
org.eclipse.jgit/META-INF/MANIFEST.MF
org.eclipse.jgit/src/org/eclipse/jgit/notes/Note.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java [new file with mode: 0644]

index 8d8508431255da1583b8882a009f0427bf094088..0a87fd3230f0bd3e376d8444e2dd7c2c420c4645 100644 (file)
@@ -26,6 +26,7 @@ Import-Package: junit.framework;version="[4.0.0,5.0.0)",
  org.eclipse.jgit.lib;version="[0.10.0,0.11.0)",
  org.eclipse.jgit.merge;version="[0.10.0,0.11.0)",
  org.eclipse.jgit.nls;version="[0.10.0,0.11.0)",
+ org.eclipse.jgit.notes;version="[0.10.0,0.11.0)",
  org.eclipse.jgit.patch;version="[0.10.0,0.11.0)",
  org.eclipse.jgit.pgm;version="[0.10.0,0.11.0)",
  org.eclipse.jgit.revplot;version="[0.10.0,0.11.0)",
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/NoteMapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/notes/NoteMapTest.java
new file mode 100644 (file)
index 0000000..786f1b9
--- /dev/null
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2010, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.notes;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryTestCase;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.RawParseUtils;
+
+public class NoteMapTest extends RepositoryTestCase {
+       private TestRepository<Repository> tr;
+
+       private ObjectReader reader;
+
+       @Override
+       protected void setUp() throws Exception {
+               super.setUp();
+
+               tr = new TestRepository<Repository>(db);
+               reader = db.newObjectReader();
+       }
+
+       @Override
+       protected void tearDown() throws Exception {
+               reader.release();
+               super.tearDown();
+       }
+
+       public void testReadFlatTwoNotes() throws Exception {
+               RevBlob a = tr.blob("a");
+               RevBlob b = tr.blob("b");
+               RevBlob data1 = tr.blob("data1");
+               RevBlob data2 = tr.blob("data2");
+
+               RevCommit r = tr.commit() //
+                               .add(a.name(), data1) //
+                               .add(b.name(), data2) //
+                               .create();
+               tr.parseBody(r);
+
+               NoteMap map = NoteMap.read(reader, r);
+               assertNotNull("have map", map);
+
+               assertTrue("has note for a", map.contains(a));
+               assertTrue("has note for b", map.contains(b));
+               assertEquals(data1, map.get(a));
+               assertEquals(data2, map.get(b));
+
+               assertFalse("no note for data1", map.contains(data1));
+               assertNull("no note for data1", map.get(data1));
+       }
+
+       public void testReadFanout2_38() throws Exception {
+               RevBlob a = tr.blob("a");
+               RevBlob b = tr.blob("b");
+               RevBlob data1 = tr.blob("data1");
+               RevBlob data2 = tr.blob("data2");
+
+               RevCommit r = tr.commit() //
+                               .add(fanout(2, a.name()), data1) //
+                               .add(fanout(2, b.name()), data2) //
+                               .create();
+               tr.parseBody(r);
+
+               NoteMap map = NoteMap.read(reader, r);
+               assertNotNull("have map", map);
+
+               assertTrue("has note for a", map.contains(a));
+               assertTrue("has note for b", map.contains(b));
+               assertEquals(data1, map.get(a));
+               assertEquals(data2, map.get(b));
+
+               assertFalse("no note for data1", map.contains(data1));
+               assertNull("no note for data1", map.get(data1));
+       }
+
+       public void testReadFanout2_2_36() throws Exception {
+               RevBlob a = tr.blob("a");
+               RevBlob b = tr.blob("b");
+               RevBlob data1 = tr.blob("data1");
+               RevBlob data2 = tr.blob("data2");
+
+               RevCommit r = tr.commit() //
+                               .add(fanout(4, a.name()), data1) //
+                               .add(fanout(4, b.name()), data2) //
+                               .create();
+               tr.parseBody(r);
+
+               NoteMap map = NoteMap.read(reader, r);
+               assertNotNull("have map", map);
+
+               assertTrue("has note for a", map.contains(a));
+               assertTrue("has note for b", map.contains(b));
+               assertEquals(data1, map.get(a));
+               assertEquals(data2, map.get(b));
+
+               assertFalse("no note for data1", map.contains(data1));
+               assertNull("no note for data1", map.get(data1));
+       }
+
+       public void testReadFullyFannedOut() throws Exception {
+               RevBlob a = tr.blob("a");
+               RevBlob b = tr.blob("b");
+               RevBlob data1 = tr.blob("data1");
+               RevBlob data2 = tr.blob("data2");
+
+               RevCommit r = tr.commit() //
+                               .add(fanout(38, a.name()), data1) //
+                               .add(fanout(38, b.name()), data2) //
+                               .create();
+               tr.parseBody(r);
+
+               NoteMap map = NoteMap.read(reader, r);
+               assertNotNull("have map", map);
+
+               assertTrue("has note for a", map.contains(a));
+               assertTrue("has note for b", map.contains(b));
+               assertEquals(data1, map.get(a));
+               assertEquals(data2, map.get(b));
+
+               assertFalse("no note for data1", map.contains(data1));
+               assertNull("no note for data1", map.get(data1));
+       }
+
+       public void testGetCachedBytes() throws Exception {
+               final String exp = "this is test data";
+               RevBlob a = tr.blob("a");
+               RevBlob data = tr.blob(exp);
+
+               RevCommit r = tr.commit() //
+                               .add(a.name(), data) //
+                               .create();
+               tr.parseBody(r);
+
+               NoteMap map = NoteMap.read(reader, r);
+               byte[] act = map.getCachedBytes(a, exp.length() * 4);
+               assertNotNull("has data for a", act);
+               assertEquals(exp, RawParseUtils.decode(act));
+       }
+
+       private static String fanout(int prefix, String name) {
+               StringBuilder r = new StringBuilder();
+               int i = 0;
+               for (; i < prefix && i < name.length(); i += 2) {
+                       if (i != 0)
+                               r.append('/');
+                       r.append(name.charAt(i + 0));
+                       r.append(name.charAt(i + 1));
+               }
+               if (i < name.length()) {
+                       if (i != 0)
+                               r.append('/');
+                       r.append(name.substring(i));
+               }
+               return r.toString();
+       }
+}
index 9e7362fd9560714104b8a409c613041652a26242..ba09e588373cd47313e0172cc0116735b9cfecff 100644 (file)
@@ -17,6 +17,7 @@ Export-Package: org.eclipse.jgit;version="0.10.0",
  org.eclipse.jgit.lib;version="0.10.0",
  org.eclipse.jgit.merge;version="0.10.0",
  org.eclipse.jgit.nls;version="0.10.0",
+ org.eclipse.jgit.notes;version="0.10.0",
  org.eclipse.jgit.patch;version="0.10.0",
  org.eclipse.jgit.revplot;version="0.10.0",
  org.eclipse.jgit.revwalk;version="0.10.0",
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/Note.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/Note.java
new file mode 100644 (file)
index 0000000..d365f9b
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2010, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.notes;
+
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** In-memory representation of a single note attached to one object. */
+class Note extends ObjectId {
+       private ObjectId data;
+
+       /**
+        * A Git note about the object referenced by {@code noteOn}.
+        *
+        * @param noteOn
+        *            the object that has a note attached to it.
+        * @param noteData
+        *            the actual note data contained in this note
+        */
+       Note(AnyObjectId noteOn, ObjectId noteData) {
+               super(noteOn);
+               data = noteData;
+       }
+
+       ObjectId getData() {
+               return data;
+       }
+
+       void setData(ObjectId newData) {
+               data = newData;
+       }
+
+       @Override
+       public String toString() {
+               return "Note[" + name() + " -> " + data.name() + "]";
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java
new file mode 100644 (file)
index 0000000..90e0480
--- /dev/null
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2010, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.notes;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.LargeObjectException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdSubclassMap;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * Index of notes from a note branch.
+ *
+ * This class is not thread-safe, and relies on an {@link ObjectReader} that it
+ * borrows/shares with the caller.
+ *
+ * The current implementation is not very efficient. During load it recursively
+ * scans the entire note branch and indexes all annotated objects. If there are
+ * more than 256 notes in the branch, and not all of them will be accessed by
+ * the caller, this aggressive up-front loading probably takes too much time.
+ */
+public class NoteMap {
+       /**
+        * Load a collection of notes from a branch.
+        *
+        * @param reader
+        *            reader to scan the note branch with. This reader may be
+        *            retained by the NoteMap for the life of the map in order to
+        *            support lazy loading of entries.
+        * @param commit
+        *            the revision of the note branch to read.
+        * @return the note map read from the commit.
+        * @throws IOException
+        *             the repository cannot be accessed through the reader.
+        * @throws CorruptObjectException
+        *             a tree object is corrupt and cannot be read.
+        * @throws IncorrectObjectTypeException
+        *             a tree object wasn't actually a tree.
+        * @throws MissingObjectException
+        *             a reference tree object doesn't exist.
+        */
+       public static NoteMap read(ObjectReader reader, RevCommit commit)
+                       throws MissingObjectException, IncorrectObjectTypeException,
+                       CorruptObjectException, IOException {
+               return read(reader, commit.getTree());
+       }
+
+       /**
+        * Load a collection of notes from a tree.
+        *
+        * @param reader
+        *            reader to scan the note branch with. This reader may be
+        *            retained by the NoteMap for the life of the map in order to
+        *            support lazy loading of entries.
+        * @param tree
+        *            the note tree to read.
+        * @return the note map read from the tree.
+        * @throws IOException
+        *             the repository cannot be accessed through the reader.
+        * @throws CorruptObjectException
+        *             a tree object is corrupt and cannot be read.
+        * @throws IncorrectObjectTypeException
+        *             a tree object wasn't actually a tree.
+        * @throws MissingObjectException
+        *             a reference tree object doesn't exist.
+        */
+       public static NoteMap read(ObjectReader reader, RevTree tree)
+                       throws MissingObjectException, IncorrectObjectTypeException,
+                       CorruptObjectException, IOException {
+               return readTree(reader, tree);
+       }
+
+       /**
+        * Load a collection of notes from a tree.
+        *
+        * @param reader
+        *            reader to scan the note branch with. This reader may be
+        *            retained by the NoteMap for the life of the map in order to
+        *            support lazy loading of entries.
+        * @param treeId
+        *            the note tree to read.
+        * @return the note map read from the tree.
+        * @throws IOException
+        *             the repository cannot be accessed through the reader.
+        * @throws CorruptObjectException
+        *             a tree object is corrupt and cannot be read.
+        * @throws IncorrectObjectTypeException
+        *             a tree object wasn't actually a tree.
+        * @throws MissingObjectException
+        *             a reference tree object doesn't exist.
+        */
+       public static NoteMap readTree(ObjectReader reader, ObjectId treeId)
+                       throws MissingObjectException, IncorrectObjectTypeException,
+                       CorruptObjectException, IOException {
+               NoteMap map = new NoteMap(reader);
+               map.load(treeId);
+               return map;
+       }
+
+       /** Borrowed reader to access the repository. */
+       private final ObjectReader reader;
+
+       /** All of the notes that have been loaded. */
+       private ObjectIdSubclassMap<Note> notes = new ObjectIdSubclassMap<Note>();
+
+       private NoteMap(ObjectReader reader) {
+               this.reader = reader;
+       }
+
+       /**
+        * Lookup a note for a specific ObjectId.
+        *
+        * @param id
+        *            the object to look for.
+        * @return the note's blob ObjectId, or null if no note exists.
+        */
+       public ObjectId get(AnyObjectId id) {
+               Note note = notes.get(id);
+               return note != null ? note.getData() : null;
+       }
+
+       /**
+        * Determine if a note exists for the specified ObjectId.
+        *
+        * @param id
+        *            the object to look for.
+        * @return true if a note exists; false if there is no note.
+        */
+       public boolean contains(AnyObjectId id) {
+               return get(id) != null;
+       }
+
+       /**
+        * Open and return the content of an object's note.
+        *
+        * This method assumes the note is fairly small and can be accessed
+        * efficiently. Larger notes should be accessed by streaming:
+        *
+        * <pre>
+        * ObjectId dataId = thisMap.get(id);
+        * if (dataId != null)
+        *      reader.open(dataId).openStream();
+        * </pre>
+        *
+        * @param id
+        *            object to lookup the note of.
+        * @param sizeLimit
+        *            maximum number of bytes to return. If the note data size is
+        *            larger than this limit, LargeObjectException will be thrown.
+        * @return if a note is defined for {@code id}, the note content. If no note
+        *         is defined, null.
+        * @throws LargeObjectException
+        *             the note data is larger than {@code sizeLimit}.
+        * @throws MissingObjectException
+        *             the note's blob does not exist in the repository.
+        * @throws IOException
+        *             the note's blob cannot be read from the repository
+        */
+       public byte[] getCachedBytes(AnyObjectId id, int sizeLimit)
+                       throws LargeObjectException, MissingObjectException, IOException {
+               ObjectId dataId = get(id);
+               if (dataId != null)
+                       return reader.open(dataId).getCachedBytes(sizeLimit);
+               else
+                       return null;
+       }
+
+       private void load(ObjectId treeId) throws MissingObjectException,
+                       IncorrectObjectTypeException, CorruptObjectException, IOException {
+               try {
+                       byte[] idBuf = new byte[Constants.OBJECT_ID_LENGTH];
+                       TreeWalk tw = new TreeWalk(reader);
+                       tw.reset();
+                       tw.setRecursive(true);
+                       tw.addTree(treeId);
+
+                       notes.clear();
+
+                       while (tw.next()) {
+                               ObjectId pathId = pathToId(idBuf, tw.getRawPath());
+                               if (pathId != null)
+                                       notes.add(new Note(pathId, tw.getObjectId(0)));
+                       }
+               } finally {
+                       reader.release();
+               }
+       }
+
+       private static ObjectId pathToId(byte[] rawIdBuf, byte[] path) {
+               int r = 0;
+
+               for (int i = 0; i < path.length; i++) {
+                       byte c = path[i];
+
+                       // Skip embedded '/' in paths, these aren't part of a name.
+                       if (c == '/')
+                               continue;
+
+                       // We need at least 2 digits to consume in this cycle.
+                       if (path.length < i + 2)
+                               return null;
+
+                       // We cannot exceed an ObjectId raw length.
+                       if (r == Constants.OBJECT_ID_STRING_LENGTH)
+                               return null;
+
+                       int high, low;
+                       try {
+                               high = RawParseUtils.parseHexInt4(c);
+                               low = RawParseUtils.parseHexInt4(path[++i]);
+                       } catch (ArrayIndexOutOfBoundsException notHex) {
+                               return null;
+                       }
+
+                       rawIdBuf[r++] = (byte) ((high << 4) | low);
+               }
+
+               // We need exactly the right number of input bytes.
+               if (r == Constants.OBJECT_ID_LENGTH)
+                       return ObjectId.fromRaw(rawIdBuf);
+               else
+                       return null;
+       }
+}