diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java index e04b797ab..4d97e7cd8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java @@ -44,9 +44,9 @@ package org.eclipse.jgit.dircache; -import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; +import static org.eclipse.jgit.lib.FileMode.TREE; +import static org.eclipse.jgit.lib.TreeFormatter.entrySize; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; @@ -55,9 +55,9 @@ import java.util.Comparator; import org.eclipse.jgit.errors.UnmergedPathException; import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.FileMode; 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; @@ -315,8 +315,8 @@ public class DirCacheTree { throws UnmergedPathException, IOException { if (id == null) { final int endIdx = cIdx + entrySpan; - final int size = computeSize(cache, cIdx, pathOffset, ow); - final ByteArrayOutputStream out = new ByteArrayOutputStream(size); + final TreeFormatter fmt = new TreeFormatter(computeSize(cache, + cIdx, pathOffset, ow)); int childIdx = 0; int entryIdx = cIdx; @@ -326,27 +326,19 @@ public class DirCacheTree { if (childIdx < childCnt) { final DirCacheTree st = children[childIdx]; if (st.contains(ep, pathOffset, ep.length)) { - FileMode.TREE.copyTo(out); - out.write(' '); - out.write(st.encodedName); - out.write(0); - st.id.copyRawTo(out); - + fmt.append(st.encodedName, TREE, st.id); entryIdx += st.entrySpan; childIdx++; continue; } } - e.getFileMode().copyTo(out); - out.write(' '); - out.write(ep, pathOffset, ep.length - pathOffset); - out.write(0); - out.write(e.idBuffer(), e.idOffset(), OBJECT_ID_LENGTH); + fmt.append(ep, pathOffset, ep.length - pathOffset, e + .getFileMode(), e.idBuffer(), e.idOffset()); entryIdx++; } - id = ow.insert(Constants.OBJ_TREE, out.toByteArray()); + id = fmt.insert(ow); } return id; } @@ -371,9 +363,7 @@ public class DirCacheTree { final int stOffset = pathOffset + st.nameLength() + 1; st.writeTree(cache, entryIdx, stOffset, ow); - size += FileMode.TREE.copyToLength(); - size += st.nameLength(); - size += OBJECT_ID_LENGTH + 2; + size += entrySize(TREE, st.nameLength()); entryIdx += st.entrySpan; childIdx++; @@ -381,10 +371,7 @@ public class DirCacheTree { } } - final FileMode mode = e.getFileMode(); - size += mode.copyToLength(); - size += ep.length - pathOffset; - size += OBJECT_ID_LENGTH + 2; + size += entrySize(e.getFileMode(), ep.length - pathOffset); entryIdx++; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java index 43e4dd9c1..f295f5b72 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java @@ -222,6 +222,23 @@ public abstract class FileMode { os.write(octalBytes); } + /** + * Copy this mode as a sequence of octal US-ASCII bytes. + * + * The mode is copied as a sequence of octal digits using the US-ASCII + * character encoding. The sequence does not use a leading '0' prefix to + * indicate octal notation. This method is suitable for generation of a mode + * string within a GIT tree object. + * + * @param buf + * buffer to copy the mode to. + * @param ptr + * position within {@code buf} for first digit. + */ + public void copyTo(byte[] buf, int ptr) { + System.arraycopy(octalBytes, 0, buf, ptr, octalBytes.length); + } + /** * @return the number of bytes written by {@link #copyTo(OutputStream)}. */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java index 2eb578b88..06ef42997 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java @@ -45,7 +45,6 @@ package org.eclipse.jgit.lib; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.text.MessageFormat; @@ -614,20 +613,16 @@ public class Tree extends TreeEntry implements Treeish { * the tree cannot be loaded, or its not in a writable state. */ public byte[] format() throws IOException { - ByteArrayOutputStream o = new ByteArrayOutputStream(); + TreeFormatter fmt = new TreeFormatter(); for (TreeEntry e : members()) { ObjectId id = e.getId(); if (id == null) throw new ObjectWritingException(MessageFormat.format(JGitText .get().objectAtPathDoesNotHaveId, e.getFullName())); - e.getMode().copyTo(o); - o.write(' '); - o.write(e.getNameUTF8()); - o.write(0); - id.copyRawTo(o); + fmt.append(e.getNameUTF8(), e.getMode(), id); } - return o.toByteArray(); + return fmt.toByteArray(); } public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeFormatter.java new file mode 100644 index 000000000..e14e81f6e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeFormatter.java @@ -0,0 +1,333 @@ +/* + * 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.lib; + +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; +import static org.eclipse.jgit.lib.Constants.OBJ_TREE; +import static org.eclipse.jgit.lib.Constants.encode; +import static org.eclipse.jgit.lib.FileMode.GITLINK; +import static org.eclipse.jgit.lib.FileMode.REGULAR_FILE; +import static org.eclipse.jgit.lib.FileMode.TREE; + +import java.io.IOException; + +import org.eclipse.jgit.revwalk.RevBlob; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.util.TemporaryBuffer; + +/** + * Mutable formatter to construct a single tree object. + * + * This formatter does not process subtrees. Callers must handle creating each + * subtree on their own. + * + * To maintain good performance for bulk operations, this formatter does not + * validate its input. Callers are responsible for ensuring the resulting tree + * object is correctly well formed by writing entries in the correct order. + */ +public class TreeFormatter { + /** + * Compute the size of a tree entry record. + * + * This method can be used to estimate the correct size of a tree prior to + * allocating a formatter. Getting the size correct at allocation time + * ensures the internal buffer is sized correctly, reducing copying. + * + * @param mode + * the mode the entry will have. + * @param nameLen + * the length of the name, in bytes. + * @return the length of the record. + */ + public static int entrySize(FileMode mode, int nameLen) { + return mode.copyToLength() + nameLen + OBJECT_ID_LENGTH + 2; + } + + private byte[] buf; + + private int ptr; + + private TemporaryBuffer.Heap overflowBuffer; + + /** Create an empty formatter with a default buffer size. */ + public TreeFormatter() { + this(8192); + } + + /** + * Create an empty formatter with the specified buffer size. + * + * @param size + * estimated size of the tree, in bytes. Callers can use + * {@link #entrySize(FileMode, int)} to estimate the size of each + * entry in advance of allocating the formatter. + */ + public TreeFormatter(int size) { + buf = new byte[size]; + } + + /** + * Add a link to a submodule commit, mode is {@link #GITLINK}. + * + * @param name + * name of the entry. + * @param commit + * the ObjectId to store in this entry. + */ + public void append(String name, RevCommit commit) { + append(name, GITLINK, commit); + } + + /** + * Add a subtree, mode is {@link #TREE}. + * + * @param name + * name of the entry. + * @param tree + * the ObjectId to store in this entry. + */ + public void append(String name, RevTree tree) { + append(name, TREE, tree); + } + + /** + * Add a regular file, mode is {@link #REGULAR_FILE}. + * + * @param name + * name of the entry. + * @param blob + * the ObjectId to store in this entry. + */ + public void append(String name, RevBlob blob) { + append(name, REGULAR_FILE, blob); + } + + /** + * Append any entry to the tree. + * + * @param name + * name of the entry. + * @param mode + * mode describing the treatment of {@code id}. + * @param id + * the ObjectId to store in this entry. + */ + public void append(String name, FileMode mode, AnyObjectId id) { + append(encode(name), mode, id); + } + + /** + * Append any entry to the tree. + * + * @param name + * name of the entry. The name should be UTF-8 encoded, but file + * name encoding is not a well defined concept in Git. + * @param mode + * mode describing the treatment of {@code id}. + * @param id + * the ObjectId to store in this entry. + */ + public void append(byte[] name, FileMode mode, AnyObjectId id) { + append(name, 0, name.length, mode, id); + } + + /** + * Append any entry to the tree. + * + * @param nameBuf + * buffer holding the name of the entry. The name should be UTF-8 + * encoded, but file name encoding is not a well defined concept + * in Git. + * @param namePos + * first position within {@code nameBuf} of the name data. + * @param nameLen + * number of bytes from {@code nameBuf} to use as the name. + * @param mode + * mode describing the treatment of {@code id}. + * @param id + * the ObjectId to store in this entry. + */ + public void append(byte[] nameBuf, int namePos, int nameLen, FileMode mode, + AnyObjectId id) { + if (fmtBuf(nameBuf, namePos, nameLen, mode)) { + id.copyRawTo(buf, ptr); + ptr += OBJECT_ID_LENGTH; + + } else { + try { + fmtOverflowBuffer(nameBuf, namePos, nameLen, mode); + id.copyRawTo(overflowBuffer); + } catch (IOException badBuffer) { + // This should never occur. + throw new RuntimeException(badBuffer); + } + } + } + + /** + * Append any entry to the tree. + * + * @param nameBuf + * buffer holding the name of the entry. The name should be UTF-8 + * encoded, but file name encoding is not a well defined concept + * in Git. + * @param namePos + * first position within {@code nameBuf} of the name data. + * @param nameLen + * number of bytes from {@code nameBuf} to use as the name. + * @param mode + * mode describing the treatment of {@code id}. + * @param idBuf + * buffer holding the raw ObjectId of the entry. + * @param idPos + * first position within {@code idBuf} to copy the id from. + */ + public void append(byte[] nameBuf, int namePos, int nameLen, FileMode mode, + byte[] idBuf, int idPos) { + if (fmtBuf(nameBuf, namePos, nameLen, mode)) { + System.arraycopy(idBuf, idPos, buf, ptr, OBJECT_ID_LENGTH); + ptr += OBJECT_ID_LENGTH; + + } else { + try { + fmtOverflowBuffer(nameBuf, namePos, nameLen, mode); + overflowBuffer.write(idBuf, idPos, OBJECT_ID_LENGTH); + } catch (IOException badBuffer) { + // This should never occur. + throw new RuntimeException(badBuffer); + } + } + } + + private boolean fmtBuf(byte[] nameBuf, int namePos, int nameLen, + FileMode mode) { + if (buf == null || buf.length < ptr + entrySize(mode, nameLen)) + return false; + + mode.copyTo(buf, ptr); + ptr += mode.copyToLength(); + buf[ptr++] = ' '; + + System.arraycopy(nameBuf, namePos, buf, ptr, nameLen); + ptr += nameLen; + buf[ptr++] = 0; + return true; + } + + private void fmtOverflowBuffer(byte[] nameBuf, int namePos, int nameLen, + FileMode mode) throws IOException { + if (buf != null) { + overflowBuffer = new TemporaryBuffer.Heap(Integer.MAX_VALUE); + overflowBuffer.write(buf, 0, ptr); + buf = null; + } + + mode.copyTo(overflowBuffer); + overflowBuffer.write((byte) ' '); + overflowBuffer.write(nameBuf, namePos, nameLen); + overflowBuffer.write((byte) 0); + } + + /** + * Compute the current tree's ObjectId. + * + * @return computed ObjectId of the tree + */ + public ObjectId getTreeId() { + final ObjectInserter.Formatter fmt = new ObjectInserter.Formatter(); + if (buf != null) + return fmt.idFor(OBJ_TREE, buf, 0, ptr); + + try { + final long len = overflowBuffer.length(); + return fmt.idFor(OBJ_TREE, len, overflowBuffer.openInputStream()); + } catch (IOException err) { + // This should never happen, its read failure on a byte array. + throw new RuntimeException(err); + } + } + + /** + * Insert this tree and obtain its ObjectId. + * + * @param ins + * the inserter to store the tree. + * @return computed ObjectId of the tree + * @throws IOException + * the tree could not be stored. + */ + public ObjectId insert(ObjectInserter ins) throws IOException { + if (buf != null) + return ins.insert(OBJ_TREE, buf, 0, ptr); + + final long len = overflowBuffer.length(); + return ins.insert(OBJ_TREE, len, overflowBuffer.openInputStream()); + } + + /** + * Copy this formatter's buffer into a byte array. + * + * This method is not efficient, as it needs to create a copy of the + * internal buffer in order to supply an array of the correct size to the + * caller. If the buffer is just to pass to an ObjectInserter, consider + * using {@link #insert(ObjectInserter)} instead. + * + * @return a copy of this formatter's buffer. + */ + public byte[] toByteArray() { + if (buf != null) { + byte[] r = new byte[ptr]; + System.arraycopy(buf, 0, r, 0, ptr); + return r; + } + + try { + return overflowBuffer.toByteArray(); + } catch (IOException err) { + // This should never happen, its read failure on a byte array. + throw new RuntimeException(err); + } + } +}