org.eclipse.jgit.internal.storage.pack.PackWriter.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jgit.internal.storage.pack.PackWriter.java

Source

/*
 * Copyright (C) 2008-2010, Google Inc.
 * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
 * 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.internal.storage.pack;

import static java.util.Objects.requireNonNull;
import static org.eclipse.jgit.internal.storage.pack.StoredObjectRepresentation.PACK_DELTA;
import static org.eclipse.jgit.internal.storage.pack.StoredObjectRepresentation.PACK_WHOLE;
import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
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.IOException;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
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.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
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.CRC32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.annotations.Nullable;
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.internal.JGitText;
import org.eclipse.jgit.internal.storage.file.PackBitmapIndexBuilder;
import org.eclipse.jgit.internal.storage.file.PackBitmapIndexWriterV1;
import org.eclipse.jgit.internal.storage.file.PackIndexWriter;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.AsyncObjectSizeQueue;
import org.eclipse.jgit.lib.BatchingProgressMonitor;
import org.eclipse.jgit.lib.BitmapIndex;
import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder;
import org.eclipse.jgit.lib.BitmapObject;
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.ObjectIdSet;
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.BitmapWalker;
import org.eclipse.jgit.revwalk.DepthWalk;
import org.eclipse.jgit.revwalk.ObjectWalk;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
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.pack.PackConfig;
import org.eclipse.jgit.storage.pack.PackStatistics;
import org.eclipse.jgit.transport.FilterSpec;
import org.eclipse.jgit.transport.ObjectCountCallback;
import org.eclipse.jgit.transport.PacketLineOut;
import org.eclipse.jgit.transport.WriteAbortedException;
import org.eclipse.jgit.util.BlockList;
import org.eclipse.jgit.util.TemporaryBuffer;

/**
 * <p>
 * PackWriter class is responsible for generating pack files from specified set
 * of objects from repository. This implementation produce pack files in format
 * version 2.
 * </p>
 * <p>
 * Source of objects may be specified in two ways:
 * <ul>
 * <li>(usually) by providing sets of interesting and uninteresting objects in
 * repository - all interesting objects and their ancestors except uninteresting
 * objects and their ancestors will be included in pack, or</li>
 * <li>by providing iterator of {@link org.eclipse.jgit.revwalk.RevObject}
 * specifying exact list and order of objects in pack</li>
 * </ul>
 * <p>
 * Typical usage consists of creating an instance, configuring options,
 * preparing the list of objects by calling {@link #preparePack(Iterator)} or
 * {@link #preparePack(ProgressMonitor, Set, Set)}, and streaming with
 * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}. If the
 * pack is being stored as a file the matching index can be written out after
 * writing the pack by {@link #writeIndex(OutputStream)}. An optional bitmap
 * index can be made by calling {@link #prepareBitmapIndex(ProgressMonitor)}
 * followed by {@link #writeBitmapIndex(OutputStream)}.
 * </p>
 * <p>
 * Class provide set of configurable options and
 * {@link org.eclipse.jgit.lib.ProgressMonitor} support, as operations may take
 * a long time for big repositories. Deltas searching algorithm is <b>NOT
 * IMPLEMENTED</b> yet - this implementation relies only on deltas and objects
 * reuse.
 * </p>
 * <p>
 * This class is not thread safe. It is intended to be used in one thread as a
 * single pass to produce one pack. Invoking methods multiple times or out of
 * order is not supported as internal data structures are destroyed during
 * certain phases to save memory when packing large repositories.
 * </p>
 */
public class PackWriter implements AutoCloseable {
    private static final int PACK_VERSION_GENERATED = 2;

    /** Empty set of objects for {@code preparePack()}. */
    public static final Set<ObjectId> NONE = Collections.emptySet();

    private static final Map<WeakReference<PackWriter>, Boolean> instances = new ConcurrentHashMap<>();

    private static final Iterable<PackWriter> instancesIterable = new Iterable<PackWriter>() {
        @Override
        public Iterator<PackWriter> iterator() {
            return new Iterator<PackWriter>() {
                private final Iterator<WeakReference<PackWriter>> it = instances.keySet().iterator();
                private PackWriter next;

                @Override
                public boolean hasNext() {
                    if (next != null)
                        return true;
                    while (it.hasNext()) {
                        WeakReference<PackWriter> ref = it.next();
                        next = ref.get();
                        if (next != null)
                            return true;
                        it.remove();
                    }
                    return false;
                }

                @Override
                public PackWriter next() {
                    if (hasNext()) {
                        PackWriter result = next;
                        next = null;
                        return result;
                    }
                    throw new NoSuchElementException();
                }

                @Override
                public void remove() {
                    throw new UnsupportedOperationException();
                }
            };
        }
    };

    /**
     * Get all allocated, non-released PackWriters instances.
     *
     * @return all allocated, non-released PackWriters instances.
     */
    public static Iterable<PackWriter> getInstances() {
        return instancesIterable;
    }

    @SuppressWarnings("unchecked")
    BlockList<ObjectToPack> objectsLists[] = new BlockList[OBJ_TAG + 1];
    {
        objectsLists[OBJ_COMMIT] = new BlockList<>();
        objectsLists[OBJ_TREE] = new BlockList<>();
        objectsLists[OBJ_BLOB] = new BlockList<>();
        objectsLists[OBJ_TAG] = new BlockList<>();
    }

    private ObjectIdOwnerMap<ObjectToPack> objectsMap = new ObjectIdOwnerMap<>();

    // edge objects for thin packs
    private List<ObjectToPack> edgeObjects = new BlockList<>();

    // Objects the client is known to have already.
    private BitmapBuilder haveObjects;

    private List<CachedPack> cachedPacks = new ArrayList<>(2);

    private Set<ObjectId> tagTargets = NONE;

    private Set<? extends ObjectId> excludeFromBitmapSelection = NONE;

    private ObjectIdSet[] excludeInPacks;

    private ObjectIdSet excludeInPackLast;

    private Deflater myDeflater;

    private final ObjectReader reader;

    /** {@link #reader} recast to the reuse interface, if it supports it. */
    private final ObjectReuseAsIs reuseSupport;

    final PackConfig config;

    private final PackStatistics.Accumulator stats;

    private final MutableState state;

    private final WeakReference<PackWriter> selfRef;

    private PackStatistics.ObjectType.Accumulator typeStats;

    private List<ObjectToPack> sortedByName;

    private byte packcsum[];

    private boolean deltaBaseAsOffset;

    private boolean reuseDeltas;

    private boolean reuseDeltaCommits;

    private boolean reuseValidate;

    private boolean thin;

    private boolean useCachedPacks;

    private boolean useBitmaps;

    private boolean ignoreMissingUninteresting = true;

    private boolean pruneCurrentObjectList;

    private boolean shallowPack;

    private boolean canBuildBitmaps;

    private boolean indexDisabled;

    private int depth;

    private Collection<? extends ObjectId> unshallowObjects;

    private PackBitmapIndexBuilder writeBitmaps;

    private CRC32 crc32;

    private ObjectCountCallback callback;

    private FilterSpec filterSpec = FilterSpec.NO_FILTER;

    private PackfileUriConfig packfileUriConfig;

