From cad10e6640258fd6bc6bc3183e4dbc61e83bf544 Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Mon, 14 Jun 2010 15:48:53 -0700 Subject: [PATCH] Refactor object writing responsiblities to ObjectDatabase The ObjectInserter API permits ObjectDatabase implementations to control their own object insertion behavior, rather than forcing it to always be a new loose file created in the local filesystem. Inserted objects can also be queued and written asynchronously to the main application, such as by appending into a pack file that is later closed and added to the repository. This change also starts to open the door to non-file based object storage, such as an in-memory HashMap for unit testing, or a more complex system built on top of a distributed hash table. To help existing application code port to the newer interface we are keeping ObjectWriter as a delegation wrapper to the new API. Each ObjectWriter instances holds a reference to an ObjectInserter for the Repository's top-level ObjectDatabase, and it flushes and releases that instance on each object processed. Change-Id: I413224fb95563e7330c82748deb0aada4e0d6ace Signed-off-by: Shawn O. Pearce --- .../org/eclipse/jgit/lib/ReadTreeTest.java | 6 +- .../jgit/lib/AlternateRepositoryDatabase.java | 5 + .../jgit/lib/CachedObjectDatabase.java | 5 + .../src/org/eclipse/jgit/lib/CoreConfig.java | 1 - .../org/eclipse/jgit/lib/FileRepository.java | 17 +- .../org/eclipse/jgit/lib/ObjectDatabase.java | 11 + .../org/eclipse/jgit/lib/ObjectDirectory.java | 14 +- .../jgit/lib/ObjectDirectoryInserter.java | 177 ++++++++ .../org/eclipse/jgit/lib/ObjectInserter.java | 395 ++++++++++++++++++ .../org/eclipse/jgit/lib/ObjectWriter.java | 331 ++++----------- .../src/org/eclipse/jgit/lib/Repository.java | 5 + .../org/eclipse/jgit/util/ChangeIdUtil.java | 11 +- 12 files changed, 695 insertions(+), 283 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDirectoryInserter.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectInserter.java diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReadTreeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReadTreeTest.java index feef66f9a..e2a356466 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReadTreeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReadTreeTest.java @@ -46,10 +46,8 @@ package org.eclipse.jgit.lib; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; @@ -125,11 +123,9 @@ private Tree buildTree(HashMap headEntries) throws IOException { } ObjectId genSha1(String data) { - InputStream is = new ByteArrayInputStream(data.getBytes()); ObjectWriter objectWriter = new ObjectWriter(db); try { - return objectWriter.writeObject(Constants.OBJ_BLOB, data - .getBytes().length, is, true); + return objectWriter.writeBlob(data.getBytes()); } catch (IOException e) { fail(e.toString()); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AlternateRepositoryDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AlternateRepositoryDatabase.java index 40f110684..64b1254cc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AlternateRepositoryDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AlternateRepositoryDatabase.java @@ -85,6 +85,11 @@ public void create() throws IOException { repository.create(); } + @Override + public ObjectInserter newInserter() { + return odb.newInserter(); + } + @Override public boolean exists() { return odb.exists(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CachedObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CachedObjectDatabase.java index 3dcea1636..f593bfca4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CachedObjectDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CachedObjectDatabase.java @@ -129,4 +129,9 @@ public ObjectDatabase newCachedDatabase() { // The situation might become even more tricky if we will consider alternates. return wrapped.newCachedDatabase(); } + + @Override + public ObjectInserter newInserter() { + return wrapped.newInserter(); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java index 9080ec139..ab024c790 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java @@ -74,7 +74,6 @@ private CoreConfig(final Config rc) { } /** - * @see ObjectWriter * @return The compression level to use when storing loose objects */ public int getCompression() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileRepository.java index f5b1bf848..0ad558b94 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileRepository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileRepository.java @@ -249,12 +249,17 @@ public FileRepository(final File d, final File workTree, final File objectDir, } refs = new RefDirectory(this); - if (objectDir != null) - objectDatabase = new ObjectDirectory(fs.resolve(objectDir, ""), - alternateObjectDir, fs); - else - objectDatabase = new ObjectDirectory(fs.resolve(gitDir, "objects"), - alternateObjectDir, fs); + if (objectDir != null) { + objectDatabase = new ObjectDirectory(repoConfig, // + fs.resolve(objectDir, ""), // + alternateObjectDir, // + fs); + } else { + objectDatabase = new ObjectDirectory(repoConfig, // + fs.resolve(gitDir, "objects"), // + alternateObjectDir, // + fs); + } if (indexFile != null) this.indexFile = indexFile; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java index 7eac79fb7..df52ae02f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java @@ -91,6 +91,17 @@ public void create() throws IOException { // Assume no action is required. } + /** + * Create a new {@code ObjectInserter} to insert new objects. + *

