/* * Copyright (C) 2008-2010, Google Inc. * Copyright (C) 2008, Marek Zawirski * 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.storage.pack; import static org.eclipse.jgit.storage.pack.StoredObjectRepresentation.PACK_DELTA; import static org.eclipse.jgit.storage.pack.StoredObjectRepresentation.PACK_WHOLE; import java.io.IOException; import java.io.OutputStream; import java.security.MessageDigest; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import org.eclipse.jgit.JGitText; 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.errors.StoredObjectRepresentationNotAvailableException; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.AsyncObjectSizeQueue; import org.eclipse.jgit.lib.BatchingProgressMonitor; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdOwnerMap; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.ThreadSafeProgressMonitor; import org.eclipse.jgit.revwalk.AsyncRevObjectQueue; import org.eclipse.jgit.revwalk.ObjectWalk; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevFlag; import org.eclipse.jgit.revwalk.RevFlagSet; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevTag; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.storage.file.PackIndexWriter; import org.eclipse.jgit.util.BlockList; import org.eclipse.jgit.util.TemporaryBuffer; /** *

* PackWriter class is responsible for generating pack files from specified set * of objects from repository. This implementation produce pack files in format * version 2. *

*

* Source of objects may be specified in two ways: *

* Typical usage consists of creating instance intended for some pack, * configuring options, preparing the list of objects by calling * {@link #preparePack(Iterator)} or * {@link #preparePack(ProgressMonitor, Collection, Collection)}, and finally * producing the stream with {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}. *

*

* Class provide set of configurable options and {@link ProgressMonitor} * support, as operations may take a long time for big repositories. Deltas * searching algorithm is NOT IMPLEMENTED yet - this implementation * relies only on deltas and objects reuse. *

*

* This class is not thread safe, it is intended to be used in one thread, with * one instance per created pack. Subsequent calls to writePack result in * undefined behavior. *

*/ public class PackWriter { private static final int PACK_VERSION_GENERATED = 2; @SuppressWarnings("unchecked") private final BlockList objectsLists[] = new BlockList[Constants.OBJ_TAG + 1]; { objectsLists[Constants.OBJ_COMMIT] = new BlockList(); objectsLists[Constants.OBJ_TREE] = new BlockList(); objectsLists[Constants.OBJ_BLOB] = new BlockList(); objectsLists[Constants.OBJ_TAG] = new BlockList(); } private final ObjectIdOwnerMap objectsMap = new ObjectIdOwnerMap(); // edge objects for thin packs private List edgeObjects = new BlockList(); private List cachedPacks = new ArrayList(2); private Set tagTargets = Collections.emptySet(); private Deflater myDeflater; private final ObjectReader reader; /** {@link #reader} recast to the reuse interface, if it supports it. */ private final ObjectReuseAsIs reuseSupport; private final PackConfig config; private final Statistics stats; private Statistics.ObjectType typeStats; private List sortedByName; private byte packcsum[]; private boolean deltaBaseAsOffset; private boolean reuseDeltas; private boolean reuseDeltaCommits; private boolean reuseValidate; private boolean thin; private boolean useCachedPacks; private boolean ignoreMissingUninteresting = true; private boolean pruneCurrentObjectList; /** * Create writer for specified repository. *

* Objects for packing are specified in {@link #preparePack(Iterator)} or * {@link #preparePack(ProgressMonitor, Collection, Collection)}. * * @param repo * repository where objects are stored. */ public PackWriter(final Repository repo) { this(repo, repo.newObjectReader()); } /** * Create a writer to load objects from the specified reader. *

* Objects for packing are specified in {@link #preparePack(Iterator)} or * {@link #preparePack(ProgressMonitor, Collection, Collection)}. * * @param reader * reader to read from the repository with. */ public PackWriter(final ObjectReader reader) { this(new PackConfig(), reader); } /** * Create writer for specified repository. *

* Objects for packing are specified in {@link #preparePack(Iterator)} or * {@link #preparePack(ProgressMonitor, Collection, Collection)}. * * @param repo * repository where objects are stored. * @param reader * reader to read from the repository with. */ public PackWriter(final Repository repo, final ObjectReader reader) { this(new PackConfig(repo), reader); } /** * Create writer with a specified configuration. *

* Objects for packing are specified in {@link #preparePack(Iterator)} or * {@link #preparePack(ProgressMonitor, Collection, Collection)}. * * @param config * configuration for the pack writer. * @param reader * reader to read from the repository with. */ public PackWriter(final PackConfig config, final ObjectReader reader) { this.config = config; this.reader = reader; if (reader instanceof ObjectReuseAsIs) reuseSupport = ((ObjectReuseAsIs) reader); else reuseSupport = null; deltaBaseAsOffset = config.isDeltaBaseAsOffset(); reuseDeltas = config.isReuseDeltas(); reuseValidate = true; // be paranoid by default stats = new Statistics(); } /** * Check whether writer can store delta base as an offset (new style * reducing pack size) or should store it as an object id (legacy style, * compatible with old readers). * * Default setting: {@value PackConfig#DEFAULT_DELTA_BASE_AS_OFFSET} * * @return true if delta base is stored as an offset; false if it is stored * as an object id. */ public boolean isDeltaBaseAsOffset() { return deltaBaseAsOffset; } /** * Set writer delta base format. Delta base can be written as an offset in a * pack file (new approach reducing file size) or as an object id (legacy * approach, compatible with old readers). * * Default setting: {@value PackConfig#DEFAULT_DELTA_BASE_AS_OFFSET} * * @param deltaBaseAsOffset * boolean indicating whether delta base can be stored as an * offset. */ public void setDeltaBaseAsOffset(boolean deltaBaseAsOffset) { this.deltaBaseAsOffset = deltaBaseAsOffset; } /** * Check if the writer will reuse commits that are already stored as deltas. * * @return true if the writer would reuse commits stored as deltas, assuming * delta reuse is already enabled. */ public boolean isReuseDeltaCommits() { return reuseDeltaCommits; } /** * Set the writer to reuse existing delta versions of commits. * * @param reuse * if true, the writer will reuse any commits stored as deltas. * By default the writer does not reuse delta commits. */ public void setReuseDeltaCommits(boolean reuse) { reuseDeltaCommits = reuse; } /** * Check if the writer validates objects before copying them. * * @return true if validation is enabled; false if the reader will handle * object validation as a side-effect of it consuming the output. */ public boolean isReuseValidatingObjects() { return reuseValidate; } /** * Enable (or disable) object validation during packing. * * @param validate * if true the pack writer will validate an object before it is * put into the output. This additional validation work may be * necessary to avoid propagating corruption from one local pack * file to another local pack file. */ public void setReuseValidatingObjects(boolean validate) { reuseValidate = validate; } /** @return true if this writer is producing a thin pack. */ public boolean isThin() { return thin; } /** * @param packthin * a boolean indicating whether writer may pack objects with * delta base object not within set of objects to pack, but * belonging to party repository (uninteresting/boundary) as * determined by set; this kind of pack is used only for * transport; true - to produce thin pack, false - otherwise. */ public void setThin(final boolean packthin) { thin = packthin; } /** @return true to reuse cached packs. If true index creation isn't available. */ public boolean isUseCachedPacks() { return useCachedPacks; } /** * @param useCached * if set to true and a cached pack is present, it will be * appended onto the end of a thin-pack, reducing the amount of * working set space and CPU used by PackWriter. Enabling this * feature prevents PackWriter from creating an index for the * newly created pack, so its only suitable for writing to a * network client, where the client will make the index. */ public void setUseCachedPacks(boolean useCached) { useCachedPacks = useCached; } /** * @return true to ignore objects that are uninteresting and also not found * on local disk; false to throw a {@link MissingObjectException} * out of {@link #preparePack(ProgressMonitor, Collection, Collection)} if an * uninteresting object is not in the source repository. By default, * true, permitting gracefully ignoring of uninteresting objects. */ public boolean isIgnoreMissingUninteresting() { return ignoreMissingUninteresting; } /** * @param ignore * true if writer should ignore non existing uninteresting * objects during construction set of objects to pack; false * otherwise - non existing uninteresting objects may cause * {@link MissingObjectException} */ public void setIgnoreMissingUninteresting(final boolean ignore) { ignoreMissingUninteresting = ignore; } /** * Set the tag targets that should be hoisted earlier during packing. *

* Callers may put objects into this set before invoking any of the * preparePack methods to influence where an annotated tag's target is * stored within the resulting pack. Typically these will be clustered * together, and hoisted earlier in the file even if they are ancient * revisions, allowing readers to find tag targets with better locality. * * @param objects * objects that annotated tags point at. */ public void setTagTargets(Set objects) { tagTargets = objects; } /** * Returns objects number in a pack file that was created by this writer. * * @return number of objects in pack. * @throws IOException * a cached pack cannot supply its object count. */ public long getObjectCount() throws IOException { if (stats.totalObjects == 0) { long objCnt = 0; objCnt += objectsLists[Constants.OBJ_COMMIT].size(); objCnt += objectsLists[Constants.OBJ_TREE].size(); objCnt += objectsLists[Constants.OBJ_BLOB].size(); objCnt += objectsLists[Constants.OBJ_TAG].size(); for (CachedPack pack : cachedPacks) objCnt += pack.getObjectCount(); return objCnt; } return stats.totalObjects; } /** * Prepare the list of objects to be written to the pack stream. *

* Iterator exactly determines which objects are included in a pack * and order they appear in pack (except that objects order by type is not * needed at input). This order should conform general rules of ordering * objects in git - by recency and path (type and delta-base first is * internally secured) and responsibility for guaranteeing this order is on * a caller side. Iterator must return each id of object to write exactly * once. *

* * @param objectsSource * iterator of object to store in a pack; order of objects within * each type is important, ordering by type is not needed; * allowed types for objects are {@link Constants#OBJ_COMMIT}, * {@link Constants#OBJ_TREE}, {@link Constants#OBJ_BLOB} and * {@link Constants#OBJ_TAG}; objects returned by iterator may be * later reused by caller as object id and type are internally * copied in each iteration. * @throws IOException * when some I/O problem occur during reading objects. */ public void preparePack(final Iterator objectsSource) throws IOException { while (objectsSource.hasNext()) { addObject(objectsSource.next()); } } /** * Prepare the list of objects to be written to the pack stream. *

* Basing on these 2 sets, another set of objects to put in a pack file is * created: this set consists of all objects reachable (ancestors) from * interesting objects, except uninteresting objects and their ancestors. * This method uses class {@link ObjectWalk} extensively to find out that * appropriate set of output objects and their optimal order in output pack. * Order is consistent with general git in-pack rules: sort by object type, * recency, path and delta-base first. *

* * @param countingMonitor * progress during object enumeration. * @param want * collection of objects to be marked as interesting (start * points of graph traversal). * @param have * collection of objects to be marked as uninteresting (end * points of graph traversal). * @throws IOException * when some I/O problem occur during reading objects. * @deprecated to be removed in 2.0; use the Set version of this method. */ @Deprecated public void preparePack(ProgressMonitor countingMonitor, final Collection want, final Collection have) throws IOException { preparePack(countingMonitor, ensureSet(want), ensureSet(have)); } /** * Prepare the list of objects to be written to the pack stream. *

* Basing on these 2 sets, another set of objects to put in a pack file is * created: this set consists of all objects reachable (ancestors) from * interesting objects, except uninteresting objects and their ancestors. * This method uses class {@link ObjectWalk} extensively to find out that * appropriate set of output objects and their optimal order in output pack. * Order is consistent with general git in-pack rules: sort by object type, * recency, path and delta-base first. *

* * @param countingMonitor * progress during object enumeration. * @param walk * ObjectWalk to perform enumeration. * @param interestingObjects * collection of objects to be marked as interesting (start * points of graph traversal). * @param uninterestingObjects * collection of objects to be marked as uninteresting (end * points of graph traversal). * @throws IOException * when some I/O problem occur during reading objects. * @deprecated to be removed in 2.0; use the Set version of this method. */ @Deprecated public void preparePack(ProgressMonitor countingMonitor, final ObjectWalk walk, final Collection interestingObjects, final Collection uninterestingObjects) throws IOException { preparePack(countingMonitor, walk, ensureSet(interestingObjects), ensureSet(uninterestingObjects)); } @SuppressWarnings("unchecked") private static Set ensureSet(Collection objs) { Set set; if (objs instanceof Set) set = (Set) objs; else if (objs == null) set = Collections.emptySet(); else set = new HashSet(objs); return set; } /** * Prepare the list of objects to be written to the pack stream. *

* Basing on these 2 sets, another set of objects to put in a pack file is * created: this set consists of all objects reachable (ancestors) from * interesting objects, except uninteresting objects and their ancestors. * This method uses class {@link ObjectWalk} extensively to find out that * appropriate set of output objects and their optimal order in output pack. * Order is consistent with general git in-pack rules: sort by object type, * recency, path and delta-base first. *

* * @param countingMonitor * progress during object enumeration. * @param want * collection of objects to be marked as interesting (start * points of graph traversal). * @param have * collection of objects to be marked as uninteresting (end * points of graph traversal). * @throws IOException * when some I/O problem occur during reading objects. */ public void preparePack(ProgressMonitor countingMonitor, Set want, Set have) throws IOException { ObjectWalk ow = new ObjectWalk(reader); preparePack(countingMonitor, ow, want, have); } /** * Prepare the list of objects to be written to the pack stream. *

* Basing on these 2 sets, another set of objects to put in a pack file is * created: this set consists of all objects reachable (ancestors) from * interesting objects, except uninteresting objects and their ancestors. * This method uses class {@link ObjectWalk} extensively to find out that * appropriate set of output objects and their optimal order in output pack. * Order is consistent with general git in-pack rules: sort by object type, * recency, path and delta-base first. *

* * @param countingMonitor * progress during object enumeration. * @param walk * ObjectWalk to perform enumeration. * @param interestingObjects * collection of objects to be marked as interesting (start * points of graph traversal). * @param uninterestingObjects * collection of objects to be marked as uninteresting (end * points of graph traversal). * @throws IOException * when some I/O problem occur during reading objects. */ public void preparePack(ProgressMonitor countingMonitor, final ObjectWalk walk, final Set interestingObjects, final Set uninterestingObjects) throws IOException { if (countingMonitor == null) countingMonitor = NullProgressMonitor.INSTANCE; findObjectsToPack(countingMonitor, walk, interestingObjects, uninterestingObjects); } /** * Determine if the pack file will contain the requested object. * * @param id * the object to test the existence of. * @return true if the object will appear in the output pack file. * @throws IOException * a cached pack cannot be examined. */ public boolean willInclude(final AnyObjectId id) throws IOException { ObjectToPack obj = objectsMap.get(id); return obj != null && !obj.isEdge(); } /** * Lookup the ObjectToPack object for a given ObjectId. * * @param id * the object to find in the pack. * @return the object we are packing, or null. */ public ObjectToPack get(AnyObjectId id) { ObjectToPack obj = objectsMap.get(id); return obj != null && !obj.isEdge() ? obj : null; } /** * Computes SHA-1 of lexicographically sorted objects ids written in this * pack, as used to name a pack file in repository. * * @return ObjectId representing SHA-1 name of a pack that was created. */ public ObjectId computeName() { final byte[] buf = new byte[Constants.OBJECT_ID_LENGTH]; final MessageDigest md = Constants.newMessageDigest(); for (ObjectToPack otp : sortByName()) { otp.copyRawTo(buf, 0); md.update(buf, 0, Constants.OBJECT_ID_LENGTH); } return ObjectId.fromRaw(md.digest()); } /** * Create an index file to match the pack file just written. *

* This method can only be invoked after * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)} has * been invoked and completed successfully. Writing a corresponding index is * an optional feature that not all pack users may require. * * @param indexStream * output for the index data. Caller is responsible for closing * this stream. * @throws IOException * the index data could not be written to the supplied stream. */ public void writeIndex(final OutputStream indexStream) throws IOException { if (!cachedPacks.isEmpty()) throw new IOException(JGitText.get().cachedPacksPreventsIndexCreation); final List list = sortByName(); final PackIndexWriter iw; int indexVersion = config.getIndexVersion(); if (indexVersion <= 0) iw = PackIndexWriter.createOldestPossible(indexStream, list); else iw = PackIndexWriter.createVersion(indexStream, indexVersion); iw.write(list, packcsum); } private List sortByName() { if (sortedByName == null) { int cnt = 0; cnt += objectsLists[Constants.OBJ_COMMIT].size(); cnt += objectsLists[Constants.OBJ_TREE].size(); cnt += objectsLists[Constants.OBJ_BLOB].size(); cnt += objectsLists[Constants.OBJ_TAG].size(); sortedByName = new BlockList(cnt); sortedByName.addAll(objectsLists[Constants.OBJ_COMMIT]); sortedByName.addAll(objectsLists[Constants.OBJ_TREE]); sortedByName.addAll(objectsLists[Constants.OBJ_BLOB]); sortedByName.addAll(objectsLists[Constants.OBJ_TAG]); Collections.sort(sortedByName); } return sortedByName; } /** * Write the prepared pack to the supplied stream. *

* At first, this method collects and sorts objects to pack, then deltas * search is performed if set up accordingly, finally pack stream is * written. *

*

* All reused objects data checksum (Adler32/CRC32) is computed and * validated against existing checksum. *

* * @param compressMonitor * progress monitor to report object compression work. * @param writeMonitor * progress monitor to report the number of objects written. * @param packStream * output stream of pack data. The stream should be buffered by * the caller. The caller is responsible for closing the stream. * @throws IOException * an error occurred reading a local object's data to include in * the pack, or writing compressed object data to the output * stream. */ public void writePack(ProgressMonitor compressMonitor, ProgressMonitor writeMonitor, OutputStream packStream) throws IOException { if (compressMonitor == null) compressMonitor = NullProgressMonitor.INSTANCE; if (writeMonitor == null) writeMonitor = NullProgressMonitor.INSTANCE; boolean needSearchForReuse = reuseSupport != null && ( reuseDeltas || config.isReuseObjects() || !cachedPacks.isEmpty()); if (compressMonitor instanceof BatchingProgressMonitor) { long delay = 1000; if (needSearchForReuse && config.isDeltaCompress()) delay = 500; ((BatchingProgressMonitor) compressMonitor).setDelayStart( delay, TimeUnit.MILLISECONDS); } if (needSearchForReuse) searchForReuse(compressMonitor); if (config.isDeltaCompress()) searchForDeltas(compressMonitor); final PackOutputStream out = new PackOutputStream(writeMonitor, packStream, this); long objCnt = getObjectCount(); stats.totalObjects = objCnt; writeMonitor.beginTask(JGitText.get().writingObjects, (int) objCnt); long writeStart = System.currentTimeMillis(); out.writeFileHeader(PACK_VERSION_GENERATED, objCnt); out.flush(); writeObjects(out); if (!edgeObjects.isEmpty() || !cachedPacks.isEmpty()) { for (Statistics.ObjectType typeStat : stats.objectTypes) { if (typeStat == null) continue; stats.thinPackBytes += typeStat.bytes; } } for (CachedPack pack : cachedPacks) { long deltaCnt = pack.getDeltaCount(); stats.reusedObjects += pack.getObjectCount(); stats.reusedDeltas += deltaCnt; stats.totalDeltas += deltaCnt; reuseSupport.copyPackAsIs(out, pack, reuseValidate); } writeChecksum(out); out.flush(); stats.timeWriting = System.currentTimeMillis() - writeStart; stats.totalBytes = out.length(); stats.reusedPacks = Collections.unmodifiableList(cachedPacks); for (Statistics.ObjectType typeStat : stats.objectTypes) { if (typeStat == null) continue; typeStat.cntDeltas += typeStat.reusedDeltas; stats.reusedObjects += typeStat.reusedObjects; stats.reusedDeltas += typeStat.reusedDeltas; stats.totalDeltas += typeStat.cntDeltas; } reader.release(); writeMonitor.endTask(); } /** * @return description of what this PackWriter did in order to create the * final pack stream. The object is only available to callers after * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)} */ public Statistics getStatistics() { return stats; } /** Release all resources used by this writer. */ public void release() { reader.release(); if (myDeflater != null) { myDeflater.end(); myDeflater = null; } } private void searchForReuse(ProgressMonitor monitor) throws IOException { int cnt = 0; cnt += objectsLists[Constants.OBJ_COMMIT].size(); cnt += objectsLists[Constants.OBJ_TREE].size(); cnt += objectsLists[Constants.OBJ_BLOB].size(); cnt += objectsLists[Constants.OBJ_TAG].size(); long start = System.currentTimeMillis(); monitor.beginTask(JGitText.get().searchForReuse, cnt); if (cnt <= 4096) { // For small object counts, do everything as one list. BlockList tmp = new BlockList(cnt); tmp.addAll(objectsLists[Constants.OBJ_COMMIT]); tmp.addAll(objectsLists[Constants.OBJ_TREE]); tmp.addAll(objectsLists[Constants.OBJ_BLOB]); tmp.addAll(objectsLists[Constants.OBJ_TAG]); searchForReuse(monitor, tmp); if (pruneCurrentObjectList) { // If the list was pruned, we need to re-prune the main lists. pruneEdgesFromObjectList(objectsLists[Constants.OBJ_COMMIT]); pruneEdgesFromObjectList(objectsLists[Constants.OBJ_TREE]); pruneEdgesFromObjectList(objectsLists[Constants.OBJ_BLOB]); pruneEdgesFromObjectList(objectsLists[Constants.OBJ_TAG]); } } else { searchForReuse(monitor, objectsLists[Constants.OBJ_COMMIT]); searchForReuse(monitor, objectsLists[Constants.OBJ_TREE]); searchForReuse(monitor, objectsLists[Constants.OBJ_BLOB]); searchForReuse(monitor, objectsLists[Constants.OBJ_TAG]); } monitor.endTask(); stats.timeSearchingForReuse = System.currentTimeMillis() - start; } private void searchForReuse(ProgressMonitor monitor, List list) throws IOException, MissingObjectException { pruneCurrentObjectList = false; reuseSupport.selectObjectRepresentation(this, monitor, list); if (pruneCurrentObjectList) pruneEdgesFromObjectList(list); } private void searchForDeltas(ProgressMonitor monitor) throws MissingObjectException, IncorrectObjectTypeException, IOException { // Commits and annotated tags tend to have too many differences to // really benefit from delta compression. Consequently just don't // bother examining those types here. // ObjectToPack[] list = new ObjectToPack[ objectsLists[Constants.OBJ_TREE].size() + objectsLists[Constants.OBJ_BLOB].size() + edgeObjects.size()]; int cnt = 0; cnt = findObjectsNeedingDelta(list, cnt, Constants.OBJ_TREE); cnt = findObjectsNeedingDelta(list, cnt, Constants.OBJ_BLOB); if (cnt == 0) return; int nonEdgeCnt = cnt; // Queue up any edge objects that we might delta against. We won't // be sending these as we assume the other side has them, but we need // them in the search phase below. // for (ObjectToPack eo : edgeObjects) { eo.setWeight(0); list[cnt++] = eo; } // Compute the sizes of the objects so we can do a proper sort. // We let the reader skip missing objects if it chooses. For // some readers this can be a huge win. We detect missing objects // by having set the weights above to 0 and allowing the delta // search code to discover the missing object and skip over it, or // abort with an exception if we actually had to have it. // final long sizingStart = System.currentTimeMillis(); monitor.beginTask(JGitText.get().searchForSizes, cnt); AsyncObjectSizeQueue sizeQueue = reader.getObjectSize( Arrays. asList(list).subList(0, cnt), false); try { final long limit = config.getBigFileThreshold(); for (;;) { monitor.update(1); try { if (!sizeQueue.next()) break; } catch (MissingObjectException notFound) { if (ignoreMissingUninteresting) { ObjectToPack otp = sizeQueue.getCurrent(); if (otp != null && otp.isEdge()) { otp.setDoNotDelta(true); continue; } otp = objectsMap.get(notFound.getObjectId()); if (otp != null && otp.isEdge()) { otp.setDoNotDelta(true); continue; } } throw notFound; } ObjectToPack otp = sizeQueue.getCurrent(); if (otp == null) otp = objectsMap.get(sizeQueue.getObjectId()); long sz = sizeQueue.getSize(); if (limit <= sz || Integer.MAX_VALUE <= sz) otp.setDoNotDelta(true); // too big, avoid costly files else if (sz <= DeltaIndex.BLKSZ) otp.setDoNotDelta(true); // too small, won't work else otp.setWeight((int) sz); } } finally { sizeQueue.release(); } monitor.endTask(); stats.timeSearchingForSizes = System.currentTimeMillis() - sizingStart; // Sort the objects by path hash so like files are near each other, // and then by size descending so that bigger files are first. This // applies "Linus' Law" which states that newer files tend to be the // bigger ones, because source files grow and hardly ever shrink. // Arrays.sort(list, 0, cnt, new Comparator() { public int compare(ObjectToPack a, ObjectToPack b) { int cmp = (a.isDoNotDelta() ? 1 : 0) - (b.isDoNotDelta() ? 1 : 0); if (cmp != 0) return cmp; cmp = a.getType() - b.getType(); if (cmp != 0) return cmp; cmp = (a.getPathHash() >>> 1) - (b.getPathHash() >>> 1); if (cmp != 0) return cmp; cmp = (a.getPathHash() & 1) - (b.getPathHash() & 1); if (cmp != 0) return cmp; cmp = (a.isEdge() ? 0 : 1) - (b.isEdge() ? 0 : 1); if (cmp != 0) return cmp; return b.getWeight() - a.getWeight(); } }); // Above we stored the objects we cannot delta onto the end. // Remove them from the list so we don't waste time on them. while (0 < cnt && list[cnt - 1].isDoNotDelta()) { if (!list[cnt - 1].isEdge()) nonEdgeCnt--; cnt--; } if (cnt == 0) return; final long searchStart = System.currentTimeMillis(); monitor.beginTask(JGitText.get().compressingObjects, nonEdgeCnt); searchForDeltas(monitor, list, cnt); monitor.endTask(); stats.deltaSearchNonEdgeObjects = nonEdgeCnt; stats.timeCompressing = System.currentTimeMillis() - searchStart; for (int i = 0; i < cnt; i++) if (!list[i].isEdge() && list[i].isDeltaRepresentation()) stats.deltasFound++; } private int findObjectsNeedingDelta(ObjectToPack[] list, int cnt, int type) { for (ObjectToPack otp : objectsLists[type]) { if (otp.isDoNotDelta()) // delta is disabled for this path continue; if (otp.isDeltaRepresentation()) // already reusing a delta continue; otp.setWeight(0); list[cnt++] = otp; } return cnt; } private void searchForDeltas(final ProgressMonitor monitor, final ObjectToPack[] list, final int cnt) throws MissingObjectException, IncorrectObjectTypeException, LargeObjectException, IOException { int threads = config.getThreads(); if (threads == 0) threads = Runtime.getRuntime().availableProcessors(); if (threads <= 1 || cnt <= 2 * config.getDeltaSearchWindowSize()) { DeltaCache dc = new DeltaCache(config); DeltaWindow dw = new DeltaWindow(config, dc, reader); dw.search(monitor, list, 0, cnt); return; } final DeltaCache dc = new ThreadSafeDeltaCache(config); final ThreadSafeProgressMonitor pm = new ThreadSafeProgressMonitor(monitor); // Guess at the size of batch we want. Because we don't really // have a way for a thread to steal work from another thread if // it ends early, we over partition slightly so the work units // are a bit smaller. // int estSize = cnt / (threads * 2); if (estSize < 2 * config.getDeltaSearchWindowSize()) estSize = 2 * config.getDeltaSearchWindowSize(); final List myTasks = new ArrayList(threads * 2); for (int i = 0; i < cnt;) { final int start = i; final int batchSize; if (cnt - i < estSize) { // If we don't have enough to fill the remaining block, // schedule what is left over as a single block. // batchSize = cnt - i; } else { // Try to split the block at the end of a path. // int end = start + estSize; while (end < cnt) { ObjectToPack a = list[end - 1]; ObjectToPack b = list[end]; if (a.getPathHash() == b.getPathHash()) end++; else break; } batchSize = end - start; } i += batchSize; myTasks.add(new DeltaTask(config, reader, dc, pm, batchSize, start, list)); } pm.startWorkers(myTasks.size()); final Executor executor = config.getExecutor(); final List errors = Collections .synchronizedList(new ArrayList()); if (executor instanceof ExecutorService) { // Caller supplied us a service, use it directly. // runTasks((ExecutorService) executor, pm, myTasks, errors); } else if (executor == null) { // Caller didn't give us a way to run the tasks, spawn up a // temporary thread pool and make sure it tears down cleanly. // ExecutorService pool = Executors.newFixedThreadPool(threads); try { runTasks(pool, pm, myTasks, errors); } finally { pool.shutdown(); for (;;) { try { if (pool.awaitTermination(60, TimeUnit.SECONDS)) break; } catch (InterruptedException e) { throw new IOException( JGitText.get().packingCancelledDuringObjectsWriting); } } } } else { // The caller gave us an executor, but it might not do // asynchronous execution. Wrap everything and hope it // can schedule these for us. // for (final DeltaTask task : myTasks) { executor.execute(new Runnable() { public void run() { try { task.call(); } catch (Throwable failure) { errors.add(failure); } } }); } try { pm.waitForCompletion(); } catch (InterruptedException ie) { // We can't abort the other tasks as we have no handle. // Cross our fingers and just break out anyway. // throw new IOException( JGitText.get().packingCancelledDuringObjectsWriting); } } // If any task threw an error, try to report it back as // though we weren't using a threaded search algorithm. // if (!errors.isEmpty()) { Throwable err = errors.get(0); if (err instanceof Error) throw (Error) err; if (err instanceof RuntimeException) throw (RuntimeException) err; if (err instanceof IOException) throw (IOException) err; IOException fail = new IOException(err.getMessage()); fail.initCause(err); throw fail; } } private void runTasks(ExecutorService pool, ThreadSafeProgressMonitor pm, List tasks, List errors) throws IOException { List> futures = new ArrayList>(tasks.size()); for (DeltaTask task : tasks) futures.add(pool.submit(task)); try { pm.waitForCompletion(); for (Future f : futures) { try { f.get(); } catch (ExecutionException failed) { errors.add(failed.getCause()); } } } catch (InterruptedException ie) { for (Future f : futures) f.cancel(true); throw new IOException( JGitText.get().packingCancelledDuringObjectsWriting); } } private void writeObjects(PackOutputStream out) throws IOException { writeObjects(out, objectsLists[Constants.OBJ_COMMIT]); writeObjects(out, objectsLists[Constants.OBJ_TAG]); writeObjects(out, objectsLists[Constants.OBJ_TREE]); writeObjects(out, objectsLists[Constants.OBJ_BLOB]); } private void writeObjects(PackOutputStream out, List list) throws IOException { if (list.isEmpty()) return; typeStats = stats.objectTypes[list.get(0).getType()]; long beginOffset = out.length(); if (reuseSupport != null) { reuseSupport.writeObjects(out, list); } else { for (ObjectToPack otp : list) out.writeObject(otp); } typeStats.bytes += out.length() - beginOffset; typeStats.cntObjects = list.size(); } void writeObject(PackOutputStream out, ObjectToPack otp) throws IOException { if (!otp.isWritten()) writeObjectImpl(out, otp); } private void writeObjectImpl(PackOutputStream out, ObjectToPack otp) throws IOException { if (otp.wantWrite()) { // A cycle exists in this delta chain. This should only occur if a // selected object representation disappeared during writing // (for example due to a concurrent repack) and a different base // was chosen, forcing a cycle. Select something other than a // delta, and write this object. // reuseDeltas = false; otp.clearDeltaBase(); otp.clearReuseAsIs(); reuseSupport.selectObjectRepresentation(this, NullProgressMonitor.INSTANCE, Collections.singleton(otp)); } otp.markWantWrite(); while (otp.isReuseAsIs()) { writeBase(out, otp.getDeltaBase()); if (otp.isWritten()) return; // Delta chain cycle caused this to write already. out.resetCRC32(); otp.setOffset(out.length()); try { reuseSupport.copyObjectAsIs(out, otp, reuseValidate); out.endObject(); otp.setCRC(out.getCRC32()); typeStats.reusedObjects++; if (otp.isDeltaRepresentation()) { typeStats.reusedDeltas++; typeStats.deltaBytes += out.length() - otp.getOffset(); } return; } catch (StoredObjectRepresentationNotAvailableException gone) { if (otp.getOffset() == out.length()) { otp.setOffset(0); otp.clearDeltaBase(); otp.clearReuseAsIs(); reuseSupport.selectObjectRepresentation(this, NullProgressMonitor.INSTANCE, Collections.singleton(otp)); continue; } else { // Object writing already started, we cannot recover. // CorruptObjectException coe; coe = new CorruptObjectException(otp, ""); coe.initCause(gone); throw coe; } } } // If we reached here, reuse wasn't possible. // if (otp.isDeltaRepresentation()) writeDeltaObjectDeflate(out, otp); else writeWholeObjectDeflate(out, otp); out.endObject(); otp.setCRC(out.getCRC32()); } private void writeBase(PackOutputStream out, ObjectToPack baseInPack) throws IOException { if (baseInPack != null && !baseInPack.isWritten()) writeObjectImpl(out, baseInPack); } private void writeWholeObjectDeflate(PackOutputStream out, final ObjectToPack otp) throws IOException { final Deflater deflater = deflater(); final ObjectLoader ldr = reader.open(otp, otp.getType()); out.resetCRC32(); otp.setOffset(out.length()); out.writeHeader(otp, ldr.getSize()); deflater.reset(); DeflaterOutputStream dst = new DeflaterOutputStream(out, deflater); ldr.copyTo(dst); dst.finish(); } private void writeDeltaObjectDeflate(PackOutputStream out, final ObjectToPack otp) throws IOException { writeBase(out, otp.getDeltaBase()); out.resetCRC32(); otp.setOffset(out.length()); DeltaCache.Ref ref = otp.popCachedDelta(); if (ref != null) { byte[] zbuf = ref.get(); if (zbuf != null) { out.writeHeader(otp, otp.getCachedSize()); out.write(zbuf); return; } } TemporaryBuffer.Heap delta = delta(otp); out.writeHeader(otp, delta.length()); Deflater deflater = deflater(); deflater.reset(); DeflaterOutputStream dst = new DeflaterOutputStream(out, deflater); delta.writeTo(dst, null); dst.finish(); typeStats.cntDeltas++; typeStats.deltaBytes += out.length() - otp.getOffset(); } private TemporaryBuffer.Heap delta(final ObjectToPack otp) throws IOException { DeltaIndex index = new DeltaIndex(buffer(otp.getDeltaBaseId())); byte[] res = buffer(otp); // We never would have proposed this pair if the delta would be // larger than the unpacked version of the object. So using it // as our buffer limit is valid: we will never reach it. // TemporaryBuffer.Heap delta = new TemporaryBuffer.Heap(res.length); index.encode(delta, res); return delta; } private byte[] buffer(AnyObjectId objId) throws IOException { return buffer(config, reader, objId); } static byte[] buffer(PackConfig config, ObjectReader or, AnyObjectId objId) throws IOException { // PackWriter should have already pruned objects that // are above the big file threshold, so our chances of // the object being below it are very good. We really // shouldn't be here, unless the implementation is odd. return or.open(objId).getCachedBytes(config.getBigFileThreshold()); } private Deflater deflater() { if (myDeflater == null) myDeflater = new Deflater(config.getCompressionLevel()); return myDeflater; } private void writeChecksum(PackOutputStream out) throws IOException { packcsum = out.getDigest(); out.write(packcsum); } private void findObjectsToPack(final ProgressMonitor countingMonitor, final ObjectWalk walker, final Set want, Set have) throws MissingObjectException, IOException, IncorrectObjectTypeException { final long countingStart = System.currentTimeMillis(); countingMonitor.beginTask(JGitText.get().countingObjects, ProgressMonitor.UNKNOWN); if (have == null) have = Collections.emptySet(); stats.interestingObjects = Collections.unmodifiableSet(new HashSet(want)); stats.uninterestingObjects = Collections.unmodifiableSet(new HashSet(have)); List all = new ArrayList(want.size() + have.size()); all.addAll(want); all.addAll(have); final Map tipToPack = new HashMap(); final RevFlag inCachedPack = walker.newFlag("inCachedPack"); final RevFlag include = walker.newFlag("include"); final RevFlag added = walker.newFlag("added"); final RevFlagSet keepOnRestart = new RevFlagSet(); keepOnRestart.add(inCachedPack); walker.setRetainBody(false); walker.carry(include); int haveEst = have.size(); if (have.isEmpty()) { walker.sort(RevSort.COMMIT_TIME_DESC); if (useCachedPacks && reuseSupport != null) { Set need = new HashSet(want); List shortCircuit = new LinkedList(); for (CachedPack pack : reuseSupport.getCachedPacks()) { if (need.containsAll(pack.getTips())) { need.removeAll(pack.getTips()); shortCircuit.add(pack); } for (ObjectId id : pack.getTips()) { tipToPack.put(id, pack); all.add(id); } } if (need.isEmpty() && !shortCircuit.isEmpty()) { cachedPacks.addAll(shortCircuit); for (CachedPack pack : shortCircuit) countingMonitor.update((int) pack.getObjectCount()); countingMonitor.endTask(); stats.timeCounting = System.currentTimeMillis() - countingStart; return; } haveEst += tipToPack.size(); } } else { walker.sort(RevSort.TOPO); if (thin) walker.sort(RevSort.BOUNDARY, true); } List wantObjs = new ArrayList(want.size()); List haveObjs = new ArrayList(haveEst); List wantTags = new ArrayList(want.size()); AsyncRevObjectQueue q = walker.parseAny(all, true); try { for (;;) { try { RevObject o = q.next(); if (o == null) break; if (tipToPack.containsKey(o)) o.add(inCachedPack); if (have.contains(o)) { haveObjs.add(o); } else if (want.contains(o)) { o.add(include); wantObjs.add(o); if (o instanceof RevTag) wantTags.add((RevTag) o); } } catch (MissingObjectException e) { if (ignoreMissingUninteresting && have.contains(e.getObjectId())) continue; throw e; } } } finally { q.release(); } if (!wantTags.isEmpty()) { all = new ArrayList(wantTags.size()); for (RevTag tag : wantTags) all.add(tag.getObject()); q = walker.parseAny(all, true); try { while (q.next() != null) { // Just need to pop the queue item to parse the object. } } finally { q.release(); } } for (RevObject obj : wantObjs) walker.markStart(obj); for (RevObject obj : haveObjs) walker.markUninteresting(obj); final int maxBases = config.getDeltaSearchWindowSize(); Set baseTrees = new HashSet(); BlockList commits = new BlockList(); RevCommit c; while ((c = walker.next()) != null) { if (c.has(inCachedPack)) { CachedPack pack = tipToPack.get(c); if (includesAllTips(pack, include, walker)) { useCachedPack(walker, keepOnRestart, // wantObjs, haveObjs, pack); commits = new BlockList(); countingMonitor.endTask(); countingMonitor.beginTask(JGitText.get().countingObjects, ProgressMonitor.UNKNOWN); continue; } } if (c.has(RevFlag.UNINTERESTING)) { if (baseTrees.size() <= maxBases) baseTrees.add(c.getTree()); continue; } commits.add(c); countingMonitor.update(1); } int commitCnt = 0; boolean putTagTargets = false; for (RevCommit cmit : commits) { if (!cmit.has(added)) { cmit.add(added); addObject(cmit, 0); commitCnt++; } for (int i = 0; i < cmit.getParentCount(); i++) { RevCommit p = cmit.getParent(i); if (!p.has(added) && !p.has(RevFlag.UNINTERESTING)) { p.add(added); addObject(p, 0); commitCnt++; } } if (!putTagTargets && 4096 < commitCnt) { for (ObjectId id : tagTargets) { RevObject obj = walker.lookupOrNull(id); if (obj instanceof RevCommit && obj.has(include) && !obj.has(RevFlag.UNINTERESTING) && !obj.has(added)) { obj.add(added); addObject(obj, 0); } } putTagTargets = true; } } commits = null; if (thin && !baseTrees.isEmpty()) { BaseSearch bases = new BaseSearch(countingMonitor, baseTrees, // objectsMap, edgeObjects, reader); RevObject o; while ((o = walker.nextObject()) != null) { if (o.has(RevFlag.UNINTERESTING)) continue; int pathHash = walker.getPathHashCode(); byte[] pathBuf = walker.getPathBuffer(); int pathLen = walker.getPathLength(); bases.addBase(o.getType(), pathBuf, pathLen, pathHash); addObject(o, pathHash); countingMonitor.update(1); } } else { RevObject o; while ((o = walker.nextObject()) != null) { if (o.has(RevFlag.UNINTERESTING)) continue; addObject(o, walker.getPathHashCode()); countingMonitor.update(1); } } for (CachedPack pack : cachedPacks) countingMonitor.update((int) pack.getObjectCount()); countingMonitor.endTask(); stats.timeCounting = System.currentTimeMillis() - countingStart; } private static void pruneEdgesFromObjectList(List list) { final int size = list.size(); int src = 0; int dst = 0; for (; src < size; src++) { ObjectToPack obj = list.get(src); if (obj.isEdge()) continue; if (dst != src) list.set(dst, obj); dst++; } while (dst < list.size()) list.remove(list.size() - 1); } private void useCachedPack(ObjectWalk walker, RevFlagSet keepOnRestart, List wantObj, List baseObj, CachedPack pack) throws MissingObjectException, IncorrectObjectTypeException, IOException { cachedPacks.add(pack); for (ObjectId id : pack.getTips()) baseObj.add(walker.lookupOrNull(id)); setThin(true); walker.resetRetain(keepOnRestart); walker.sort(RevSort.TOPO); walker.sort(RevSort.BOUNDARY, true); for (RevObject id : wantObj) walker.markStart(id); for (RevObject id : baseObj) walker.markUninteresting(id); } private static boolean includesAllTips(CachedPack pack, RevFlag include, ObjectWalk walker) { for (ObjectId id : pack.getTips()) { if (!walker.lookupOrNull(id).has(include)) return false; } return true; } /** * Include one object to the output file. *

* Objects are written in the order they are added. If the same object is * added twice, it may be written twice, creating a larger than necessary * file. * * @param object * the object to add. * @throws IncorrectObjectTypeException * the object is an unsupported type. */ public void addObject(final RevObject object) throws IncorrectObjectTypeException { addObject(object, 0); } private void addObject(final RevObject object, final int pathHashCode) { final ObjectToPack otp; if (reuseSupport != null) otp = reuseSupport.newObjectToPack(object); else otp = new ObjectToPack(object); otp.setPathHash(pathHashCode); objectsLists[object.getType()].add(otp); objectsMap.add(otp); } /** * Select an object representation for this writer. *

* An {@link ObjectReader} implementation should invoke this method once for * each representation available for an object, to allow the writer to find * the most suitable one for the output. * * @param otp * the object being packed. * @param next * the next available representation from the repository. */ public void select(ObjectToPack otp, StoredObjectRepresentation next) { int nFmt = next.getFormat(); if (!cachedPacks.isEmpty()) { if (otp.isEdge()) return; if ((nFmt == PACK_WHOLE) | (nFmt == PACK_DELTA)) { for (CachedPack pack : cachedPacks) { if (pack.hasObject(otp, next)) { otp.setEdge(); otp.clearDeltaBase(); otp.clearReuseAsIs(); pruneCurrentObjectList = true; return; } } } } if (nFmt == PACK_DELTA && reuseDeltas && reuseDeltaFor(otp)) { ObjectId baseId = next.getDeltaBase(); ObjectToPack ptr = objectsMap.get(baseId); if (ptr != null && !ptr.isEdge()) { otp.setDeltaBase(ptr); otp.setReuseAsIs(); } else if (thin && ptr != null && ptr.isEdge()) { otp.setDeltaBase(baseId); otp.setReuseAsIs(); } else { otp.clearDeltaBase(); otp.clearReuseAsIs(); } } else if (nFmt == PACK_WHOLE && config.isReuseObjects()) { int nWeight = next.getWeight(); if (otp.isReuseAsIs() && !otp.isDeltaRepresentation()) { // We've chosen another PACK_WHOLE format for this object, // choose the one that has the smaller compressed size. // if (otp.getWeight() <= nWeight) return; } otp.clearDeltaBase(); otp.setReuseAsIs(); otp.setWeight(nWeight); } else { otp.clearDeltaBase(); otp.clearReuseAsIs(); } otp.select(next); } private boolean reuseDeltaFor(ObjectToPack otp) { switch (otp.getType()) { case Constants.OBJ_COMMIT: return reuseDeltaCommits; case Constants.OBJ_TREE: return true; case Constants.OBJ_BLOB: return true; case Constants.OBJ_TAG: return false; default: return true; } } /** Summary of how PackWriter created the pack. */ public static class Statistics { /** Statistics about a single class of object. */ public static class ObjectType { long cntObjects; long cntDeltas; long reusedObjects; long reusedDeltas; long bytes; long deltaBytes; /** * @return total number of objects output. This total includes the * value of {@link #getDeltas()}. */ public long getObjects() { return cntObjects; } /** * @return total number of deltas output. This may be lower than the * actual number of deltas if a cached pack was reused. */ public long getDeltas() { return cntDeltas; } /** * @return number of objects whose existing representation was * reused in the output. This count includes * {@link #getReusedDeltas()}. */ public long getReusedObjects() { return reusedObjects; } /** * @return number of deltas whose existing representation was reused * in the output, as their base object was also output or * was assumed present for a thin pack. This may be lower * than the actual number of reused deltas if a cached pack * was reused. */ public long getReusedDeltas() { return reusedDeltas; } /** * @return total number of bytes written. This size includes the * object headers as well as the compressed data. This size * also includes all of {@link #getDeltaBytes()}. */ public long getBytes() { return bytes; } /** * @return number of delta bytes written. This size includes the * object headers for the delta objects. */ public long getDeltaBytes() { return deltaBytes; } } Set interestingObjects; Set uninterestingObjects; Collection reusedPacks; int deltaSearchNonEdgeObjects; int deltasFound; long totalObjects; long totalDeltas; long reusedObjects; long reusedDeltas; long totalBytes; long thinPackBytes; long timeCounting; long timeSearchingForReuse; long timeSearchingForSizes; long timeCompressing; long timeWriting; ObjectType[] objectTypes; { objectTypes = new ObjectType[5]; objectTypes[Constants.OBJ_COMMIT] = new ObjectType(); objectTypes[Constants.OBJ_TREE] = new ObjectType(); objectTypes[Constants.OBJ_BLOB] = new ObjectType(); objectTypes[Constants.OBJ_TAG] = new ObjectType(); } /** * @return unmodifiable collection of objects to be included in the * pack. May be null if the pack was hand-crafted in a unit * test. */ public Set getInterestingObjects() { return interestingObjects; } /** * @return unmodifiable collection of objects that should be excluded * from the pack, as the peer that will receive the pack already * has these objects. */ public Set getUninterestingObjects() { return uninterestingObjects; } /** * @return unmodifiable collection of the cached packs that were reused * in the output, if any were selected for reuse. */ public Collection getReusedPacks() { return reusedPacks; } /** * @return number of objects in the output pack that went through the * delta search process in order to find a potential delta base. */ public int getDeltaSearchNonEdgeObjects() { return deltaSearchNonEdgeObjects; } /** * @return number of objects in the output pack that went through delta * base search and found a suitable base. This is a subset of * {@link #getDeltaSearchNonEdgeObjects()}. */ public int getDeltasFound() { return deltasFound; } /** * @return total number of objects output. This total includes the value * of {@link #getTotalDeltas()}. */ public long getTotalObjects() { return totalObjects; } /** * @return total number of deltas output. This may be lower than the * actual number of deltas if a cached pack was reused. */ public long getTotalDeltas() { return totalDeltas; } /** * @return number of objects whose existing representation was reused in * the output. This count includes {@link #getReusedDeltas()}. */ public long getReusedObjects() { return reusedObjects; } /** * @return number of deltas whose existing representation was reused in * the output, as their base object was also output or was * assumed present for a thin pack. This may be lower than the * actual number of reused deltas if a cached pack was reused. */ public long getReusedDeltas() { return reusedDeltas; } /** * @return total number of bytes written. This size includes the pack * header, trailer, thin pack, and reused cached pack(s). */ public long getTotalBytes() { return totalBytes; } /** * @return size of the thin pack in bytes, if a thin pack was generated. * A thin pack is created when the client already has objects * and some deltas are created against those objects, or if a * cached pack is being used and some deltas will reference * objects in the cached pack. This size does not include the * pack header or trailer. */ public long getThinPackBytes() { return thinPackBytes; } /** * @param typeCode * object type code, e.g. OBJ_COMMIT or OBJ_TREE. * @return information about this type of object in the pack. */ public ObjectType byObjectType(int typeCode) { return objectTypes[typeCode]; } /** * @return time in milliseconds spent enumerating the objects that need * to be included in the output. This time includes any restarts * that occur when a cached pack is selected for reuse. */ public long getTimeCounting() { return timeCounting; } /** * @return time in milliseconds spent matching existing representations * against objects that will be transmitted, or that the client * can be assumed to already have. */ public long getTimeSearchingForReuse() { return timeSearchingForReuse; } /** * @return time in milliseconds spent finding the sizes of all objects * that will enter the delta compression search window. The * sizes need to be known to better match similar objects * together and improve delta compression ratios. */ public long getTimeSearchingForSizes() { return timeSearchingForSizes; } /** * @return time in milliseconds spent on delta compression. This is * observed wall-clock time and does not accurately track CPU * time used when multiple threads were used to perform the * delta compression. */ public long getTimeCompressing() { return timeCompressing; } /** * @return time in milliseconds spent writing the pack output, from * start of header until end of trailer. The transfer speed can * be approximated by dividing {@link #getTotalBytes()} by this * value. */ public long getTimeWriting() { return timeWriting; } /** * @return get the average output speed in terms of bytes-per-second. * {@code getTotalBytes() / (getTimeWriting() / 1000.0)}. */ public double getTransferRate() { return getTotalBytes() / (getTimeWriting() / 1000.0); } /** @return formatted message string for display to clients. */ public String getMessage() { return MessageFormat.format(JGitText.get().packWriterStatistics, // totalObjects, totalDeltas, // reusedObjects, reusedDeltas); } } }