    /**
     * Create writer for specified repository.
     * <p>
     * Objects for packing are specified in {@link #preparePack(Iterator)} or
     * {@link #preparePack(ProgressMonitor, Set, Set)}.
     *
     * @param repo
     *            repository where objects are stored.
     */
    public PackWriter(Repository repo) {
        this(repo, repo.newObjectReader());
    }

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

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

    /**
     * Create writer with a specified configuration.
     * <p>
     * Objects for packing are specified in {@link #preparePack(Iterator)} or
     * {@link #preparePack(ProgressMonitor, Set, Set)}.
     *
     * @param config
     *            configuration for the pack writer.
     * @param reader
     *            reader to read from the repository with.
     */
    public PackWriter(PackConfig config, ObjectReader reader) {
        this(config, reader, null);
    }

    /**
     * Create writer with a specified configuration.
     * <p>
     * Objects for packing are specified in {@link #preparePack(Iterator)} or
     * {@link #preparePack(ProgressMonitor, Set, Set)}.
     *
     * @param config
     *            configuration for the pack writer.
     * @param reader
     *            reader to read from the repository with.
     * @param statsAccumulator
     *            accumulator for statics
     */
    public PackWriter(PackConfig config, final ObjectReader reader,
            @Nullable PackStatistics.Accumulator statsAccumulator) {
        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 = statsAccumulator != null ? statsAccumulator : new PackStatistics.Accumulator();
        state = new MutableState();
        selfRef = new WeakReference<>(this);
        instances.put(selfRef, Boolean.TRUE);
    }