+ * The returned inserter is not itself thread-safe, but multiple concurrent + * inserter instances created from the same {@code ObjectDatabase} must be + * thread-safe. + * + * @return writer the caller can use to create objects in this database. + */ + public abstract ObjectInserter newInserter(); + /** * Close any resources held by this database and its active alternates. */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDirectory.java index 9a5bcdb68..ac3c7bf27 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDirectory.java @@ -76,6 +76,8 @@ public class ObjectDirectory extends ObjectDatabase { private static final PackList NO_PACKS = new PackList(-1, -1, new PackFile[0]); + private final Config config; + private final File objects; private final File infoDirectory; @@ -93,6 +95,8 @@ public class ObjectDirectory extends ObjectDatabase { /** * Initialize a reference to an on-disk object directory. * + * @param cfg + * configuration this directory consults for write settings. * @param dir * the location of the objects directory. * @param alternateObjectDir @@ -101,7 +105,8 @@ public class ObjectDirectory extends ObjectDatabase { * the file system abstraction which will be necessary to * perform certain file system operations. */ - public ObjectDirectory(final File dir, File[] alternateObjectDir, FS fs) { + public ObjectDirectory(final Config cfg, final File dir, File[] alternateObjectDir, FS fs) { + config = cfg; objects = dir; this.alternateObjectDir = alternateObjectDir; infoDirectory = new File(objects, "info"); @@ -130,6 +135,11 @@ public void create() throws IOException { packDirectory.mkdir(); } + @Override + public ObjectInserter newInserter() { + return new ObjectDirectoryInserter(this, config); + } + @Override public void closeSelf() { final PackList packs = packList.get(); @@ -501,7 +511,7 @@ private ObjectDatabase openAlternate(File objdir) throws IOException { final Repository db = RepositoryCache.open(FileKey.exact(parent, fs)); return new AlternateRepositoryDatabase(db); } - return new ObjectDirectory(objdir, null, fs); + return new ObjectDirectory(config, objdir, null, fs); } private static final class PackList { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDirectoryInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDirectoryInserter.java new file mode 100644 index 000000000..146d2d625 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDirectoryInserter.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2007, Robin Rosenberg + * Copyright (C) 2008, Shawn O. Pearce + * Copyright (C) 2009, 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 java.io.EOFException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +import org.eclipse.jgit.errors.ObjectWritingException; + +/** Creates loose objects in a {@link ObjectDirectory}. */ +class ObjectDirectoryInserter extends ObjectInserter { + private final ObjectDirectory db; + + private final Config config; + + private Deflater deflate; + + ObjectDirectoryInserter(final ObjectDirectory dest, final Config cfg) { + db = dest; + config = cfg; + } + + @Override + public ObjectId insert(final int type, long len, final InputStream is) + throws IOException { + final MessageDigest md = digest(); + final File tmp = toTemp(md, type, len, is); + final ObjectId id = ObjectId.fromRaw(md.digest()); + if (db.hasObject(id)) { + // Object is already in the repository, remove temporary file. + // + tmp.delete(); + return id; + } + + final File dst = db.fileFor(id); + if (tmp.renameTo(dst)) + return id; + + // Maybe the directory doesn't exist yet as the object + // directories are always lazily created. Note that we + // try the rename first as the directory likely does exist. + // + dst.getParentFile().mkdir(); + if (tmp.renameTo(dst)) + return id; + + if (db.hasObject(id)) { + tmp.delete(); + return id; + } + + // The object failed to be renamed into its proper + // location and it doesn't exist in the repository + // either. We really don't know what went wrong, so + // fail. + // + tmp.delete(); + throw new ObjectWritingException("Unable to create new object: " + dst); + } + + @Override + public void flush() throws IOException { + // Do nothing. Objects are immediately visible. + } + + @Override + public void release() { + if (deflate != null) { + try { + deflate.end(); + } finally { + deflate = null; + } + } + } + + private File toTemp(final MessageDigest md, final int type, long len, + final InputStream is) throws IOException, FileNotFoundException, + Error { + boolean delete = true; + File tmp = File.createTempFile("noz", null, db.getDirectory()); + try { + DigestOutputStream dOut = new DigestOutputStream( + compress(new FileOutputStream(tmp)), md); + try { + dOut.write(Constants.encodedTypeString(type)); + dOut.write((byte) ' '); + dOut.write(Constants.encodeASCII(len)); + dOut.write((byte) 0); + + final byte[] buf = buffer(); + while (len > 0) { + int n = is.read(buf, 0, (int) Math.min(len, buf.length)); + if (n <= 0) + throw shortInput(len); + dOut.write(buf, 0, n); + len -= n; + } + } finally { + dOut.close(); + } + + tmp.setReadOnly(); + delete = false; + return tmp; + } finally { + if (delete) + tmp.delete(); + } + } + + private DeflaterOutputStream compress(final OutputStream out) { + if (deflate == null) + deflate = new Deflater(config.get(CoreConfig.KEY).getCompression()); + else + deflate.reset(); + return new DeflaterOutputStream(out, deflate); + } + + private static EOFException shortInput(long missing) { + return new EOFException("Input did not match supplied length. " + + missing + " bytes are missing."); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectInserter.java new file mode 100644 index 000000000..fd99d39e7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectInserter.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2007, Robin Rosenberg + * Copyright (C) 2008, Shawn O. Pearce + * Copyright (C) 2009, 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 java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.text.MessageFormat; + +import org.eclipse.jgit.JGitText; +import org.eclipse.jgit.errors.ObjectWritingException; + +/** + * Inserts objects into an existing {@code ObjectDatabase}. + *

+ * An inserter is not thread-safe. Individual threads should each obtain their + * own unique inserter instance, or must arrange for locking at a higher level + * to ensure the inserter is in use by no more than one thread at a time. + *

+ * Objects written by an inserter may not be immediately visible for reading + * after the insert method completes. Callers must invoke either + * {@link #release()} or {@link #flush()} prior to updating references or + * otherwise making the returned ObjectIds visible to other code. + */ +public abstract class ObjectInserter { + private static final byte[] htree = Constants.encodeASCII("tree"); + + private static final byte[] hparent = Constants.encodeASCII("parent"); + + private static final byte[] hauthor = Constants.encodeASCII("author"); + + private static final byte[] hcommitter = Constants.encodeASCII("committer"); + + private static final byte[] hencoding = Constants.encodeASCII("encoding"); + + /** An inserter that can be used for formatting and id generation only. */ + public static class Formatter extends ObjectInserter { + @Override + public ObjectId insert(int objectType, long length, InputStream in) + throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void flush() throws IOException { + // Do nothing. + } + + @Override + public void release() { + // Do nothing. + } + } + + /** Digest to compute the name of an object. */ + private final MessageDigest digest; + + /** Temporary working buffer for streaming data through. */ + private byte[] tempBuffer; + + /** Create a new inserter for a database. */ + protected ObjectInserter() { + digest = Constants.newMessageDigest(); + } + + /** @return a temporary byte array for use by the caller. */ + protected byte[] buffer() { + if (tempBuffer == null) + tempBuffer = new byte[8192]; + return tempBuffer; + } + + /** @return digest to help compute an ObjectId */ + protected MessageDigest digest() { + digest.reset(); + return digest; + } + + /** + * Compute the name of an object, without inserting it. + * + * @param type + * type code of the object to store. + * @param data + * complete content of the object. + * @return the name of the object. + */ + public ObjectId idFor(int type, byte[] data) { + return idFor(type, data, 0, data.length); + } + + /** + * Compute the name of an object, without inserting it. + * + * @param type + * type code of the object to store. + * @param data + * complete content of the object. + * @param off + * first position within {@code data}. + * @param len + * number of bytes to copy from {@code data}. + * @return the name of the object. + */ + public ObjectId idFor(int type, byte[] data, int off, int len) { + MessageDigest md = digest(); + md.update(Constants.encodedTypeString(type)); + md.update((byte) ' '); + md.update(Constants.encodeASCII(len)); + md.update((byte) 0); + md.update(data, off, len); + return ObjectId.fromRaw(md.digest()); + } + + /** + * Compute the name of an object, without inserting it. + * + * @param objectType + * type code of the object to store. + * @param length + * number of bytes to scan from {@code in}. + * @param in + * stream providing the object content. The caller is responsible + * for closing the stream. + * @return the name of the object. + * @throws IOException + * the source stream could not be read. + */ + public ObjectId idFor(int objectType, long length, InputStream in) + throws IOException { + MessageDigest md = digest(); + md.update(Constants.encodedTypeString(objectType)); + md.update((byte) ' '); + md.update(Constants.encodeASCII(length)); + md.update((byte) 0); + byte[] buf = buffer(); + while (length > 0) { + int n = in.read(buf, 0, (int) Math.min(length, buf.length)); + if (n < 0) + throw new EOFException("Unexpected end of input"); + md.update(buf, 0, n); + length -= n; + } + return ObjectId.fromRaw(md.digest()); + } + + /** + * Insert a single object into the store, returning its unique name. + * + * @param type + * type code of the object to store. + * @param data + * complete content of the object. + * @return the name of the object. + * @throws IOException + * the object could not be stored. + */ + public ObjectId insert(final int type, final byte[] data) + throws IOException { + return insert(type, data, 0, data.length); + } + + /** + * Insert a single object into the store, returning its unique name. + * + * @param type + * type code of the object to store. + * @param data + * complete content of the object. + * @param off + * first position within {@code data}. + * @param len + * number of bytes to copy from {@code data}. + * @return the name of the object. + * @throws IOException + * the object could not be stored. + */ + public ObjectId insert(int type, byte[] data, int off, int len) + throws IOException { + return insert(type, len, new ByteArrayInputStream(data, off, len)); + } + + /** + * Insert a single object into the store, returning its unique name. + * + * @param objectType + * type code of the object to store. + * @param length + * number of bytes to copy from {@code in}. + * @param in + * stream providing the object content. The caller is responsible + * for closing the stream. + * @return the name of the object. + * @throws IOException + * the object could not be stored, or the source stream could + * not be read. + */ + public abstract ObjectId insert(int objectType, long length, InputStream in) + throws IOException; + + /** + * Make all inserted objects visible. + *

+ * The flush may take some period of time to make the objects available to + * other threads. + * + * @throws IOException + * the flush could not be completed; objects inserted thus far + * are in an indeterminate state. + */ + public abstract void flush() throws IOException; + + /** + * Release any resources used by this inserter. + *

+ * An inserter that has been released can be used again, but may need to be + * released after the subsequent usage. + */ + public abstract void release(); + + /** + * Format a Tree in canonical format. + * + * @param tree + * the tree object to format + * @return canonical encoding of the tree object. + * @throws IOException + * the tree cannot be loaded, or its not in a writable state. + */ + public final byte[] format(Tree tree) throws IOException { + ByteArrayOutputStream o = new ByteArrayOutputStream(); + for (TreeEntry e : tree.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); + } + return o.toByteArray(); + } + + /** + * Format a Commit in canonical format. + * + * @param commit + * the commit object to format + * @return canonical encoding of the commit object. + * @throws UnsupportedEncodingException + * the commit's chosen encoding isn't supported on this JVM. + */ + public final byte[] format(Commit commit) + throws UnsupportedEncodingException { + String encoding = commit.getEncoding(); + if (encoding == null) + encoding = Constants.CHARACTER_ENCODING; + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + OutputStreamWriter w = new OutputStreamWriter(os, encoding); + try { + os.write(htree); + os.write(' '); + commit.getTreeId().copyTo(os); + os.write('\n'); + + ObjectId[] ps = commit.getParentIds(); + for (int i = 0; i < ps.length; ++i) { + os.write(hparent); + os.write(' '); + ps[i].copyTo(os); + os.write('\n'); + } + + os.write(hauthor); + os.write(' '); + w.write(commit.getAuthor().toExternalString()); + w.flush(); + os.write('\n'); + + os.write(hcommitter); + os.write(' '); + w.write(commit.getCommitter().toExternalString()); + w.flush(); + os.write('\n'); + + if (!encoding.equals(Constants.CHARACTER_ENCODING)) { + os.write(hencoding); + os.write(' '); + os.write(Constants.encodeASCII(encoding)); + os.write('\n'); + } + + os.write('\n'); + w.write(commit.getMessage()); + w.flush(); + } catch (IOException err) { + // This should never occur, the only way to get it above is + // for the ByteArrayOutputStream to throw, but it doesn't. + // + throw new RuntimeException(err); + } + return os.toByteArray(); + } + + /** + * Format a Tag in canonical format. + * + * @param tag + * the tag object to format + * @return canonical encoding of the tag object. + */ + public final byte[] format(Tag tag) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + OutputStreamWriter w = new OutputStreamWriter(os, Constants.CHARSET); + try { + w.write("object "); + tag.getObjId().copyTo(w); + w.write('\n'); + + w.write("type "); + w.write(tag.getType()); + w.write("\n"); + + w.write("tag "); + w.write(tag.getTag()); + w.write("\n"); + + w.write("tagger "); + w.write(tag.getAuthor().toExternalString()); + w.write('\n'); + + w.write('\n'); + w.write(tag.getMessage()); + w.close(); + } catch (IOException err) { + // This should never occur, the only way to get it above is + // for the ByteArrayOutputStream to throw, but it doesn't. + // + throw new RuntimeException(err); + } + return os.toByteArray(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectWriter.java index 3ba67476e..ce91efb8e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectWriter.java @@ -1,6 +1,7 @@ /* * Copyright (C) 2007, Robin Rosenberg * Copyright (C) 2006-2008, Shawn O. Pearce + * Copyright (C) 2009, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -44,61 +45,49 @@ package org.eclipse.jgit.lib; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; +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_TAG; +import static org.eclipse.jgit.lib.Constants.OBJ_TREE; + import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStreamWriter; -import java.security.MessageDigest; -import java.text.MessageFormat; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; - -import org.eclipse.jgit.JGitText; -import org.eclipse.jgit.errors.ObjectWritingException; /** * A class for writing loose objects. + * + * @deprecated Use {@link Repository#newObjectInserter()}. */ public class ObjectWriter { - private static final byte[] htree = Constants.encodeASCII("tree"); - - private static final byte[] hparent = Constants.encodeASCII("parent"); - - private static final byte[] hauthor = Constants.encodeASCII("author"); - - private static final byte[] hcommitter = Constants.encodeASCII("committer"); - - private static final byte[] hencoding = Constants.encodeASCII("encoding"); - - private final Repository r; - - private final byte[] buf; - - private final MessageDigest md; + private final ObjectInserter inserter; /** * Construct an Object writer for the specified repository + * * @param d */ public ObjectWriter(final Repository d) { - r = d; - buf = new byte[8192]; - md = Constants.newMessageDigest(); + inserter = d.newObjectInserter(); } /** * Write a blob with the specified data * - * @param b bytes of the blob + * @param b + * bytes of the blob * @return SHA-1 of the blob * @throws IOException */ public ObjectId writeBlob(final byte[] b) throws IOException { - return writeBlob(b.length, new ByteArrayInputStream(b)); + try { + ObjectId id = inserter.insert(OBJ_BLOB, b); + inserter.flush(); + return id; + } finally { + inserter.release(); + } } /** @@ -130,174 +119,101 @@ public ObjectId writeBlob(final File f) throws IOException { */ public ObjectId writeBlob(final long len, final InputStream is) throws IOException { - return writeObject(Constants.OBJ_BLOB, len, is, true); + try { + ObjectId id = inserter.insert(OBJ_BLOB, len, is); + inserter.flush(); + return id; + } finally { + inserter.release(); + } } /** * Write a Tree to the object database. * - * @param t + * @param tree * Tree * @return SHA-1 of the tree * @throws IOException */ - public ObjectId writeTree(final Tree t) throws IOException { - final ByteArrayOutputStream o = new ByteArrayOutputStream(); - final TreeEntry[] items = t.members(); - for (int k = 0; k < items.length; k++) { - final TreeEntry e = items[k]; - final 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); + public ObjectId writeTree(Tree tree) throws IOException { + try { + ObjectId id = inserter.insert(OBJ_TREE, inserter.format(tree)); + inserter.flush(); + return id; + } finally { + inserter.release(); } - return writeCanonicalTree(o.toByteArray()); } /** * Write a canonical tree to the object database. * - * @param b + * @param treeData * the canonical encoding of the tree object. * @return SHA-1 of the tree * @throws IOException */ - public ObjectId writeCanonicalTree(final byte[] b) throws IOException { - return writeTree(b.length, new ByteArrayInputStream(b)); - } - - private ObjectId writeTree(final long len, final InputStream is) - throws IOException { - return writeObject(Constants.OBJ_TREE, len, is, true); + public ObjectId writeCanonicalTree(byte[] treeData) throws IOException { + try { + ObjectId id = inserter.insert(OBJ_TREE, treeData); + inserter.flush(); + return id; + } finally { + inserter.release(); + } } /** * Write a Commit to the object database * - * @param c + * @param commit * Commit to store * @return SHA-1 of the commit * @throws IOException */ - public ObjectId writeCommit(final Commit c) throws IOException { - final ByteArrayOutputStream os = new ByteArrayOutputStream(); - String encoding = c.getEncoding(); - if (encoding == null) - encoding = Constants.CHARACTER_ENCODING; - final OutputStreamWriter w = new OutputStreamWriter(os, encoding); - - os.write(htree); - os.write(' '); - c.getTreeId().copyTo(os); - os.write('\n'); - - ObjectId[] ps = c.getParentIds(); - for (int i=0; i 0 - && (n = is.read(buf, 0, (int) Math.min(len, buf.length))) > 0) { - md.update(buf, 0, n); - if (deflateStream != null) - deflateStream.write(buf, 0, n); - len -= n; - } - - if (len != 0) - throw new IOException("Input did not match supplied length. " - + len + " bytes are missing."); - - if (deflateStream != null ) { - deflateStream.close(); - if (t != null) - t.setReadOnly(); - } - - id = ObjectId.fromRaw(md.digest()); + return inserter.idFor(type, len, is); } finally { - if (id == null && deflateStream != null) { - try { - deflateStream.close(); - } finally { - t.delete(); - } - } - if (def != null) { - def.end(); - } + inserter.release(); } - - if (t == null) - return id; - - if (r.hasObject(id)) { - // Object is already in the repository so remove - // the temporary file. - // - t.delete(); - } else { - final File o = r.toFile(id); - if (!t.renameTo(o)) { - // Maybe the directory doesn't exist yet as the object - // directories are always lazily created. Note that we - // try the rename first as the directory likely does exist. - // - o.getParentFile().mkdir(); - if (!t.renameTo(o)) { - if (!r.hasObject(id)) { - // The object failed to be renamed into its proper - // location and it doesn't exist in the repository - // either. We really don't know what went wrong, so - // fail. - // - t.delete(); - throw new ObjectWritingException("Unable to" - + " create new object: " + o); - } - } - } - } - - return id; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java index 201c7a3fd..334581415 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -148,6 +148,11 @@ public File getDirectory() { */ public abstract ObjectDatabase getObjectDatabase(); + /** @return a new inserter to create objects in {@link #getObjectDatabase()} */ + public ObjectInserter newObjectInserter() { + return getObjectDatabase().newInserter(); + } + /** @return the reference database which stores the reference namespace. */ public abstract RefDatabase getRefDatabase(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java index 6c146f79f..2ffcbed9f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java @@ -42,13 +42,12 @@ */ package org.eclipse.jgit.util; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.regex.Pattern; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.ObjectWriter; import org.eclipse.jgit.lib.PersonIdent; /** @@ -113,12 +112,8 @@ public static ObjectId computeChangeId(final ObjectId treeId, b.append(committer.toExternalString()); b.append("\n\n"); b.append(cleanMessage); - ObjectWriter w = new ObjectWriter(null); - byte[] bytes = b.toString().getBytes(Constants.CHARACTER_ENCODING); - ByteArrayInputStream is = new ByteArrayInputStream(bytes); - ObjectId sha1 = w.computeObjectSha1(Constants.OBJ_COMMIT, bytes.length, - is); - return sha1; + return new ObjectInserter.Formatter().idFor(Constants.OBJ_COMMIT, // + b.toString().getBytes(Constants.CHARACTER_ENCODING)); } private static final Pattern issuePattern = Pattern