ObjectWalk.java
/*
* Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.revwalk;
import static java.util.Objects.requireNonNull;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
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.internal.JGitText;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.filter.ObjectFilter;
import org.eclipse.jgit.util.RawParseUtils;
/**
* Specialized subclass of RevWalk to include trees, blobs and tags.
* <p>
* Unlike RevWalk this subclass is able to remember starting roots that include
* annotated tags, or arbitrary trees or blobs. Once commit generation is
* complete and all commits have been popped by the application, individual
* annotated tag, tree and blob objects can be popped through the additional
* method {@link #nextObject()}.
* <p>
* Tree and blob objects reachable from interesting commits are automatically
* scheduled for inclusion in the results of {@link #nextObject()}, returning
* each object exactly once. Objects are sorted and returned according to the
* the commits that reference them and the order they appear within a tree.
* Ordering can be affected by changing the
* {@link org.eclipse.jgit.revwalk.RevSort} used to order the commits that are
* returned first.
*/
public class ObjectWalk extends RevWalk {
private static final int ID_SZ = 20;
private static final int TYPE_SHIFT = 12;
private static final int TYPE_TREE = 0040000 >>> TYPE_SHIFT;
private static final int TYPE_SYMLINK = 0120000 >>> TYPE_SHIFT;
private static final int TYPE_FILE = 0100000 >>> TYPE_SHIFT;
private static final int TYPE_GITLINK = 0160000 >>> TYPE_SHIFT;
/**
* Indicates a non-RevCommit is in {@link #pendingObjects}.
* <p>
* We can safely reuse {@link RevWalk#REWRITE} here for the same value as it
* is only set on RevCommit and {@link #pendingObjects} never has RevCommit
* instances inserted into it.
*/
private static final int IN_PENDING = RevWalk.REWRITE;
/**
* When walking over a tree and blob graph, objects are usually marked as
* seen as they are visited and this "seen" status is checked upon the next
* visit. If they are already "seen" then they are not processed (returned
* by {@link ObjectWalk#nextObject()}) again. However, this behavior can be
* overridden by supplying a different implementation of this class.
*
* @since 5.4
*/
public interface VisitationPolicy {
/**
* Whenever the rev or object walk reaches a Git object, if that object
* already exists as a RevObject, this method is called to determine if
* that object should be visited.
*
* @param o
* the object to check if it should be visited
* @return true if the object should be visited
*/
boolean shouldVisit(RevObject o);
/**
* Called when an object is visited.
*
* @param o
* the object that was visited
*/
void visited(RevObject o);
}
/**
* The default visitation policy: causes all objects to be visited exactly
* once.
*
* @since 5.4
*/
public static final VisitationPolicy SIMPLE_VISITATION_POLICY =
new VisitationPolicy() {
@Override
public boolean shouldVisit(RevObject o) {
return (o.flags & SEEN) == 0;
}
@Override
public void visited(RevObject o) {
o.flags |= SEEN;
}
};
private List<RevObject> rootObjects;
private BlockObjQueue pendingObjects;
private ObjectFilter objectFilter;
private TreeVisit freeVisit;
private TreeVisit currVisit;
private byte[] pathBuf;
private int pathLen;
private boolean boundary;
private VisitationPolicy visitationPolicy = SIMPLE_VISITATION_POLICY;
/**
* Create a new revision and object walker for a given repository.
*
* @param repo
* the repository the walker will obtain data from.
*/
public ObjectWalk(Repository repo) {
this(repo.newObjectReader(), true);
}
/**
* Create a new revision and object walker for a given repository.
*
* @param or
* the reader the walker will obtain data from. The reader should
* be released by the caller when the walker is no longer
* required.
*/
public ObjectWalk(ObjectReader or) {
this(or, false);
}
private ObjectWalk(ObjectReader or, boolean closeReader) {
super(or, closeReader);
setRetainBody(false);
rootObjects = new ArrayList<>();
pendingObjects = new BlockObjQueue();
objectFilter = ObjectFilter.ALL;
pathBuf = new byte[256];
}
/**
* Create an object reachability checker that will use bitmaps if possible.
*
* This reachability checker accepts any object as target. For checks
* exclusively between commits, see
* {@link RevWalk#createReachabilityChecker()}.
*
* @return an object reachability checker, using bitmaps if possible.
*
* @throws IOException
* when the index fails to load.
*
* @since 5.8
* @deprecated use
* {@code ObjectReader#createObjectReachabilityChecker(ObjectWalk)}
* instead.
*/
@Deprecated
public final ObjectReachabilityChecker createObjectReachabilityChecker()
throws IOException {
return reader.createObjectReachabilityChecker(this);
}
/**
* Mark an object or commit to start graph traversal from.
* <p>
* Callers are encouraged to use
* {@link org.eclipse.jgit.revwalk.RevWalk#parseAny(AnyObjectId)} instead of
* {@link org.eclipse.jgit.revwalk.RevWalk#lookupAny(AnyObjectId, int)}, as
* this method requires the object to be parsed before it can be added as a
* root for the traversal.
* <p>
* The method will automatically parse an unparsed object, but error
* handling may be more difficult for the application to explain why a
* RevObject is not actually valid. The object pool of this walker would
* also be 'poisoned' by the invalid RevObject.
* <p>
* This method will automatically call
* {@link org.eclipse.jgit.revwalk.RevWalk#markStart(RevCommit)} if passed
* RevCommit instance, or a RevTag that directly (or indirectly) references
* a RevCommit.
*
* @param o
* the object to start traversing from. The object passed must be
* from this same revision walker.
* @throws org.eclipse.jgit.errors.MissingObjectException
* the object supplied is not available from the object
* database. This usually indicates the supplied object is
* invalid, but the reference was constructed during an earlier
* invocation to
* {@link org.eclipse.jgit.revwalk.RevWalk#lookupAny(AnyObjectId, int)}.
* @throws org.eclipse.jgit.errors.IncorrectObjectTypeException
* the object was not parsed yet and it was discovered during
* parsing that it is not actually the type of the instance
* passed in. This usually indicates the caller used the wrong
* type in a
* {@link org.eclipse.jgit.revwalk.RevWalk#lookupAny(AnyObjectId, int)}
* call.
* @throws java.io.IOException
* a pack file or loose object could not be read.
*/
public void markStart(RevObject o) throws MissingObjectException,
IncorrectObjectTypeException, IOException {
while (o instanceof RevTag) {
addObject(o);
o = ((RevTag) o).getObject();
parseHeaders(o);
}
if (o instanceof RevCommit)
super.markStart((RevCommit) o);
else
addObject(o);
}
/**
* Mark an object to not produce in the output.
* <p>
* Uninteresting objects denote not just themselves but also their entire
* reachable chain, back until the merge base of an uninteresting commit and
* an otherwise interesting commit.
* <p>
* Callers are encouraged to use
* {@link org.eclipse.jgit.revwalk.RevWalk#parseAny(AnyObjectId)} instead of
* {@link org.eclipse.jgit.revwalk.RevWalk#lookupAny(AnyObjectId, int)}, as
* this method requires the object to be parsed before it can be added as a
* root for the traversal.
* <p>
* The method will automatically parse an unparsed object, but error
* handling may be more difficult for the application to explain why a
* RevObject is not actually valid. The object pool of this walker would
* also be 'poisoned' by the invalid RevObject.
* <p>
* This method will automatically call
* {@link org.eclipse.jgit.revwalk.RevWalk#markStart(RevCommit)} if passed
* RevCommit instance, or a RevTag that directly (or indirectly) references
* a RevCommit.
*
* @param o
* the object to start traversing from. The object passed must be
* @throws org.eclipse.jgit.errors.MissingObjectException
* the object supplied is not available from the object
* database. This usually indicates the supplied object is
* invalid, but the reference was constructed during an earlier
* invocation to
* {@link org.eclipse.jgit.revwalk.RevWalk#lookupAny(AnyObjectId, int)}.
* @throws org.eclipse.jgit.errors.IncorrectObjectTypeException
* the object was not parsed yet and it was discovered during
* parsing that it is not actually the type of the instance
* passed in. This usually indicates the caller used the wrong
* type in a
* {@link org.eclipse.jgit.revwalk.RevWalk#lookupAny(AnyObjectId, int)}
* call.
* @throws java.io.IOException
* a pack file or loose object could not be read.
*/
public void markUninteresting(RevObject o) throws MissingObjectException,
IncorrectObjectTypeException, IOException {
while (o instanceof RevTag) {
o.flags |= UNINTERESTING;
if (boundary)
addObject(o);
o = ((RevTag) o).getObject();
parseHeaders(o);
}
if (o instanceof RevCommit)
super.markUninteresting((RevCommit) o);
else if (o instanceof RevTree)
markTreeUninteresting((RevTree) o);
else
o.flags |= UNINTERESTING;
if (o.getType() != OBJ_COMMIT && boundary)
addObject(o);
}
/** {@inheritDoc} */
@Override
public void sort(RevSort s) {
super.sort(s);
boundary = hasRevSort(RevSort.BOUNDARY);
}
/** {@inheritDoc} */
@Override
public void sort(RevSort s, boolean use) {
super.sort(s, use);
boundary = hasRevSort(RevSort.BOUNDARY);
}
/**
* Get the currently configured object filter.
*
* @return the current filter. Never null as a filter is always needed.
* @since 4.0
*/
public ObjectFilter getObjectFilter() {
return objectFilter;
}
/**
* Set the object filter for this walker. This filter affects the objects
* visited by {@link #nextObject()}. It does not affect the commits listed
* by {@link #next()}.
* <p>
* If the filter returns false for an object, then that object is skipped
* and objects reachable from it are not enqueued to be walked recursively.
* This can be used to speed up the object walk by skipping subtrees that
* are known to be uninteresting.
*
* @param newFilter
* the new filter. If null the special
* {@link org.eclipse.jgit.revwalk.filter.ObjectFilter#ALL}
* filter will be used instead, as it matches every object.
* @since 4.0
*/
public void setObjectFilter(ObjectFilter newFilter) {
assertNotStarted();
objectFilter = newFilter != null ? newFilter : ObjectFilter.ALL;
}
/**
* Sets the visitation policy to use during this walk.
*
* @param policy
* the {@code VisitationPolicy} to use
* @since 5.4
*/
public void setVisitationPolicy(VisitationPolicy policy) {
assertNotStarted();
visitationPolicy = requireNonNull(policy);
}
/** {@inheritDoc} */
@Override
public RevCommit next() throws MissingObjectException,
IncorrectObjectTypeException, IOException {
for (;;) {
final RevCommit r = super.next();
if (r == null) {
return null;
}
final RevTree t = r.getTree();
if ((r.flags & UNINTERESTING) != 0) {
if (objectFilter.include(this, t)) {
markTreeUninteresting(t);
}
if (boundary) {
return r;
}
continue;
}
if (objectFilter.include(this, t)) {
pendingObjects.add(t);
}
return r;
}
}
/**
* Skips the current tree such that {@link #nextObject()} does not return
* any objects inside it. This should be called right after
* {@link #nextObject()} returns the tree.
*
* @since 5.4
*/
public void skipTree() {
if (currVisit != null) {
currVisit.ptr = currVisit.buf.length;
}
}
/**
* Pop the next most recent object.
*
* @return next most recent object; null if traversal is over.
* @throws org.eclipse.jgit.errors.MissingObjectException
* one or more of the next objects are not available from the
* object database, but were thought to be candidates for
* traversal. This usually indicates a broken link.
* @throws org.eclipse.jgit.errors.IncorrectObjectTypeException
* one or more of the objects in a tree do not match the type
* indicated.
* @throws java.io.IOException
* a pack file or loose object could not be read.
*/
public RevObject nextObject() throws MissingObjectException,
IncorrectObjectTypeException, IOException {
pathLen = 0;
TreeVisit tv = currVisit;
while (tv != null) {
byte[] buf = tv.buf;
for (int ptr = tv.ptr; ptr < buf.length;) {
int startPtr = ptr;
ptr = findObjectId(buf, ptr);
idBuffer.fromRaw(buf, ptr);
ptr += ID_SZ;
if (!objectFilter.include(this, idBuffer)) {
continue;
}
RevObject obj = objects.get(idBuffer);
if (obj != null && !visitationPolicy.shouldVisit(obj))
continue;
int mode = parseMode(buf, startPtr, ptr, tv);
switch (mode >>> TYPE_SHIFT) {
case TYPE_FILE:
case TYPE_SYMLINK:
if (obj == null) {
obj = new RevBlob(idBuffer);
visitationPolicy.visited(obj);
objects.add(obj);
return obj;
}
if (!(obj instanceof RevBlob))
throw new IncorrectObjectTypeException(obj, OBJ_BLOB);
visitationPolicy.visited(obj);
if ((obj.flags & UNINTERESTING) == 0)
return obj;
if (boundary)
return obj;
continue;
case TYPE_TREE:
if (obj == null) {
obj = new RevTree(idBuffer);
visitationPolicy.visited(obj);
objects.add(obj);
return pushTree(obj);
}
if (!(obj instanceof RevTree))
throw new IncorrectObjectTypeException(obj, OBJ_TREE);
visitationPolicy.visited(obj);
if ((obj.flags & UNINTERESTING) == 0)
return pushTree(obj);
if (boundary)
return pushTree(obj);
continue;
case TYPE_GITLINK:
continue;
default:
throw new CorruptObjectException(MessageFormat.format(
JGitText.get().corruptObjectInvalidMode3,
String.format("%o", Integer.valueOf(mode)), //$NON-NLS-1$
idBuffer.name(),
RawParseUtils.decode(buf, tv.namePtr, tv.nameEnd),
tv.obj));
}
}
currVisit = tv.parent;
releaseTreeVisit(tv);
tv = currVisit;
}
for (;;) {
RevObject o = pendingObjects.next();
if (o == null) {
return null;
}
if (!visitationPolicy.shouldVisit(o)) {
continue;
}
visitationPolicy.visited(o);
if ((o.flags & UNINTERESTING) == 0 || boundary) {
if (o instanceof RevTree) {
// The previous while loop should have exhausted the stack
// of trees.
assert currVisit == null;
pushTree(o);
}
return o;
}
}
}
private static int findObjectId(byte[] buf, int ptr) {
// Skip over the mode and name until the NUL before the ObjectId
// can be located. Skip the NUL as the function returns.
for (;;) {
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
if (buf[++ptr] == 0) return ++ptr;
}
}
private static int parseMode(byte[] buf, int startPtr, int recEndPtr, TreeVisit tv) {
int mode = buf[startPtr] - '0';
for (;;) {
byte c = buf[++startPtr];
if (' ' == c)
break;
mode <<= 3;
mode += c - '0';
c = buf[++startPtr];
if (' ' == c)
break;
mode <<= 3;
mode += c - '0';
c = buf[++startPtr];
if (' ' == c)
break;
mode <<= 3;
mode += c - '0';
c = buf[++startPtr];
if (' ' == c)
break;
mode <<= 3;
mode += c - '0';
c = buf[++startPtr];
if (' ' == c)
break;
mode <<= 3;
mode += c - '0';
c = buf[++startPtr];
if (' ' == c)
break;
mode <<= 3;
mode += c - '0';
c = buf[++startPtr];
if (' ' == c)
break;
mode <<= 3;
mode += c - '0';
}
tv.ptr = recEndPtr;
tv.namePtr = startPtr + 1;
tv.nameEnd = recEndPtr - (ID_SZ + 1);
return mode;
}
/**
* Verify all interesting objects are available, and reachable.
* <p>
* Callers should populate starting points and ending points with
* {@link #markStart(RevObject)} and {@link #markUninteresting(RevObject)}
* and then use this method to verify all objects between those two points
* exist in the repository and are readable.
* <p>
* This method returns successfully if everything is connected; it throws an
* exception if there is a connectivity problem. The exception message
* provides some detail about the connectivity failure.
*
* @throws org.eclipse.jgit.errors.MissingObjectException
* one or more of the next objects are not available from the
* object database, but were thought to be candidates for
* traversal. This usually indicates a broken link.
* @throws org.eclipse.jgit.errors.IncorrectObjectTypeException
* one or more of the objects in a tree do not match the type
* indicated.
* @throws java.io.IOException
* a pack file or loose object could not be read.
*/
public void checkConnectivity() throws MissingObjectException,
IncorrectObjectTypeException, IOException {
for (;;) {
final RevCommit c = next();
if (c == null)
break;
}
for (;;) {
final RevObject o = nextObject();
if (o == null)
break;
if (o instanceof RevBlob && !reader.has(o))
throw new MissingObjectException(o, OBJ_BLOB);
}
}
/**
* Get the current object's complete path.
* <p>
* This method is not very efficient and is primarily meant for debugging
* and final output generation. Applications should try to avoid calling it,
* and if invoked do so only once per interesting entry, where the name is
* absolutely required for correct function.
*
* @return complete path of the current entry, from the root of the
* repository. If the current entry is in a subtree there will be at
* least one '/' in the returned string. Null if the current entry
* has no path, such as for annotated tags or root level trees.
*/
public String getPathString() {
if (pathLen == 0) {
pathLen = updatePathBuf(currVisit);
if (pathLen == 0)
return null;
}
return RawParseUtils.decode(pathBuf, 0, pathLen);
}
/**
* @return the current traversal depth from the root tree object
* @since 5.4
*/
public int getTreeDepth() {
if (currVisit == null) {
return 0;
}
return currVisit.depth;
}
/**
* Get the current object's path hash code.
* <p>
* This method computes a hash code on the fly for this path, the hash is
* suitable to cluster objects that may have similar paths together.
*
* @return path hash code; any integer may be returned.
*/
public int getPathHashCode() {
TreeVisit tv = currVisit;
if (tv == null)
return 0;
int nameEnd = tv.nameEnd;
if (nameEnd == 0) {
// When nameEnd == 0 the subtree is itself the current path
// being visited. The name hash must be obtained from its
// parent tree. If there is no parent, this is a root tree with
// a hash code of 0.
tv = tv.parent;
if (tv == null)
return 0;
nameEnd = tv.nameEnd;
}
byte[] buf;
int ptr;
if (16 <= (nameEnd - tv.namePtr)) {
buf = tv.buf;
ptr = nameEnd - 16;
} else {
nameEnd = pathLen;
if (nameEnd == 0) {
nameEnd = updatePathBuf(currVisit);
pathLen = nameEnd;
}
buf = pathBuf;
ptr = Math.max(0, nameEnd - 16);
}
int hash = 0;
for (; ptr < nameEnd; ptr++) {
byte c = buf[ptr];
if (c != ' ')
hash = (hash >>> 2) + (c << 24);
}
return hash;
}
/**
* Get the internal buffer holding the current path.
*
* @return the internal buffer holding the current path.
*/
public byte[] getPathBuffer() {
if (pathLen == 0)
pathLen = updatePathBuf(currVisit);
return pathBuf;
}
/**
* Get length of the path in {@link #getPathBuffer()}.
*
* @return length of the path in {@link #getPathBuffer()}.
*/
public int getPathLength() {
if (pathLen == 0)
pathLen = updatePathBuf(currVisit);
return pathLen;
}
private int updatePathBuf(TreeVisit tv) {
if (tv == null)
return 0;
// If nameEnd == 0 this tree has not yet contributed an entry.
// Update only for the parent, which if null will be empty.
int nameEnd = tv.nameEnd;
if (nameEnd == 0)
return updatePathBuf(tv.parent);
int ptr = tv.pathLen;
if (ptr == 0) {
ptr = updatePathBuf(tv.parent);
if (ptr == pathBuf.length)
growPathBuf(ptr);
if (ptr != 0)
pathBuf[ptr++] = '/';
tv.pathLen = ptr;
}
int namePtr = tv.namePtr;
int nameLen = nameEnd - namePtr;
int end = ptr + nameLen;
while (pathBuf.length < end)
growPathBuf(ptr);
System.arraycopy(tv.buf, namePtr, pathBuf, ptr, nameLen);
return end;
}
private void growPathBuf(int ptr) {
byte[] newBuf = new byte[pathBuf.length << 1];
System.arraycopy(pathBuf, 0, newBuf, 0, ptr);
pathBuf = newBuf;
}
/** {@inheritDoc} */
@Override
public void dispose() {
super.dispose();
pendingObjects = new BlockObjQueue();
currVisit = null;
freeVisit = null;
}
/** {@inheritDoc} */
@Override
protected void reset(int retainFlags) {
super.reset(retainFlags);
for (RevObject obj : rootObjects)
obj.flags &= ~IN_PENDING;
rootObjects = new ArrayList<>();
pendingObjects = new BlockObjQueue();
currVisit = null;
freeVisit = null;
}
private void addObject(RevObject o) {
if ((o.flags & IN_PENDING) == 0) {
o.flags |= IN_PENDING;
rootObjects.add(o);
pendingObjects.add(o);
}
}
private void markTreeUninteresting(RevTree tree)
throws MissingObjectException, IncorrectObjectTypeException,
IOException {
if ((tree.flags & UNINTERESTING) != 0)
return;
tree.flags |= UNINTERESTING;
byte[] raw = reader.open(tree, OBJ_TREE).getCachedBytes();
for (int ptr = 0; ptr < raw.length;) {
byte c = raw[ptr];
int mode = c - '0';
for (;;) {
c = raw[++ptr];
if (' ' == c)
break;
mode <<= 3;
mode += c - '0';
}
while (raw[++ptr] != 0) {
// Skip entry name.
}
ptr++; // Skip NUL after entry name.
switch (mode >>> TYPE_SHIFT) {
case TYPE_FILE:
case TYPE_SYMLINK:
idBuffer.fromRaw(raw, ptr);
lookupBlob(idBuffer).flags |= UNINTERESTING;
break;
case TYPE_TREE:
idBuffer.fromRaw(raw, ptr);
markTreeUninteresting(lookupTree(idBuffer));
break;
case TYPE_GITLINK:
break;
default:
idBuffer.fromRaw(raw, ptr);
throw new CorruptObjectException(MessageFormat.format(
JGitText.get().corruptObjectInvalidMode3,
String.format("%o", Integer.valueOf(mode)), //$NON-NLS-1$
idBuffer.name(), "", tree)); //$NON-NLS-1$
}
ptr += ID_SZ;
}
}
private RevObject pushTree(RevObject obj) throws LargeObjectException,
MissingObjectException, IncorrectObjectTypeException, IOException {
TreeVisit tv = freeVisit;
if (tv != null) {
freeVisit = tv.parent;
tv.ptr = 0;
tv.namePtr = 0;
tv.nameEnd = 0;
tv.pathLen = 0;
} else {
tv = new TreeVisit();
}
tv.obj = obj;
tv.buf = reader.open(obj, OBJ_TREE).getCachedBytes();
tv.parent = currVisit;
currVisit = tv;
if (tv.parent == null) {
tv.depth = 1;
} else {
tv.depth = tv.parent.depth + 1;
}
return obj;
}
private void releaseTreeVisit(TreeVisit tv) {
tv.buf = null;
tv.parent = freeVisit;
freeVisit = tv;
}
private static class TreeVisit {
/** Parent tree visit that entered this tree, null if root tree. */
TreeVisit parent;
/** The RevTree currently being iterated through. */
RevObject obj;
/** Canonical encoding of the tree named by {@link #obj}. */
byte[] buf;
/** Index of next entry to parse in {@link #buf}. */
int ptr;
/** Start of the current name entry in {@link #buf}. */
int namePtr;
/** One past end of name, {@code nameEnd - namePtr} is the length. */
int nameEnd;
/** Number of bytes in the path leading up to this tree. */
int pathLen;
/** Number of levels deep from the root tree. 0 for root tree. */
int depth;
}
}