    /**
     * Set the {@code ObjectCountCallback}.
     * <p>
     * It should be set before calling
     * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}.
     *
     * @param callback
     *            the callback to set
     * @return this object for chaining.
     */
    public PackWriter setObjectCountCallback(ObjectCountCallback callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Records the set of shallow commits in the client.
     *
     * @param clientShallowCommits
     *            the shallow commits in the client
     */
    public void setClientShallowCommits(Set<ObjectId> clientShallowCommits) {
        stats.clientShallowCommits = Collections.unmodifiableSet(new HashSet<>(clientShallowCommits));
    }

    /**
     * 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;
    }

    /**
     * Whether this writer is producing a thin pack.
     *
     * @return true if this writer is producing a thin pack.
     */
    public boolean isThin() {
        return thin;
    }

    /**
     * Whether writer may pack objects with delta base object not within set of
     * objects to pack
     *
     * @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(boolean packthin) {
        thin = packthin;
    }

    /**
     * Whether to reuse cached packs.
     *
     * @return {@code true} to reuse cached packs. If true index creation isn't
     *         available.
     */
    public boolean isUseCachedPacks() {
        return useCachedPacks;
    }

    /**
     * Whether to use cached packs
     *
     * @param useCached
     *            if set to {@code 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;
    }

    /**
     * Whether to use bitmaps
     *
     * @return {@code true} to use bitmaps for ObjectWalks, if available.
     */
    public boolean isUseBitmaps() {
        return useBitmaps;
    }

    /**
     * Whether to use bitmaps
     *
     * @param useBitmaps
     *            if set to true, bitmaps will be used when preparing a pack.
     */
    public void setUseBitmaps(boolean useBitmaps) {
        this.useBitmaps = useBitmaps;
    }

    /**
     * Whether the index file cannot be created by this PackWriter.
     *
     * @return {@code true} if the index file cannot be created by this
     *         PackWriter.
     */
    public boolean isIndexDisabled() {
        return indexDisabled || !cachedPacks.isEmpty();
    }

    /**
     * Whether to disable creation of the index file.
     *
     * @param noIndex
     *            {@code true} to disable creation of the index file.
     */
    public void setIndexDisabled(boolean noIndex) {
        this.indexDisabled = noIndex;
    }

    /**
     * Whether to ignore missing uninteresting objects
     *
     * @return {@code true} to ignore objects that are uninteresting and also
     *         not found on local disk; false to throw a
     *         {@link org.eclipse.jgit.errors.MissingObjectException} out of
     *         {@link #preparePack(ProgressMonitor, Set, Set)} if an
     *         uninteresting object is not in the source repository. By default,
     *         true, permitting gracefully ignoring of uninteresting objects.
     */
    public boolean isIgnoreMissingUninteresting() {
        return ignoreMissingUninteresting;
    }

    /**
     * Whether writer should ignore non existing uninteresting objects
     *
     * @param ignore
     *            {@code 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 org.eclipse.jgit.errors.MissingObjectException}
     */
    public void setIgnoreMissingUninteresting(boolean ignore) {
        ignoreMissingUninteresting = ignore;
    }

    /**
     * Set the tag targets that should be hoisted earlier during packing.
     * <p>
     * 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<ObjectId> objects) {
        tagTargets = objects;
    }

    /**
     * Configure this pack for a shallow clone.
     *
     * @param depth
     *            maximum depth of history to return. 1 means return only the
     *            "wants".
     * @param unshallow
     *            objects which used to be shallow on the client, but are being
     *            extended as part of this fetch
     */
    public void setShallowPack(int depth, Collection<? extends ObjectId> unshallow) {
        this.shallowPack = true;
        this.depth = depth;
        this.unshallowObjects = unshallow;
    }

    /**
     * @param filter the filter which indicates what and what not this writer
     *            should include
     */
    public void setFilterSpec(@NonNull FilterSpec filter) {
        filterSpec = requireNonNull(filter);
    }

    /**
     * @param config configuration related to packfile URIs
     * @since 5.5
     */
    public void setPackfileUriConfig(PackfileUriConfig config) {
        packfileUriConfig = config;
    }

    /**
     * Returns objects number in a pack file that was created by this writer.
     *
     * @return number of objects in pack.
     * @throws java.io.IOException
     *             a cached pack cannot supply its object count.
     */
    public long getObjectCount() throws IOException {
        if (stats.totalObjects == 0) {
            long objCnt = 0;

            objCnt += objectsLists[OBJ_COMMIT].size();
            objCnt += objectsLists[OBJ_TREE].size();
            objCnt += objectsLists[OBJ_BLOB].size();
            objCnt += objectsLists[OBJ_TAG].size();

            for (CachedPack pack : cachedPacks)
                objCnt += pack.getObjectCount();
            return objCnt;
        }
        return stats.totalObjects;
    }

    private long getUnoffloadedObjectCount() throws IOException {
        long objCnt = 0;

        objCnt += objectsLists[OBJ_COMMIT].size();
        objCnt += objectsLists[OBJ_TREE].size();
        objCnt += objectsLists[OBJ_BLOB].size();
        objCnt += objectsLists[OBJ_TAG].size();

        for (CachedPack pack : cachedPacks) {
            CachedPackUriProvider.PackInfo packInfo = packfileUriConfig.cachedPackUriProvider.getInfo(pack,
                    packfileUriConfig.protocolsSupported);
            if (packInfo == null) {
                objCnt += pack.getObjectCount();
            }
        }

        return objCnt;
    }

    /**
     * Returns the object ids in the pack file that was created by this writer.
     * <p>
     * This method can only be invoked after
     * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)} has
     * been invoked and completed successfully.
     *
     * @return set of objects in pack.
     * @throws java.io.IOException
     *             a cached pack cannot supply its object ids.
     */
    public ObjectIdOwnerMap<ObjectIdOwnerMap.Entry> getObjectSet() throws IOException {
        if (!cachedPacks.isEmpty())
            throw new IOException(JGitText.get().cachedPacksPreventsListingObjects);

        if (writeBitmaps != null) {
            return writeBitmaps.getObjectSet();
        }

        ObjectIdOwnerMap<ObjectIdOwnerMap.Entry> r = new ObjectIdOwnerMap<>();
        for (BlockList<ObjectToPack> objList : objectsLists) {
            if (objList != null) {
                for (ObjectToPack otp : objList)
                    r.add(new ObjectIdOwnerMap.Entry(otp) {
                        // A new entry that copies the ObjectId
                    });
            }
        }
        return r;
    }

    /**
     * Add a pack index whose contents should be excluded from the result.
     *
     * @param idx
     *            objects in this index will not be in the output pack.
     */
    public void excludeObjects(ObjectIdSet idx) {
        if (excludeInPacks == null) {
            excludeInPacks = new ObjectIdSet[] { idx };
            excludeInPackLast = idx;
        } else {
            int cnt = excludeInPacks.length;
            ObjectIdSet[] newList = new ObjectIdSet[cnt + 1];
            System.arraycopy(excludeInPacks, 0, newList, 0, cnt);
            newList[cnt] = idx;
            excludeInPacks = newList;
        }
    }

    /**
     * Prepare the list of objects to be written to the pack stream.
     * <p>
     * Iterator <b>exactly</b> 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.
     * </p>
     *
     * @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 org.eclipse.jgit.lib.Constants#OBJ_COMMIT},
     *            {@link org.eclipse.jgit.lib.Constants#OBJ_TREE},
     *            {@link org.eclipse.jgit.lib.Constants#OBJ_BLOB} and
     *            {@link org.eclipse.jgit.lib.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 java.io.IOException
     *             when some I/O problem occur during reading objects.
     */
    public void preparePack(@NonNull Iterator<RevObject> objectsSource) throws IOException {
        while (objectsSource.hasNext()) {
            addObject(objectsSource.next());
        }
    }

    /**
     * Prepare the list of objects to be written to the pack stream.
     * <p>
     * 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 org.eclipse.jgit.revwalk.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.
     * </p>
     *
     * @param countingMonitor
     *            progress during object enumeration.
     * @param want
     *            collection of objects to be marked as interesting (start
     *            points of graph traversal). Must not be {@code null}.
     * @param have
     *            collection of objects to be marked as uninteresting (end
     *            points of graph traversal). Pass {@link #NONE} if all objects
     *            reachable from {@code want} are desired, such as when serving
     *            a clone.
     * @throws java.io.IOException
     *             when some I/O problem occur during reading objects.
     */
    public void preparePack(ProgressMonitor countingMonitor, @NonNull Set<? extends ObjectId> want,
            @NonNull Set<? extends ObjectId> have) throws IOException {
        preparePack(countingMonitor, want, have, NONE, NONE);
    }

    /**
     * Prepare the list of objects to be written to the pack stream.
     * <p>
     * Like {@link #preparePack(ProgressMonitor, Set, Set)} but also allows
     * specifying commits that should not be walked past ("shallow" commits).
     * The caller is responsible for filtering out commits that should not be
     * shallow any more ("unshallow" commits as in {@link #setShallowPack}) from
     * the shallow set.
     *
     * @param countingMonitor
     *            progress during object enumeration.
     * @param want
     *            objects of interest, ancestors of which will be included in
     *            the pack. Must not be {@code null}.
     * @param have
     *            objects whose ancestors (up to and including {@code shallow}
     *            commits) do not need to be included in the pack because they
     *            are already available from elsewhere. Must not be
     *            {@code null}.
     * @param shallow
     *            commits indicating the boundary of the history marked with
     *            {@code have}. Shallow commits have parents but those parents
     *            are considered not to be already available. Parents of
     *            {@code shallow} commits and earlier generations will be
     *            included in the pack if requested by {@code want}. Must not be
     *            {@code null}.
     * @throws java.io.IOException
     *             an I/O problem occurred while reading objects.
     */
    public void preparePack(ProgressMonitor countingMonitor, @NonNull Set<? extends ObjectId> want,
            @NonNull Set<? extends ObjectId> have, @NonNull Set<? extends ObjectId> shallow) throws IOException {
        preparePack(countingMonitor, want, have, shallow, NONE);
    }

    /**
     * Prepare the list of objects to be written to the pack stream.
     * <p>
     * Like {@link #preparePack(ProgressMonitor, Set, Set)} but also allows
     * specifying commits that should not be walked past ("shallow" commits).
     * The caller is responsible for filtering out commits that should not be
     * shallow any more ("unshallow" commits as in {@link #setShallowPack}) from
     * the shallow set.
     *
     * @param countingMonitor
     *            progress during object enumeration.
     * @param want
     *            objects of interest, ancestors of which will be included in
     *            the pack. Must not be {@code null}.
     * @param have
     *            objects whose ancestors (up to and including {@code shallow}
     *            commits) do not need to be included in the pack because they
     *            are already available from elsewhere. Must not be
     *            {@code null}.
     * @param shallow
     *            commits indicating the boundary of the history marked with
     *            {@code have}. Shallow commits have parents but those parents
     *            are considered not to be already available. Parents of
     *            {@code shallow} commits and earlier generations will be
     *            included in the pack if requested by {@code want}. Must not be
     *            {@code null}.
     * @param noBitmaps
     *            collection of objects to be excluded from bitmap commit
     *            selection.
     * @throws java.io.IOException
     *             an I/O problem occurred while reading objects.
     */
    public void preparePack(ProgressMonitor countingMonitor, @NonNull Set<? extends ObjectId> want,
            @NonNull Set<? extends ObjectId> have, @NonNull Set<? extends ObjectId> shallow,
            @NonNull Set<? extends ObjectId> noBitmaps) throws IOException {
        try (ObjectWalk ow = getObjectWalk()) {
            ow.assumeShallow(shallow);
            preparePack(countingMonitor, ow, want, have, noBitmaps);
        }
    }

    private ObjectWalk getObjectWalk() {
        return shallowPack ? new DepthWalk.ObjectWalk(reader, depth - 1) : new ObjectWalk(reader);
    }

    /**
     * A visitation policy which uses the depth at which the object is seen to
     * decide if re-traversal is necessary. In particular, if the object has
     * already been visited at this depth or shallower, it is not necessary to
     * re-visit at this depth.
     */
    private static class DepthAwareVisitationPolicy implements ObjectWalk.VisitationPolicy {
        private final Map<ObjectId, Integer> lowestDepthVisited = new HashMap<>();

        private final ObjectWalk walk;

        DepthAwareVisitationPolicy(ObjectWalk walk) {
            this.walk = requireNonNull(walk);
        }

        @Override
        public boolean shouldVisit(RevObject o) {
            Integer lastDepth = lowestDepthVisited.get(o);
            if (lastDepth == null) {
                return true;
            }
            return walk.getTreeDepth() < lastDepth.intValue();
        }

        @Override
        public void visited(RevObject o) {
            lowestDepthVisited.put(o, Integer.valueOf(walk.getTreeDepth()));
        }
    }

    /**
     * Prepare the list of objects to be written to the pack stream.
     * <p>
     * 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 org.eclipse.jgit.revwalk.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.
     * </p>
     *
     * @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). Must not be {@code null}.
     * @param uninterestingObjects
     *            collection of objects to be marked as uninteresting (end
     *            points of graph traversal). Pass {@link #NONE} if all objects
     *            reachable from {@code want} are desired, such as when serving
     *            a clone.
     * @param noBitmaps
     *            collection of objects to be excluded from bitmap commit
     *            selection.
     * @throws java.io.IOException
     *             when some I/O problem occur during reading objects.
     */
    public void preparePack(ProgressMonitor countingMonitor, @NonNull ObjectWalk walk,
            @NonNull Set<? extends ObjectId> interestingObjects,
            @NonNull Set<? extends ObjectId> uninterestingObjects, @NonNull Set<? extends ObjectId> noBitmaps)
            throws IOException {
        if (countingMonitor == null)
            countingMonitor = NullProgressMonitor.INSTANCE;
        if (shallowPack && !(walk instanceof DepthWalk.ObjectWalk))
            throw new IllegalArgumentException(JGitText.get().shallowPacksRequireDepthWalk);
        if (filterSpec.getTreeDepthLimit() >= 0) {
            walk.setVisitationPolicy(new DepthAwareVisitationPolicy(walk));
        }
        findObjectsToPack(countingMonitor, walk, interestingObjects, uninterestingObjects, noBitmaps);
    }

    /**
     * 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 java.io.IOException
     *             a cached pack cannot be examined.
     */
    public boolean willInclude(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[OBJECT_ID_LENGTH];
        final MessageDigest md = Constants.newMessageDigest();
        for (ObjectToPack otp : sortByName()) {
            otp.copyRawTo(buf, 0);
            md.update(buf, 0, OBJECT_ID_LENGTH);
        }
        return ObjectId.fromRaw(md.digest());
    }

    /**
     * Returns the index format version that will be written.
     * <p>
     * This method can only be invoked after
     * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)} has
     * been invoked and completed successfully.
     *
     * @return the index format version.
     */
    public int getIndexVersion() {
        int indexVersion = config.getIndexVersion();
        if (indexVersion <= 0) {
            for (BlockList<ObjectToPack> objs : objectsLists)
                indexVersion = Math.max(indexVersion, PackIndexWriter.oldestPossibleFormat(objs));
        }
        return indexVersion;
    }

    /**
     * Create an index file to match the pack file just written.
     * <p>
     * Called after
     * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}.
     * <p>
     * Writing an index is only required for local pack storage. Packs sent on
     * the network do not need to create an index.
     *
     * @param indexStream
     *            output for the index data. Caller is responsible for closing
     *            this stream.
     * @throws java.io.IOException
     *             the index data could not be written to the supplied stream.
     */
    public void writeIndex(OutputStream indexStream) throws IOException {
        if (isIndexDisabled())
            throw new IOException(JGitText.get().cachedPacksPreventsIndexCreation);

        long writeStart = System.currentTimeMillis();
        final PackIndexWriter iw = PackIndexWriter.createVersion(indexStream, getIndexVersion());
        iw.write(sortByName(), packcsum);
        stats.timeWriting += System.currentTimeMillis() - writeStart;
    }

    /**
     * Create a bitmap index file to match the pack file just written.
     * <p>
     * Called after {@link #prepareBitmapIndex(ProgressMonitor)}.
     *
     * @param bitmapIndexStream
     *            output for the bitmap index data. Caller is responsible for
     *            closing this stream.
     * @throws java.io.IOException
     *             the index data could not be written to the supplied stream.
     */
    public void writeBitmapIndex(OutputStream bitmapIndexStream) throws IOException {
        if (writeBitmaps == null)
            throw new IOException(JGitText.get().bitmapsMustBePrepared);

        long writeStart = System.currentTimeMillis();
        final PackBitmapIndexWriterV1 iw = new PackBitmapIndexWriterV1(bitmapIndexStream);
        iw.write(writeBitmaps, packcsum);
        stats.timeWriting += System.currentTimeMillis() - writeStart;
    }

    private List<ObjectToPack> sortByName() {
        if (sortedByName == null) {
            int cnt = 0;
            cnt += objectsLists[OBJ_COMMIT].size();
            cnt += objectsLists[OBJ_TREE].size();
            cnt += objectsLists[OBJ_BLOB].size();
            cnt += objectsLists[OBJ_TAG].size();

            sortedByName = new BlockList<>(cnt);
            sortedByName.addAll(objectsLists[OBJ_COMMIT]);
            sortedByName.addAll(objectsLists[OBJ_TREE]);
            sortedByName.addAll(objectsLists[OBJ_BLOB]);
            sortedByName.addAll(objectsLists[OBJ_TAG]);
            Collections.sort(sortedByName);
        }
        return sortedByName;
    }

    private void beginPhase(PackingPhase phase, ProgressMonitor monitor, long cnt) {
        state.phase = phase;
        String task;
        switch (phase) {
        case COUNTING:
            task = JGitText.get().countingObjects;
            break;
        case GETTING_SIZES:
            task = JGitText.get().searchForSizes;
            break;
        case FINDING_SOURCES:
            task = JGitText.get().searchForReuse;
            break;
        case COMPRESSING:
            task = JGitText.get().compressingObjects;
            break;
        case WRITING:
            task = JGitText.get().writingObjects;
            break;
        case BUILDING_BITMAPS:
            task = JGitText.get().buildingBitmaps;
            break;
        default:
            throw new IllegalArgumentException(MessageFormat.format(JGitText.get().illegalPackingPhase, phase));
        }
        monitor.beginTask(task, (int) cnt);
    }

    private void endPhase(ProgressMonitor monitor) {
        monitor.endTask();
    }

    /**
     * Write the prepared pack to the supplied stream.
     * <p>
     * Called after
     * {@link #preparePack(ProgressMonitor, ObjectWalk, Set, Set, Set)} or
     * {@link #preparePack(ProgressMonitor, Set, Set)}.
     * <p>
     * Performs delta search if enabled and writes the pack stream.
     * <p>
     * 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 java.io.IOException
     *             an error occurred reading a local object's data to include in
     *             the pack, or writing compressed object data to the output
     *             stream.
     * @throws WriteAbortedException
     *             the write operation is aborted by
     *             {@link org.eclipse.jgit.transport.ObjectCountCallback} .
     */
    public void writePack(ProgressMonitor compressMonitor, ProgressMonitor writeMonitor, OutputStream packStream)
            throws IOException {
        if (compressMonitor == null)
            compressMonitor = NullProgressMonitor.INSTANCE;
        if (writeMonitor == null)
            writeMonitor = NullProgressMonitor.INSTANCE;

        excludeInPacks = null;
        excludeInPackLast = null;

        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);

        crc32 = new CRC32();
        final PackOutputStream out = new PackOutputStream(writeMonitor,
                isIndexDisabled() ? packStream : new CheckedOutputStream(packStream, crc32), this);

        long objCnt = packfileUriConfig == null ? getObjectCount() : getUnoffloadedObjectCount();
        stats.totalObjects = objCnt;
        if (callback != null)
            callback.setObjectCount(objCnt);
        beginPhase(PackingPhase.WRITING, writeMonitor, objCnt);
        long writeStart = System.currentTimeMillis();
        try {
            List<CachedPack> unwrittenCachedPacks;

            if (packfileUriConfig != null) {
                unwrittenCachedPacks = new ArrayList<>();
                CachedPackUriProvider p = packfileUriConfig.cachedPackUriProvider;
                PacketLineOut o = packfileUriConfig.pckOut;

                o.writeString("packfile-uris\n"); //$NON-NLS-1$
                for (CachedPack pack : cachedPacks) {
                    CachedPackUriProvider.PackInfo packInfo = p.getInfo(pack, packfileUriConfig.protocolsSupported);
                    if (packInfo != null) {
                        o.writeString(packInfo.getHash() + ' ' + packInfo.getUri() + '\n');
                        stats.offloadedPackfiles += 1;
                        stats.offloadedPackfileSize += packInfo.getSize();
                    } else {
                        unwrittenCachedPacks.add(pack);
                    }
                }
                packfileUriConfig.pckOut.writeDelim();
                packfileUriConfig.pckOut.writeString("packfile\n"); //$NON-NLS-1$
            } else {
                unwrittenCachedPacks = cachedPacks;
            }

            out.writeFileHeader(PACK_VERSION_GENERATED, objCnt);
            out.flush();

            writeObjects(out);
            if (!edgeObjects.isEmpty() || !cachedPacks.isEmpty()) {
                for (PackStatistics.ObjectType.Accumulator typeStat : stats.objectTypes) {
                    if (typeStat == null)
                        continue;
                    stats.thinPackBytes += typeStat.bytes;
                }
            }

            stats.reusedPacks = Collections.unmodifiableList(cachedPacks);
            for (CachedPack pack : unwrittenCachedPacks) {
                long deltaCnt = pack.getDeltaCount();
                stats.reusedObjects += pack.getObjectCount();
                stats.reusedDeltas += deltaCnt;
                stats.totalDeltas += deltaCnt;
                reuseSupport.copyPackAsIs(out, pack);
            }
            writeChecksum(out);
            out.flush();
        } finally {
            stats.timeWriting = System.currentTimeMillis() - writeStart;
            stats.depth = depth;

            for (PackStatistics.ObjectType.Accumulator typeStat : stats.objectTypes) {
                if (typeStat == null)
                    continue;
                typeStat.cntDeltas += typeStat.reusedDeltas;
                stats.reusedObjects += typeStat.reusedObjects;
                stats.reusedDeltas += typeStat.reusedDeltas;
                stats.totalDeltas += typeStat.cntDeltas;
            }
        }

        stats.totalBytes = out.length();
        reader.close();
        endPhase(writeMonitor);
    }

