| /* | |
| * Copyright (C) 2008-2009, Google Inc. | |
| * Copyright (C) 2008, 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.dircache; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import static org.eclipse.jgit.lib.FileMode.TREE; | |
| import static org.eclipse.jgit.lib.TreeFormatter.entrySize; | |
| import java.io.IOException; | |
| import java.io.OutputStream; | |
| import java.nio.ByteBuffer; | |
| import java.util.Arrays; | |
| import java.util.Comparator; | |
| import org.eclipse.jgit.errors.UnmergedPathException; | |
| import org.eclipse.jgit.lib.Constants; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.ObjectInserter; | |
| import org.eclipse.jgit.lib.TreeFormatter; | |
| import org.eclipse.jgit.util.MutableInteger; | |
| import org.eclipse.jgit.util.RawParseUtils; | |
| /** | |
| * Single tree record from the 'TREE' {@link org.eclipse.jgit.dircache.DirCache} | |
| * extension. | |
| * <p> | |
| * A valid cache tree record contains the object id of a tree object and the | |
| * total number of {@link org.eclipse.jgit.dircache.DirCacheEntry} instances | |
| * (counted recursively) from the DirCache contained within the tree. This | |
| * information facilitates faster traversal of the index and quicker generation | |
| * of tree objects prior to creating a new commit. | |
| * <p> | |
| * An invalid cache tree record indicates a known subtree whose file entries | |
| * have changed in ways that cause the tree to no longer have a known object id. | |
| * Invalid cache tree records must be revalidated prior to use. | |
| */ | |
| public class DirCacheTree { | |
| private static final byte[] NO_NAME = {}; | |
| private static final DirCacheTree[] NO_CHILDREN = {}; | |
| private static final Comparator<DirCacheTree> TREE_CMP = (DirCacheTree o1, | |
| DirCacheTree o2) -> { | |
| final byte[] a = o1.encodedName; | |
| final byte[] b = o2.encodedName; | |
| final int aLen = a.length; | |
| final int bLen = b.length; | |
| int cPos; | |
| for (cPos = 0; cPos < aLen && cPos < bLen; cPos++) { | |
| final int cmp = (a[cPos] & 0xff) - (b[cPos] & 0xff); | |
| if (cmp != 0) { | |
| return cmp; | |
| } | |
| } | |
| if (aLen == bLen) { | |
| return 0; | |
| } | |
| if (aLen < bLen) { | |
| return '/' - (b[cPos] & 0xff); | |
| } | |
| return (a[cPos] & 0xff) - '/'; | |
| }; | |
| /** Tree this tree resides in; null if we are the root. */ | |
| private DirCacheTree parent; | |
| /** Name of this tree within its parent. */ | |
| byte[] encodedName; | |
| /** Number of {@link DirCacheEntry} records that belong to this tree. */ | |
| private int entrySpan; | |
| /** Unique SHA-1 of this tree; null if invalid. */ | |
| private ObjectId id; | |
| /** Child trees, if any, sorted by {@link #encodedName}. */ | |
| private DirCacheTree[] children; | |
| /** Number of valid children in {@link #children}. */ | |
| private int childCnt; | |
| DirCacheTree() { | |
| encodedName = NO_NAME; | |
| children = NO_CHILDREN; | |
| childCnt = 0; | |
| entrySpan = -1; | |
| } | |
| private DirCacheTree(final DirCacheTree myParent, final byte[] path, | |
| final int pathOff, final int pathLen) { | |
| parent = myParent; | |
| encodedName = new byte[pathLen]; | |
| System.arraycopy(path, pathOff, encodedName, 0, pathLen); | |
| children = NO_CHILDREN; | |
| childCnt = 0; | |
| entrySpan = -1; | |
| } | |
| DirCacheTree(final byte[] in, final MutableInteger off, | |
| final DirCacheTree myParent) { | |
| parent = myParent; | |
| int ptr = RawParseUtils.next(in, off.value, '\0'); | |
| final int nameLen = ptr - off.value - 1; | |
| if (nameLen > 0) { | |
| encodedName = new byte[nameLen]; | |
| System.arraycopy(in, off.value, encodedName, 0, nameLen); | |
| } else | |
| encodedName = NO_NAME; | |
| entrySpan = RawParseUtils.parseBase10(in, ptr, off); | |
| final int subcnt = RawParseUtils.parseBase10(in, off.value, off); | |
| off.value = RawParseUtils.next(in, off.value, '\n'); | |
| if (entrySpan >= 0) { | |
| // Valid trees have a positive entry count and an id of a | |
| // tree object that should exist in the object database. | |
| // | |
| id = ObjectId.fromRaw(in, off.value); | |
| off.value += Constants.OBJECT_ID_LENGTH; | |
| } | |
| if (subcnt > 0) { | |
| boolean alreadySorted = true; | |
| children = new DirCacheTree[subcnt]; | |
| for (int i = 0; i < subcnt; i++) { | |
| children[i] = new DirCacheTree(in, off, this); | |
| // C Git's ordering differs from our own; it prefers to | |
| // sort by length first. This sometimes produces a sort | |
| // we do not desire. On the other hand it may have been | |
| // created by us, and be sorted the way we want. | |
| // | |
| if (alreadySorted && i > 0 | |
| && TREE_CMP.compare(children[i - 1], children[i]) > 0) | |
| alreadySorted = false; | |
| } | |
| if (!alreadySorted) | |
| Arrays.sort(children, 0, subcnt, TREE_CMP); | |
| } else { | |
| // Leaf level trees have no children, only (file) entries. | |
| // | |
| children = NO_CHILDREN; | |
| } | |
| childCnt = subcnt; | |
| } | |
| void write(byte[] tmp, OutputStream os) throws IOException { | |
| int ptr = tmp.length; | |
| tmp[--ptr] = '\n'; | |
| ptr = RawParseUtils.formatBase10(tmp, ptr, childCnt); | |
| tmp[--ptr] = ' '; | |
| ptr = RawParseUtils.formatBase10(tmp, ptr, isValid() ? entrySpan : -1); | |
| tmp[--ptr] = 0; | |
| os.write(encodedName); | |
| os.write(tmp, ptr, tmp.length - ptr); | |
| if (isValid()) { | |
| id.copyRawTo(tmp, 0); | |
| os.write(tmp, 0, Constants.OBJECT_ID_LENGTH); | |
| } | |
| for (int i = 0; i < childCnt; i++) | |
| children[i].write(tmp, os); | |
| } | |
| /** | |
| * Determine if this cache is currently valid. | |
| * <p> | |
| * A valid cache tree knows how many | |
| * {@link org.eclipse.jgit.dircache.DirCacheEntry} instances from the parent | |
| * {@link org.eclipse.jgit.dircache.DirCache} reside within this tree | |
| * (recursively enumerated). It also knows the object id of the tree, as the | |
| * tree should be readily available from the repository's object database. | |
| * | |
| * @return true if this tree is knows key details about itself; false if the | |
| * tree needs to be regenerated. | |
| */ | |
| public boolean isValid() { | |
| return id != null; | |
| } | |
| /** | |
| * Get the number of entries this tree spans within the DirCache. | |
| * <p> | |
| * If this tree is not valid (see {@link #isValid()}) this method's return | |
| * value is always strictly negative (less than 0) but is otherwise an | |
| * undefined result. | |
| * | |
| * @return total number of entries (recursively) contained within this tree. | |
| */ | |
| public int getEntrySpan() { | |
| return entrySpan; | |
| } | |
| /** | |
| * Get the number of cached subtrees contained within this tree. | |
| * | |
| * @return number of child trees available through this tree. | |
| */ | |
| public int getChildCount() { | |
| return childCnt; | |
| } | |
| /** | |
| * Get the i-th child cache tree. | |
| * | |
| * @param i | |
| * index of the child to obtain. | |
| * @return the child tree. | |
| */ | |
| public DirCacheTree getChild(int i) { | |
| return children[i]; | |
| } | |
| /** | |
| * Get the tree's ObjectId. | |
| * <p> | |
| * If {@link #isValid()} returns false this method will return null. | |
| * | |
| * @return ObjectId of this tree or null. | |
| * @since 4.3 | |
| */ | |
| public ObjectId getObjectId() { | |
| return id; | |
| } | |
| /** | |
| * Get the tree's name within its parent. | |
| * <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 name of the tree. This does not contain any '/' characters. | |
| */ | |
| public String getNameString() { | |
| final ByteBuffer bb = ByteBuffer.wrap(encodedName); | |
| return UTF_8.decode(bb).toString(); | |
| } | |
| /** | |
| * Get the tree's path within the repository. | |
| * <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 path of the tree, relative to the repository root. If this is not | |
| * the root tree the path ends with '/'. The root tree's path string | |
| * is the empty string (""). | |
| */ | |
| public String getPathString() { | |
| final StringBuilder r = new StringBuilder(); | |
| appendName(r); | |
| return r.toString(); | |
| } | |
| /** | |
| * Write (if necessary) this tree to the object store. | |
| * | |
| * @param cache | |
| * the complete cache from DirCache. | |
| * @param cIdx | |
| * first position of <code>cache</code> that is a member of this | |
| * tree. The path of <code>cache[cacheIdx].path</code> for the | |
| * range <code>[0,pathOff-1)</code> matches the complete path of | |
| * this tree, from the root of the repository. | |
| * @param pathOffset | |
| * number of bytes of <code>cache[cacheIdx].path</code> that | |
| * matches this tree's path. The value at array position | |
| * <code>cache[cacheIdx].path[pathOff-1]</code> is always '/' if | |
| * <code>pathOff</code> is > 0. | |
| * @param ow | |
| * the writer to use when serializing to the store. | |
| * @return identity of this tree. | |
| * @throws UnmergedPathException | |
| * one or more paths contain higher-order stages (stage > 0), | |
| * which cannot be stored in a tree object. | |
| * @throws IOException | |
| * an unexpected error occurred writing to the object store. | |
| */ | |
| ObjectId writeTree(final DirCacheEntry[] cache, int cIdx, | |
| final int pathOffset, final ObjectInserter ow) | |
| throws UnmergedPathException, IOException { | |
| if (id == null) { | |
| final int endIdx = cIdx + entrySpan; | |
| final TreeFormatter fmt = new TreeFormatter(computeSize(cache, | |
| cIdx, pathOffset, ow)); | |
| int childIdx = 0; | |
| int entryIdx = cIdx; | |
| while (entryIdx < endIdx) { | |
| final DirCacheEntry e = cache[entryIdx]; | |
| final byte[] ep = e.path; | |
| if (childIdx < childCnt) { | |
| final DirCacheTree st = children[childIdx]; | |
| if (st.contains(ep, pathOffset, ep.length)) { | |
| fmt.append(st.encodedName, TREE, st.id); | |
| entryIdx += st.entrySpan; | |
| childIdx++; | |
| continue; | |
| } | |
| } | |
| fmt.append(ep, pathOffset, ep.length - pathOffset, e | |
| .getFileMode(), e.idBuffer(), e.idOffset()); | |
| entryIdx++; | |
| } | |
| id = ow.insert(fmt); | |
| } | |
| return id; | |
| } | |
| private int computeSize(final DirCacheEntry[] cache, int cIdx, | |
| final int pathOffset, final ObjectInserter ow) | |
| throws UnmergedPathException, IOException { | |
| final int endIdx = cIdx + entrySpan; | |
| int childIdx = 0; | |
| int entryIdx = cIdx; | |
| int size = 0; | |
| while (entryIdx < endIdx) { | |
| final DirCacheEntry e = cache[entryIdx]; | |
| if (e.getStage() != 0) | |
| throw new UnmergedPathException(e); | |
| final byte[] ep = e.path; | |
| if (childIdx < childCnt) { | |
| final DirCacheTree st = children[childIdx]; | |
| if (st.contains(ep, pathOffset, ep.length)) { | |
| final int stOffset = pathOffset + st.nameLength() + 1; | |
| st.writeTree(cache, entryIdx, stOffset, ow); | |
| size += entrySize(TREE, st.nameLength()); | |
| entryIdx += st.entrySpan; | |
| childIdx++; | |
| continue; | |
| } | |
| } | |
| size += entrySize(e.getFileMode(), ep.length - pathOffset); | |
| entryIdx++; | |
| } | |
| return size; | |
| } | |
| private void appendName(StringBuilder r) { | |
| if (parent != null) { | |
| parent.appendName(r); | |
| r.append(getNameString()); | |
| r.append('/'); | |
| } else if (nameLength() > 0) { | |
| r.append(getNameString()); | |
| r.append('/'); | |
| } | |
| } | |
| final int nameLength() { | |
| return encodedName.length; | |
| } | |
| final boolean contains(byte[] a, int aOff, int aLen) { | |
| final byte[] e = encodedName; | |
| final int eLen = e.length; | |
| for (int eOff = 0; eOff < eLen && aOff < aLen; eOff++, aOff++) | |
| if (e[eOff] != a[aOff]) | |
| return false; | |
| if (aOff >= aLen) | |
| return false; | |
| return a[aOff] == '/'; | |
| } | |
| /** | |
| * Update (if necessary) this tree's entrySpan. | |
| * | |
| * @param cache | |
| * the complete cache from DirCache. | |
| * @param cCnt | |
| * number of entries in <code>cache</code> that are valid for | |
| * iteration. | |
| * @param cIdx | |
| * first position of <code>cache</code> that is a member of this | |
| * tree. The path of <code>cache[cacheIdx].path</code> for the | |
| * range <code>[0,pathOff-1)</code> matches the complete path of | |
| * this tree, from the root of the repository. | |
| * @param pathOff | |
| * number of bytes of <code>cache[cacheIdx].path</code> that | |
| * matches this tree's path. The value at array position | |
| * <code>cache[cacheIdx].path[pathOff-1]</code> is always '/' if | |
| * <code>pathOff</code> is > 0. | |
| */ | |
| void validate(final DirCacheEntry[] cache, final int cCnt, int cIdx, | |
| final int pathOff) { | |
| if (entrySpan >= 0 && cIdx + entrySpan <= cCnt) { | |
| // If we are valid, our children are also valid. | |
| // We have no need to validate them. | |
| // | |
| return; | |
| } | |
| entrySpan = 0; | |
| if (cCnt == 0) { | |
| // Special case of an empty index, and we are the root tree. | |
| // | |
| return; | |
| } | |
| final byte[] firstPath = cache[cIdx].path; | |
| int stIdx = 0; | |
| while (cIdx < cCnt) { | |
| final byte[] currPath = cache[cIdx].path; | |
| if (pathOff > 0 && !peq(firstPath, currPath, pathOff)) { | |
| // The current entry is no longer in this tree. Our | |
| // span is updated and the remainder goes elsewhere. | |
| // | |
| break; | |
| } | |
| DirCacheTree st = stIdx < childCnt ? children[stIdx] : null; | |
| final int cc = namecmp(currPath, pathOff, st); | |
| if (cc > 0) { | |
| // This subtree is now empty. | |
| // | |
| removeChild(stIdx); | |
| continue; | |
| } | |
| if (cc < 0) { | |
| final int p = slash(currPath, pathOff); | |
| if (p < 0) { | |
| // The entry has no '/' and thus is directly in this | |
| // tree. Count it as one of our own. | |
| // | |
| cIdx++; | |
| entrySpan++; | |
| continue; | |
| } | |
| // Build a new subtree for this entry. | |
| // | |
| st = new DirCacheTree(this, currPath, pathOff, p - pathOff); | |
| insertChild(stIdx, st); | |
| } | |
| // The entry is contained in this subtree. | |
| // | |
| assert(st != null); | |
| st.validate(cache, cCnt, cIdx, pathOff + st.nameLength() + 1); | |
| cIdx += st.entrySpan; | |
| entrySpan += st.entrySpan; | |
| stIdx++; | |
| } | |
| // None of our remaining children can be in this tree | |
| // as the current cache entry is after our own name. | |
| // | |
| while (stIdx < childCnt) | |
| removeChild(childCnt - 1); | |
| } | |
| private void insertChild(int stIdx, DirCacheTree st) { | |
| final DirCacheTree[] c = children; | |
| if (childCnt + 1 <= c.length) { | |
| if (stIdx < childCnt) | |
| System.arraycopy(c, stIdx, c, stIdx + 1, childCnt - stIdx); | |
| c[stIdx] = st; | |
| childCnt++; | |
| return; | |
| } | |
| final int n = c.length; | |
| final DirCacheTree[] a = new DirCacheTree[n + 1]; | |
| if (stIdx > 0) | |
| System.arraycopy(c, 0, a, 0, stIdx); | |
| a[stIdx] = st; | |
| if (stIdx < n) | |
| System.arraycopy(c, stIdx, a, stIdx + 1, n - stIdx); | |
| children = a; | |
| childCnt++; | |
| } | |
| private void removeChild(int stIdx) { | |
| final int n = --childCnt; | |
| if (stIdx < n) | |
| System.arraycopy(children, stIdx + 1, children, stIdx, n - stIdx); | |
| children[n] = null; | |
| } | |
| static boolean peq(byte[] a, byte[] b, int aLen) { | |
| if (b.length < aLen) | |
| return false; | |
| for (aLen--; aLen >= 0; aLen--) | |
| if (a[aLen] != b[aLen]) | |
| return false; | |
| return true; | |
| } | |
| private static int namecmp(byte[] a, int aPos, DirCacheTree ct) { | |
| if (ct == null) | |
| return -1; | |
| final byte[] b = ct.encodedName; | |
| final int aLen = a.length; | |
| final int bLen = b.length; | |
| int bPos = 0; | |
| for (; aPos < aLen && bPos < bLen; aPos++, bPos++) { | |
| final int cmp = (a[aPos] & 0xff) - (b[bPos] & 0xff); | |
| if (cmp != 0) | |
| return cmp; | |
| } | |
| if (bPos == bLen) | |
| return a[aPos] == '/' ? 0 : -1; | |
| return aLen - bLen; | |
| } | |
| private static int slash(byte[] a, int aPos) { | |
| final int aLen = a.length; | |
| for (; aPos < aLen; aPos++) | |
| if (a[aPos] == '/') | |
| return aPos; | |
| return -1; | |
| } | |
| @Override | |
| public String toString() { | |
| return getNameString(); | |
| } | |
| } |