    /**
     * Get statistics of what this PackWriter did in order to create the final
     * pack stream.
     *
     * @return description of what this PackWriter did in order to create the
     *         final pack stream. This should only be invoked after the calls to
     *         create the pack/index/bitmap have completed.
     */
    public PackStatistics getStatistics() {
        return new PackStatistics(stats);
    }

    /**
     * Get snapshot of the current state of this PackWriter.
     *
     * @return snapshot of the current state of this PackWriter.
     */
    public State getState() {
        return state.snapshot();
    }

    /**
     * {@inheritDoc}
     * <p>
     * Release all resources used by this writer.
     */
    @Override
    public void close() {
        reader.close();
        if (myDeflater != null) {
            myDeflater.end();
            myDeflater = null;
        }
        instances.remove(selfRef);
    }

    private void searchForReuse(ProgressMonitor monitor) throws IOException {
        long cnt = 0;
        cnt += objectsLists[OBJ_COMMIT].size();
        cnt += objectsLists[OBJ_TREE].size();
        cnt += objectsLists[OBJ_BLOB].size();
        cnt += objectsLists[OBJ_TAG].size();

        long start = System.currentTimeMillis();
        beginPhase(PackingPhase.FINDING_SOURCES, monitor, cnt);
        if (cnt <= 4096) {
            // For small object counts, do everything as one list.
            BlockList<ObjectToPack> tmp = new BlockList<>((int) cnt);
            tmp.addAll(objectsLists[OBJ_TAG]);
            tmp.addAll(objectsLists[OBJ_COMMIT]);
            tmp.addAll(objectsLists[OBJ_TREE]);
            tmp.addAll(objectsLists[OBJ_BLOB]);
            searchForReuse(monitor, tmp);
            if (pruneCurrentObjectList) {
                // If the list was pruned, we need to re-prune the main lists.
                pruneEdgesFromObjectList(objectsLists[OBJ_COMMIT]);
                pruneEdgesFromObjectList(objectsLists[OBJ_TREE]);
                pruneEdgesFromObjectList(objectsLists[OBJ_BLOB]);
                pruneEdgesFromObjectList(objectsLists[OBJ_TAG]);
            }
        } else {
            searchForReuse(monitor, objectsLists[OBJ_TAG]);
            searchForReuse(monitor, objectsLists[OBJ_COMMIT]);
            searchForReuse(monitor, objectsLists[OBJ_TREE]);
            searchForReuse(monitor, objectsLists[OBJ_BLOB]);
        }
        endPhase(monitor);
        stats.timeSearchingForReuse = System.currentTimeMillis() - start;

        if (config.isReuseDeltas() && config.getCutDeltaChains()) {
            cutDeltaChains(objectsLists[OBJ_TREE]);
            cutDeltaChains(objectsLists[OBJ_BLOB]);
        }
    }

    private void searchForReuse(ProgressMonitor monitor, List<ObjectToPack> list)
            throws IOException, MissingObjectException {
        pruneCurrentObjectList = false;
        reuseSupport.selectObjectRepresentation(this, monitor, list);
        if (pruneCurrentObjectList)
            pruneEdgesFromObjectList(list);
    }

    private void cutDeltaChains(BlockList<ObjectToPack> list) throws IOException {
        int max = config.getMaxDeltaDepth();
        for (int idx = list.size() - 1; idx >= 0; idx--) {
            int d = 0;
            ObjectToPack b = list.get(idx).getDeltaBase();
            while (b != null) {
                if (d < b.getChainLength())
                    break;
                b.setChainLength(++d);
                if (d >= max && b.isDeltaRepresentation()) {
                    reselectNonDelta(b);
                    break;
                }
                b = b.getDeltaBase();
            }
        }
        if (config.isDeltaCompress()) {
            for (ObjectToPack otp : list)
                otp.clearChainLength();
        }
    }

    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[OBJ_TREE].size() + objectsLists[OBJ_BLOB].size()
                + edgeObjects.size()];
        int cnt = 0;
        cnt = findObjectsNeedingDelta(list, cnt, OBJ_TREE);
        cnt = findObjectsNeedingDelta(list, cnt, 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();
        beginPhase(PackingPhase.GETTING_SIZES, monitor, cnt);
        AsyncObjectSizeQueue<ObjectToPack> sizeQueue = reader
                .getObjectSize(Arrays.<ObjectToPack>asList(list).subList(0, cnt), false);
        try {
            final long limit = Math.min(config.getBigFileThreshold(), Integer.MAX_VALUE);
            for (;;) {
                try {
                    if (!sizeQueue.next())
                        break;
                } catch (MissingObjectException notFound) {
                    monitor.update(1);
                    if (ignoreMissingUninteresting) {
                        ObjectToPack otp = sizeQueue.getCurrent();
                        if (otp != null && otp.isEdge()) {
                            otp.setDoNotDelta();
                            continue;
                        }

                        otp = objectsMap.get(notFound.getObjectId());
                        if (otp != null && otp.isEdge()) {
                            otp.setDoNotDelta();
                            continue;
                        }
                    }
                    throw notFound;
                }

                ObjectToPack otp = sizeQueue.getCurrent();
                if (otp == null)
                    otp = objectsMap.get(sizeQueue.getObjectId());

                long sz = sizeQueue.getSize();
                if (DeltaIndex.BLKSZ < sz && sz < limit)
                    otp.setWeight((int) sz);
                else
                    otp.setDoNotDelta(); // too small, or too big
                monitor.update(1);
            }
        } finally {
            sizeQueue.release();
        }
        endPhase(monitor);
        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, (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();
        searchForDeltas(monitor, list, cnt);
        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 reselectNonDelta(ObjectToPack otp) throws IOException {
        otp.clearDeltaBase();
        otp.clearReuseAsIs();
        boolean old = reuseDeltas;
        reuseDeltas = false;
        reuseSupport.selectObjectRepresentation(this, NullProgressMonitor.INSTANCE, Collections.singleton(otp));
        reuseDeltas = old;
    }

    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 <= config.getDeltaSearchWindowSize())
            singleThreadDeltaSearch(monitor, list, cnt);
        else
            parallelDeltaSearch(monitor, list, cnt, threads);
    }

    private void singleThreadDeltaSearch(ProgressMonitor monitor, ObjectToPack[] list, int cnt) throws IOException {
        long totalWeight = 0;
        for (int i = 0; i < cnt; i++) {
            ObjectToPack o = list[i];
            totalWeight += DeltaTask.getAdjustedWeight(o);
        }

        long bytesPerUnit = 1;
        while (DeltaTask.MAX_METER <= (totalWeight / bytesPerUnit))
            bytesPerUnit <<= 10;
        int cost = (int) (totalWeight / bytesPerUnit);
        if (totalWeight % bytesPerUnit != 0)
            cost++;

        beginPhase(PackingPhase.COMPRESSING, monitor, cost);
        new DeltaWindow(config, new DeltaCache(config), reader, monitor, bytesPerUnit, list, 0, cnt).search();
        endPhase(monitor);
    }

    private void parallelDeltaSearch(ProgressMonitor monitor, ObjectToPack[] list, int cnt, int threads)
            throws IOException {
        DeltaCache dc = new ThreadSafeDeltaCache(config);
        ThreadSafeProgressMonitor pm = new ThreadSafeProgressMonitor(monitor);
        DeltaTask.Block taskBlock = new DeltaTask.Block(threads, config, reader, dc, pm, list, 0, cnt);
        taskBlock.partitionTasks();
        beginPhase(PackingPhase.COMPRESSING, monitor, taskBlock.cost());
        pm.startWorkers(taskBlock.tasks.size());

        Executor executor = config.getExecutor();
        final List<Throwable> errors = Collections.synchronizedList(new ArrayList<>(threads));
        if (executor instanceof ExecutorService) {
            // Caller supplied us a service, use it directly.
            runTasks((ExecutorService) executor, pm, taskBlock, 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, taskBlock, 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 (DeltaTask task : taskBlock.tasks) {
                executor.execute(() -> {
                    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;

            throw new IOException(err.getMessage(), err);
        }
        endPhase(monitor);
    }

    private static void runTasks(ExecutorService pool, ThreadSafeProgressMonitor pm, DeltaTask.Block tb,
            List<Throwable> errors) throws IOException {
        List<Future<?>> futures = new ArrayList<>(tb.tasks.size());
        for (DeltaTask task : tb.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[OBJ_COMMIT]);
        writeObjects(out, objectsLists[OBJ_TAG]);
        writeObjects(out, objectsLists[OBJ_TREE]);
        writeObjects(out, objectsLists[OBJ_BLOB]);
    }

    private void writeObjects(PackOutputStream out, List<ObjectToPack> 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.
            reselectNonDelta(otp);
        }
        otp.markWantWrite();

        while (otp.isReuseAsIs()) {
            writeBase(out, otp.getDeltaBase());
            if (otp.isWritten())
                return; // Delta chain cycle caused this to write already.

            crc32.reset();
            otp.setOffset(out.length());
            try {
                reuseSupport.copyObjectAsIs(out, otp, reuseValidate);
                out.endObject();
                otp.setCRC((int) crc32.getValue());
                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;
                }
                // Object writing already started, we cannot recover.
                //
                CorruptObjectException coe;
                coe = new CorruptObjectException(otp, ""); //$NON-NLS-1$
                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((int) crc32.getValue());
    }

    private void writeBase(PackOutputStream out, ObjectToPack base) throws IOException {
        if (base != null && !base.isWritten() && !base.isEdge())
            writeObjectImpl(out, base);
    }

    private void writeWholeObjectDeflate(PackOutputStream out, final ObjectToPack otp) throws IOException {
        final Deflater deflater = deflater();
        final ObjectLoader ldr = reader.open(otp, otp.getType());

        crc32.reset();
        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());

        crc32.reset();
        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);
                typeStats.cntDeltas++;
                typeStats.deltaBytes += out.length() - otp.getOffset();
                return;
            }
        }

        try (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(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(@NonNull ProgressMonitor countingMonitor, @NonNull ObjectWalk walker,
            @NonNull Set<? extends ObjectId> want, @NonNull Set<? extends ObjectId> have,
            @NonNull Set<? extends ObjectId> noBitmaps) throws IOException {
        final long countingStart = System.currentTimeMillis();
        beginPhase(PackingPhase.COUNTING, countingMonitor, ProgressMonitor.UNKNOWN);

        stats.interestingObjects = Collections.unmodifiableSet(new HashSet<ObjectId>(want));
        stats.uninterestingObjects = Collections.unmodifiableSet(new HashSet<ObjectId>(have));
        excludeFromBitmapSelection = noBitmaps;

        canBuildBitmaps = config.isBuildBitmaps() && !shallowPack && have.isEmpty()
                && (excludeInPacks == null || excludeInPacks.length == 0);
        if (!shallowPack && useBitmaps) {
            BitmapIndex bitmapIndex = reader.getBitmapIndex();
            if (bitmapIndex != null) {
                BitmapWalker bitmapWalker = new BitmapWalker(walker, bitmapIndex, countingMonitor);
                findObjectsToPackUsingBitmaps(bitmapWalker, want, have);
                endPhase(countingMonitor);
                stats.timeCounting = System.currentTimeMillis() - countingStart;
                stats.bitmapIndexMisses = bitmapWalker.getCountOfBitmapIndexMisses();
                return;
            }
        }

        List<ObjectId> all = new ArrayList<>(want.size() + have.size());
        all.addAll(want);
        all.addAll(have);

        final RevFlag include = walker.newFlag("include"); //$NON-NLS-1$
        final RevFlag added = walker.newFlag("added"); //$NON-NLS-1$

        walker.carry(include);

        int haveEst = have.size();
        if (have.isEmpty()) {
            walker.sort(RevSort.COMMIT_TIME_DESC);
        } else {
            walker.sort(RevSort.TOPO);
            if (thin)
                walker.sort(RevSort.BOUNDARY, true);
        }

        List<RevObject> wantObjs = new ArrayList<>(want.size());
        List<RevObject> haveObjs = new ArrayList<>(haveEst);
        List<RevTag> wantTags = new ArrayList<>(want.size());

        // Retrieve the RevWalk's versions of "want" and "have" objects to
        // maintain any state previously set in the RevWalk.
        AsyncRevObjectQueue q = walker.parseAny(all, true);
        try {
            for (;;) {
                try {
                    RevObject o = q.next();
                    if (o == null)
                        break;
                    if (have.contains(o))
                        haveObjs.add(o);
                    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();
            }
        }

        if (walker instanceof DepthWalk.ObjectWalk) {
            DepthWalk.ObjectWalk depthWalk = (DepthWalk.ObjectWalk) walker;
            for (RevObject obj : wantObjs) {
                depthWalk.markRoot(obj);
            }
            // Mark the tree objects associated with "have" commits as
            // uninteresting to avoid writing redundant blobs. A normal RevWalk
            // lazily propagates the "uninteresting" state from a commit to its
            // tree during the walk, but DepthWalks can terminate early so
            // preemptively propagate that state here.
            for (RevObject obj : haveObjs) {
                if (obj instanceof RevCommit) {
                    RevTree t = ((RevCommit) obj).getTree();
                    depthWalk.markUninteresting(t);
                }
            }

            if (unshallowObjects != null) {
                for (ObjectId id : unshallowObjects) {
                    depthWalk.markUnshallow(walker.parseAny(id));
                }
            }
        } else {
            for (RevObject obj : wantObjs)
                walker.markStart(obj);
        }
        for (RevObject obj : haveObjs)
            walker.markUninteresting(obj);

        final int maxBases = config.getDeltaSearchWindowSize();
        Set<RevTree> baseTrees = new HashSet<>();
        BlockList<RevCommit> commits = new BlockList<>();
        Set<ObjectId> roots = new HashSet<>();
        RevCommit c;
        while ((c = walker.next()) != null) {
            if (exclude(c))
                continue;
            if (c.has(RevFlag.UNINTERESTING)) {
                if (baseTrees.size() <= maxBases)
                    baseTrees.add(c.getTree());
                continue;
            }

            commits.add(c);
            if (c.getParentCount() == 0) {
                roots.add(c.copy());
            }
            countingMonitor.update(1);
        }
        stats.rootCommits = Collections.unmodifiableSet(roots);

        if (shallowPack) {
            for (RevCommit cmit : commits) {
                addObject(cmit, 0);
            }
        } else {
            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) && !exclude(p)) {
                        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;
                if (exclude(o))
                    continue;

                int pathHash = walker.getPathHashCode();
                byte[] pathBuf = walker.getPathBuffer();
                int pathLen = walker.getPathLength();
                bases.addBase(o.getType(), pathBuf, pathLen, pathHash);
                if (!depthSkip(o, walker)) {
                    filterAndAddObject(o, o.getType(), pathHash, want);
                }
                countingMonitor.update(1);
            }
        } else {
            RevObject o;
            while ((o = walker.nextObject()) != null) {
                if (o.has(RevFlag.UNINTERESTING))
                    continue;
                if (exclude(o))
                    continue;
                if (!depthSkip(o, walker)) {
                    filterAndAddObject(o, o.getType(), walker.getPathHashCode(), want);
                }
                countingMonitor.update(1);
            }
        }

        for (CachedPack pack : cachedPacks)
            countingMonitor.update((int) pack.getObjectCount());
        endPhase(countingMonitor);
        stats.timeCounting = System.currentTimeMillis() - countingStart;
        stats.bitmapIndexMisses = -1;
    }

    private void findObjectsToPackUsingBitmaps(BitmapWalker bitmapWalker, Set<? extends ObjectId> want,
            Set<? extends ObjectId> have) throws MissingObjectException, IncorrectObjectTypeException, IOException {
        BitmapBuilder haveBitmap = bitmapWalker.findObjects(have, null, true);
        BitmapBuilder wantBitmap = bitmapWalker.findObjects(want, haveBitmap, false);
        BitmapBuilder needBitmap = wantBitmap.andNot(haveBitmap);

        if (useCachedPacks && reuseSupport != null && !reuseValidate
                && (excludeInPacks == null || excludeInPacks.length == 0))
            cachedPacks.addAll(reuseSupport.getCachedPacksAndUpdate(needBitmap));

        for (BitmapObject obj : needBitmap) {
            ObjectId objectId = obj.getObjectId();
            if (exclude(objectId)) {
                needBitmap.remove(objectId);
                continue;
            }
            filterAndAddObject(objectId, obj.getType(), 0, want);
        }

        if (thin)
            haveObjects = haveBitmap;
    }

    private static void pruneEdgesFromObjectList(List<ObjectToPack> 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);
    }

    /**
     * Include one object to the output file.
     * <p>
     * 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 org.eclipse.jgit.errors.IncorrectObjectTypeException
     *             the object is an unsupported type.
     */
    public void addObject(RevObject object) throws IncorrectObjectTypeException {
        if (!exclude(object))
            addObject(object, 0);
    }

    private void addObject(RevObject object, int pathHashCode) {
        addObject(object, object.getType(), pathHashCode);
    }

    private void addObject(final AnyObjectId src, final int type, final int pathHashCode) {
        final ObjectToPack otp;
        if (reuseSupport != null)
            otp = reuseSupport.newObjectToPack(src, type);
        else
            otp = new ObjectToPack(src, type);
        otp.setPathHash(pathHashCode);
        objectsLists[type].add(otp);
        objectsMap.add(otp);
    }

    /**
     * Determines if the object should be omitted from the pack as a result of
     * its depth (probably because of the tree:<depth> filter).
     * <p>
     * Causes {@code walker} to skip traversing the current tree, which ought to
     * have just started traversal, assuming this method is called as soon as a
     * new depth is reached.
     * <p>
     * This method increments the {@code treesTraversed} statistic.
     *
     * @param obj
     *            the object to check whether it should be omitted.
     * @param walker
     *            the walker being used for traveresal.
     * @return whether the given object should be skipped.
     */
    private boolean depthSkip(@NonNull RevObject obj, ObjectWalk walker) {
        long treeDepth = walker.getTreeDepth();

        // Check if this object needs to be rejected because it is a tree or
        // blob that is too deep from the root tree.

        // A blob is considered one level deeper than the tree that contains it.
        if (obj.getType() == OBJ_BLOB) {
            treeDepth++;
        } else {
            stats.treesTraversed++;
        }

        if (filterSpec.getTreeDepthLimit() < 0 || treeDepth <= filterSpec.getTreeDepthLimit()) {
            return false;
        }

        walker.skipTree();
        return true;
    }

    // Adds the given object as an object to be packed, first performing
    // filtering on blobs at or exceeding a given size.
    private void filterAndAddObject(@NonNull AnyObjectId src, int type, int pathHashCode,
            @NonNull Set<? extends AnyObjectId> want) throws IOException {

        // Check if this object needs to be rejected, doing the cheaper
        // checks first.
        boolean reject = filterSpec.getBlobLimit() >= 0 && type == OBJ_BLOB && !want.contains(src)
                && reader.getObjectSize(src, OBJ_BLOB) > filterSpec.getBlobLimit();
        if (!reject) {
            addObject(src, type, pathHashCode);
        }
    }

    private boolean exclude(AnyObjectId objectId) {
        if (excludeInPacks == null)
            return false;
        if (excludeInPackLast.contains(objectId))
            return true;
        for (ObjectIdSet idx : excludeInPacks) {
            if (idx.contains(objectId)) {
                excludeInPackLast = idx;
                return true;
            }
        }
        return false;
    }

    /**
     * Select an object representation for this writer.
     * <p>
     * An {@link org.eclipse.jgit.lib.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 && have(ptr, baseId)) {
                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.setDeltaAttempted(reuseDeltas && next.wasDeltaAttempted());
        otp.select(next);
    }

    private final boolean have(ObjectToPack ptr, AnyObjectId objectId) {
        return (ptr != null && ptr.isEdge()) || (haveObjects != null && haveObjects.contains(objectId));
    }

    /**
     * Prepares the bitmaps to be written to the bitmap index file.
     * <p>
     * Bitmaps can be used to speed up fetches and clones by storing the entire
     * object graph at selected commits. Writing a bitmap index is an optional
     * feature that not all pack users may require.
     * <p>
     * Called after {@link #writeIndex(OutputStream)}.
     * <p>
     * To reduce memory internal state is cleared during this method, rendering
     * the PackWriter instance useless for anything further than a call to write
     * out the new bitmaps with {@link #writeBitmapIndex(OutputStream)}.
     *
     * @param pm
     *            progress monitor to report bitmap building work.
     * @return whether a bitmap index may be written.
     * @throws java.io.IOException
     *             when some I/O problem occur during reading objects.
     */
    public boolean prepareBitmapIndex(ProgressMonitor pm) throws IOException {
        if (!canBuildBitmaps || getObjectCount() > Integer.MAX_VALUE || !cachedPacks.isEmpty())
            return false;

        if (pm == null)
            pm = NullProgressMonitor.INSTANCE;

        int numCommits = objectsLists[OBJ_COMMIT].size();
        List<ObjectToPack> byName = sortByName();
        sortedByName = null;
        objectsLists = null;
        objectsMap = null;
        writeBitmaps = new PackBitmapIndexBuilder(byName);
        byName = null;

        PackWriterBitmapPreparer bitmapPreparer = new PackWriterBitmapPreparer(reader, writeBitmaps, pm,
                stats.interestingObjects, config);

        Collection<PackWriterBitmapPreparer.BitmapCommit> selectedCommits = bitmapPreparer.selectCommits(numCommits,
                excludeFromBitmapSelection);

        beginPhase(PackingPhase.BUILDING_BITMAPS, pm, selectedCommits.size());

        BitmapWalker walker = bitmapPreparer.newBitmapWalker();
        AnyObjectId last = null;
        for (PackWriterBitmapPreparer.BitmapCommit cmit : selectedCommits) {
            if (!cmit.isReuseWalker()) {
                walker = bitmapPreparer.newBitmapWalker();
            }
            BitmapBuilder bitmap = walker.findObjects(Collections.singleton(cmit), null, false);

            if (last != null && cmit.isReuseWalker() && !bitmap.contains(last))
                throw new IllegalStateException(
                        MessageFormat.format(JGitText.get().bitmapMissingObject, cmit.name(), last.name()));
            last = cmit;
            writeBitmaps.addBitmap(cmit, bitmap.build(), cmit.getFlags());

            pm.update(1);
        }

        endPhase(pm);
        return true;
    }

    private boolean reuseDeltaFor(ObjectToPack otp) {
        int type = otp.getType();
        if ((type & 2) != 0) // OBJ_TREE(2) or OBJ_BLOB(3)
            return true;
        if (type == OBJ_COMMIT)
            return reuseDeltaCommits;
        if (type == OBJ_TAG)
            return false;
        return true;
    }

    private class MutableState {
        /** Estimated size of a single ObjectToPack instance. */
        // Assume 64-bit pointers, since this is just an estimate.
        private static final long OBJECT_TO_PACK_SIZE = (2 * 8) // Object header
                + (2 * 8) + (2 * 8) // ObjectToPack fields
                + (8 + 8) // PackedObjectInfo fields
                + 8 // ObjectIdOwnerMap fields
                + 40 // AnyObjectId fields
                + 8; // Reference in BlockList

        private final long totalDeltaSearchBytes;

        private volatile PackingPhase phase;

        MutableState() {
            phase = PackingPhase.COUNTING;
            if (config.isDeltaCompress()) {
                int threads = config.getThreads();
                if (threads <= 0)
                    threads = Runtime.getRuntime().availableProcessors();
                totalDeltaSearchBytes = (threads * config.getDeltaSearchMemoryLimit())
                        + config.getBigFileThreshold();
            } else
                totalDeltaSearchBytes = 0;
        }

        State snapshot() {
            long objCnt = 0;
            BlockList<ObjectToPack>[] lists = objectsLists;
            if (lists != null) {
                objCnt += lists[OBJ_COMMIT].size();
                objCnt += lists[OBJ_TREE].size();
                objCnt += lists[OBJ_BLOB].size();
                objCnt += lists[OBJ_TAG].size();
                // Exclude CachedPacks.
            }

            long bytesUsed = OBJECT_TO_PACK_SIZE * objCnt;
            PackingPhase curr = phase;
            if (curr == PackingPhase.COMPRESSING)
                bytesUsed += totalDeltaSearchBytes;
            return new State(curr, bytesUsed);
        }
    }

    /** Possible states that a PackWriter can be in. */
    public static enum PackingPhase {
        /** Counting objects phase. */
        COUNTING,

        /** Getting sizes phase. */
        GETTING_SIZES,

        /** Finding sources phase. */
        FINDING_SOURCES,

        /** Compressing objects phase. */
        COMPRESSING,

        /** Writing objects phase. */
        WRITING,

        /** Building bitmaps phase. */
        BUILDING_BITMAPS;
    }

    /** Summary of the current state of a PackWriter. */
    public class State {
        private final PackingPhase phase;

        private final long bytesUsed;

        State(PackingPhase phase, long bytesUsed) {
            this.phase = phase;
            this.bytesUsed = bytesUsed;
        }

        /** @return the PackConfig used to build the writer. */
        public PackConfig getConfig() {
            return config;
        }

        /** @return the current phase of the writer. */
        public PackingPhase getPhase() {
            return phase;
        }

        /** @return an estimate of the total memory used by the writer. */
        public long estimateBytesUsed() {
            return bytesUsed;
        }

        @SuppressWarnings("nls")
        @Override
        public String toString() {
            return "PackWriter.State[" + phase + ", memory=" + bytesUsed + "]";
        }
    }

    /**
     * Configuration related to the packfile URI feature.
     *
     * @since 5.5
     */
    public static class PackfileUriConfig {
        @NonNull
        private final PacketLineOut pckOut;

        @NonNull
        private final Collection<String> protocolsSupported;

        @NonNull
        private final CachedPackUriProvider cachedPackUriProvider;

        /**
         * @param pckOut where to write "packfile-uri" lines to (should
         *     output to the same stream as the one passed to
         *     PackWriter#writePack)
         * @param protocolsSupported list of protocols supported (e.g. "https")
         * @param cachedPackUriProvider provider of URIs corresponding
         *     to cached packs
         * @since 5.5
         */
        public PackfileUriConfig(@NonNull PacketLineOut pckOut, @NonNull Collection<String> protocolsSupported,
                @NonNull CachedPackUriProvider cachedPackUriProvider) {
            this.pckOut = pckOut;
            this.protocolsSupported = protocolsSupported;
            this.cachedPackUriProvider = cachedPackUriProvider;
        }
    }
}