();
+
+ private final Repository db;
+
+ private final DigestOutputStream out;
+
+ private final CountingOutputStream countingOut;
+
+ private final Deflater deflater;
+
+ private final ProgressMonitor monitor;
+
+ private final byte[] buf = new byte[16384]; // 16 KB
+
+ private final WindowCursor windowCursor = new WindowCursor();
+
+ private boolean reuseDeltas = DEFAULT_REUSE_DELTAS;
+
+ private boolean reuseObjects = DEFAULT_REUSE_OBJECTS;
+
+ private boolean deltaBaseAsOffset = DEFAULT_DELTA_BASE_AS_OFFSET;
+
+ private int maxDeltaDepth = DEFAULT_MAX_DELTA_DEPTH;
+
+ private boolean thin;
+
+ /**
+ * Create writer for specified repository, that will write a pack to
+ * provided output stream. Objects for packing are specified in
+ * {@link #writePack(Iterator)} or
+ * {@link #writePack(Collection, Collection, boolean)}.
+ *
+ * @param repo
+ * repository where objects are stored.
+ * @param out
+ * output stream of pack data; no buffering is guaranteed by
+ * writer.
+ * @param monitor
+ * operations progress monitor, used within
+ * {@link #writePack(Iterator)} or
+ * {@link #writePack(Collection, Collection, boolean)}.
+ */
+ public PackWriter(final Repository repo, final OutputStream out,
+ final ProgressMonitor monitor) {
+ this.db = repo;
+ this.monitor = monitor;
+ this.countingOut = new CountingOutputStream(out);
+ this.out = new DigestOutputStream(countingOut, Constants
+ .newMessageDigest());
+ this.deflater = new Deflater(db.getConfig().getCore().getCompression());
+ }
+
+ /**
+ * Check whether object is configured to reuse deltas existing in
+ * repository.
+ *
+ * Default setting: {@value #DEFAULT_REUSE_DELTAS}
+ *
+ *
+ * @return true if object is configured to reuse deltas; false otherwise.
+ */
+ public boolean isReuseDeltas() {
+ return reuseDeltas;
+ }
+
+ /**
+ * Set reuse deltas configuration option for this writer. When enabled,
+ * writer will search for delta representation of object in repository and
+ * use it if possible. Normally, only deltas with base to another object
+ * existing in set of objects to pack will be used. Exception is however
+ * thin-pack (see {@link #writePack(Collection, Collection, boolean)} and
+ * {@link #writePack(Iterator)}) where base object must exist on other side
+ * machine.
+ *
+ * When raw delta data is directly copied from a pack file, checksum is
+ * computed to verify data.
+ *
+ *
+ * Default setting: {@value #DEFAULT_REUSE_DELTAS}
+ *
+ *
+ * @param reuseDeltas
+ * boolean indicating whether or not try to reuse deltas.
+ */
+ public void setReuseDeltas(boolean reuseDeltas) {
+ this.reuseDeltas = reuseDeltas;
+ }
+
+ /**
+ * Checks whether object is configured to reuse existing objects
+ * representation in repository.
+ *
+ * Default setting: {@value #DEFAULT_REUSE_OBJECTS}
+ *
+ *
+ * @return true if writer is configured to reuse objects representation from
+ * pack; false otherwise.
+ */
+ public boolean isReuseObjects() {
+ return reuseObjects;
+ }
+
+ /**
+ * Set reuse objects configuration option for this writer. If enabled,
+ * writer searches for representation in a pack file. If possible,
+ * compressed data is directly copied from such a pack file. Data checksum
+ * is verified.
+ *
+ * Default setting: {@value #DEFAULT_REUSE_OBJECTS}
+ *
+ *
+ * @param reuseObjects
+ * boolean indicating whether or not writer should reuse existing
+ * objects representation.
+ */
+ public void setReuseObjects(boolean reuseObjects) {
+ this.reuseObjects = reuseObjects;
+ }
+
+ /**
+ * 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 #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 #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;
+ }
+
+ /**
+ * Get maximum depth of delta chain set up for this writer. Generated chains
+ * are not longer than this value.
+ *
+ * Default setting: {@value #DEFAULT_MAX_DELTA_DEPTH}
+ *
+ *
+ * @return maximum delta chain depth.
+ */
+ public int getMaxDeltaDepth() {
+ return maxDeltaDepth;
+ }
+
+ /**
+ * Set up maximum depth of delta chain for this writer. Generated chains are
+ * not longer than this value. Too low value causes low compression level,
+ * while too big makes unpacking (reading) longer.
+ *
+ * Default setting: {@value #DEFAULT_MAX_DELTA_DEPTH}
+ *
+ *
+ * @param maxDeltaDepth
+ * maximum delta chain depth.
+ */
+ public void setMaxDeltaDepth(int maxDeltaDepth) {
+ this.maxDeltaDepth = maxDeltaDepth;
+ }
+
+ /**
+ * Returns objects number in a pack file that was created by this writer.
+ *
+ * @return number of objects in pack.
+ */
+ public int getObjectsNumber() {
+ return objectsMap.size();
+ }
+
+ /**
+ * Write pack to output stream according to current writer configuration for
+ * provided source iterator of objects.
+ *
+ * Iterator exactly determines which objects are included in a pack
+ * and order they appear in pack (except that objects order by type is not
+ * needed at input). This order should conform general rules of ordering
+ * objects in git - by recency and path (type and delta-base first is
+ * internally secured) and responsibility for guaranteeing this order is on
+ * a caller side. Iterator must return each id of object to write exactly
+ * once.
+ *
+ *
+ * When iterator returns object that has {@link RevFlag#UNINTERESTING} flag,
+ * this object won't be included in an output pack. Instead, it is recorded
+ * as edge-object (known to remote repository) for thin-pack. In such a case
+ * writer may pack objects with delta base object not within set of objects
+ * to pack, but belonging to party repository - those marked with
+ * {@link RevFlag#UNINTERESTING} flag. This type of pack is used only for
+ * transport.
+ *
+ *
+ * At first, this method collects and sorts objects to pack, then deltas
+ * search is performed if set up accordingly, finally pack stream is
+ * written. {@link ProgressMonitor} tasks {@value #SEARCHING_REUSE_PROGRESS}
+ * (only if resueDeltas or reuseObjects is enabled) and
+ * {@value #WRITING_OBJECTS_PROGRESS} are updated during packing.
+ *
+ *
+ * All reused objects data checksum (Adler32/CRC32) is computed and
+ * validated against existing checksum.
+ *
+ *
+ * @param objectsSource
+ * iterator of object to store in a pack; order of objects within
+ * each type is important, ordering by type is not needed;
+ * allowed types for objects are {@link Constants#OBJ_COMMIT},
+ * {@link Constants#OBJ_TREE}, {@link Constants#OBJ_BLOB} and
+ * {@link Constants#OBJ_TAG}; objects returned by iterator may
+ * be later reused by caller as object id and type are internally
+ * copied in each iteration; if object returned by iterator has
+ * {@link RevFlag#UNINTERESTING} flag set, it won't be included
+ * in a pack, but is considered as edge-object for thin-pack.
+ * @throws IOException
+ * when some I/O problem occur during reading objects for pack
+ * or writing pack stream.
+ */
+ public void writePack(final Iterator objectsSource)
+ throws IOException {
+ while (objectsSource.hasNext()) {
+ addObject(objectsSource.next());
+ }
+ writePackInternal();
+ }
+
+ /**
+ * Write pack to output stream according to current writer configuration for
+ * provided sets of interesting and uninteresting objects.
+ *
+ * Basing on these 2 sets, another set of objects to put in a pack file is
+ * created: this set consists of all objects reachable (ancestors) from
+ * interesting objects, except uninteresting objects and their ancestors.
+ * This method uses class {@link ObjectWalk} extensively to find out that
+ * appropriate set of output objects and their optimal order in output pack.
+ * Order is consistent with general git in-pack rules: sort by object type,
+ * recency, path and delta-base first.
+ *
+ *
+ * At first, this method collects and sorts objects to pack, then deltas
+ * search is performed if set up accordingly, finally pack stream is
+ * written. {@link ProgressMonitor} tasks
+ * {@value #COUNTING_OBJECTS_PROGRESS}, {@value #SEARCHING_REUSE_PROGRESS}
+ * (only if resueDeltas or reuseObjects is enabled) and
+ * {@value #WRITING_OBJECTS_PROGRESS} are updated during packing.
+ *
+ *
+ * All reused objects data checksum (Adler32/CRC32) is computed and
+ * validated against existing checksum.
+ *
+ *
+ * @param interestingObjects
+ * collection of objects to be marked as interesting (start
+ * points of graph traversal).
+ * @param uninterestingObjects
+ * collection of objects to be marked as uninteresting (end
+ * points of graph traversal).
+ * @param thin
+ * 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.
+ * @throws IOException
+ * when some I/O problem occur during reading objects for pack
+ * or writing pack stream.
+ */
+ public void writePack(final Collection interestingObjects,
+ final Collection uninterestingObjects, boolean thin)
+ throws IOException {
+ ObjectWalk walker = setUpWalker(interestingObjects,
+ uninterestingObjects, thin);
+ findObjectsToPack(walker);
+ writePackInternal();
+ }
+
+ /**
+ * 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 ArrayList sorted = new ArrayList(
+ objectsMap.size());
+ for (List list : objectsLists) {
+ for (ObjectToPack otp : list)
+ sorted.add(otp);
+ }
+
+ final MessageDigest md = Constants.newMessageDigest();
+ Collections.sort(sorted);
+ for (ObjectToPack otp : sorted) {
+ otp.copyRawTo(buf, 0);
+ md.update(buf, 0, Constants.OBJECT_ID_LENGTH);
+ }
+ return ObjectId.fromRaw(md.digest());
+ }
+
+ private void writePackInternal() throws IOException {
+ if (reuseDeltas || reuseObjects)
+ searchForReuse();
+
+ monitor.beginTask(WRITING_OBJECTS_PROGRESS, getObjectsNumber());
+ writeHeader();
+ writeObjects();
+ writeChecksum();
+
+ out.flush();
+ windowCursor.release();
+ monitor.endTask();
+ }
+
+ private void searchForReuse() throws IOException {
+ monitor.beginTask(SEARCHING_REUSE_PROGRESS, getObjectsNumber());
+ final Collection reuseLoaders = new LinkedList();
+
+ for (List list : objectsLists) {
+ for (ObjectToPack otp : list) {
+ if (monitor.isCancelled())
+ throw new IOException(
+ "Packing cancelled during objects writing");
+ reuseLoaders.clear();
+ db.openObjectInAllPacks(otp, reuseLoaders, windowCursor);
+ if (reuseDeltas) {
+ selectDeltaReuseForObject(otp, reuseLoaders);
+ }
+ // delta reuse is preferred over object reuse
+ if (reuseObjects && !otp.hasReuseLoader()) {
+ selectObjectReuseForObject(otp, reuseLoaders);
+ }
+ monitor.update(1);
+ }
+ }
+
+ monitor.endTask();
+ }
+
+ private void selectDeltaReuseForObject(final ObjectToPack otp,
+ final Collection loaders) throws IOException {
+ PackedObjectLoader bestLoader = null;
+ ObjectId bestBase = null;
+
+ for (PackedObjectLoader loader : loaders) {
+ ObjectId idBase = loader.getDeltaBase();
+ if (idBase == null)
+ continue;
+ ObjectToPack otpBase = objectsMap.get(idBase);
+
+ // only if base is in set of objects to write or thin-pack's edge
+ if ((otpBase != null || (thin && edgeObjects.get(idBase) != null))
+ // select smallest possible delta if > 1 available
+ && isBetterDeltaReuseLoader(bestLoader, loader)) {
+ bestLoader = loader;
+ bestBase = (otpBase != null ? otpBase : idBase);
+ }
+ }
+
+ if (bestLoader != null) {
+ otp.setReuseLoader(bestLoader);
+ otp.setDeltaBase(bestBase);
+ }
+ }
+
+ private boolean isBetterDeltaReuseLoader(PackedObjectLoader currentLoader,
+ PackedObjectLoader loader) throws IOException {
+ if (currentLoader == null)
+ return true;
+ if (loader.getRawSize() < currentLoader.getRawSize())
+ return true;
+ return (loader.getRawSize() == currentLoader.getRawSize()
+ && loader.supportsFastCopyRawData() && !currentLoader
+ .supportsFastCopyRawData());
+ }
+
+ private void selectObjectReuseForObject(final ObjectToPack otp,
+ final Collection loaders) {
+ for (final PackedObjectLoader loader : loaders) {
+ if (loader instanceof WholePackedObjectLoader) {
+ otp.setReuseLoader(loader);
+ return;
+ }
+ }
+ }
+
+ private void writeHeader() throws IOException {
+ out.write(Constants.PACK_SIGNATURE);
+
+ NB.encodeInt32(buf, 0, PACK_VERSION_GENERATED);
+ out.write(buf, 0, 4);
+
+ NB.encodeInt32(buf, 0, getObjectsNumber());
+ out.write(buf, 0, 4);
+ }
+
+ private void writeObjects() throws IOException {
+ for (List list : objectsLists) {
+ for (ObjectToPack otp : list) {
+ if (monitor.isCancelled())
+ throw new IOException(
+ "Packing cancelled during objects writing");
+ if (!otp.isWritten())
+ writeObject(otp);
+ }
+ }
+ }
+
+ private void writeObject(final ObjectToPack otp) throws IOException {
+ otp.markWantWrite();
+ if (otp.isDeltaRepresentation()) {
+ ObjectToPack deltaBase = otp.getDeltaBase();
+ assert deltaBase != null || thin;
+ if (deltaBase != null && !deltaBase.isWritten()) {
+ if (deltaBase.wantWrite()) {
+ otp.clearDeltaBase(); // cycle detected
+ otp.disposeLoader();
+ } else {
+ writeObject(deltaBase);
+ }
+ }
+
+ otp.updateDeltaDepth();
+ if (otp.getDeltaDepth() > maxDeltaDepth) {
+ otp.clearDeltaBase();
+ otp.disposeLoader();
+ }
+ }
+
+ assert !otp.isWritten();
+
+ otp.markWritten(countingOut.getCount());
+ if (otp.isDeltaRepresentation())
+ writeDeltaObject(otp);
+ else
+ writeWholeObject(otp);
+
+ monitor.update(1);
+ }
+
+ private void writeWholeObject(final ObjectToPack otp) throws IOException {
+ if (otp.hasReuseLoader()) {
+ final PackedObjectLoader loader = otp.getReuseLoader();
+ writeObjectHeader(loader.getType(), loader.getSize());
+ loader.copyRawData(out, buf);
+ otp.disposeLoader();
+ } else {
+ final ObjectLoader loader = db.openObject(windowCursor, otp);
+ final DeflaterOutputStream deflaterOut = new DeflaterOutputStream(
+ out, deflater);
+ writeObjectHeader(loader.getType(), loader.getSize());
+ deflaterOut.write(loader.getCachedBytes());
+ deflaterOut.finish();
+ deflater.reset();
+ }
+ }
+
+ private void writeDeltaObject(final ObjectToPack otp) throws IOException {
+ final PackedObjectLoader loader = otp.getReuseLoader();
+ if (deltaBaseAsOffset && otp.getDeltaBase() != null) {
+ writeObjectHeader(Constants.OBJ_OFS_DELTA, loader.getRawSize());
+
+ final ObjectToPack deltaBase = otp.getDeltaBase();
+ long offsetDiff = otp.getOffset() - deltaBase.getOffset();
+ int pos = buf.length - 1;
+ buf[pos] = (byte) (offsetDiff & 0x7F);
+ while ((offsetDiff >>= 7) > 0) {
+ buf[--pos] = (byte) (0x80 | (--offsetDiff & 0x7F));
+ }
+
+ out.write(buf, pos, buf.length - pos);
+ } else {
+ writeObjectHeader(Constants.OBJ_REF_DELTA, loader.getRawSize());
+ otp.getDeltaBaseId().copyRawTo(buf, 0);
+ out.write(buf, 0, Constants.OBJECT_ID_LENGTH);
+ }
+ loader.copyRawData(out, buf);
+ otp.disposeLoader();
+ }
+
+ private void writeObjectHeader(final int objectType, long dataLength)
+ throws IOException {
+ long nextLength = dataLength >>> 4;
+ int size = 0;
+ buf[size++] = (byte) ((nextLength > 0 ? 0x80 : 0x00)
+ | (objectType << 4) | (dataLength & 0x0F));
+ dataLength = nextLength;
+ while (dataLength > 0) {
+ nextLength >>>= 7;
+ buf[size++] = (byte) ((nextLength > 0 ? 0x80 : 0x00) | (dataLength & 0x7F));
+ dataLength = nextLength;
+ }
+ out.write(buf, 0, size);
+ }
+
+ private void writeChecksum() throws IOException {
+ out.on(false);
+ final byte checksum[] = out.getMessageDigest().digest();
+ out.write(checksum);
+ }
+
+ private ObjectWalk setUpWalker(
+ final Collection interestingObjects,
+ final Collection uninterestingObjects, boolean thin)
+ throws MissingObjectException, IOException,
+ IncorrectObjectTypeException {
+ final ObjectWalk walker = new ObjectWalk(db);
+ walker.sort(RevSort.TOPO, true);
+ walker.sort(RevSort.COMMIT_TIME_DESC, true);
+ if (thin)
+ walker.sort(RevSort.BOUNDARY);
+
+ for (ObjectId id : interestingObjects) {
+ RevObject o = walker.parseAny(id);
+ walker.markStart(o);
+ }
+ for (ObjectId id : uninterestingObjects) {
+ RevObject o = walker.parseAny(id);
+ walker.markUninteresting(o);
+ }
+ return walker;
+ }
+
+ private void findObjectsToPack(final ObjectWalk walker)
+ throws MissingObjectException, IncorrectObjectTypeException,
+ IOException {
+ monitor.beginTask(COUNTING_OBJECTS_PROGRESS, ProgressMonitor.UNKNOWN);
+ RevObject o;
+
+ while ((o = walker.next()) != null) {
+ addObject(o);
+ monitor.update(1);
+ }
+ while ((o = walker.nextObject()) != null) {
+ addObject(o);
+ monitor.update(1);
+ }
+ monitor.endTask();
+ }
+
+ private void addObject(RevObject object)
+ throws IncorrectObjectTypeException {
+ if (object.has(RevFlag.UNINTERESTING)) {
+ edgeObjects.add(object);
+ thin = true;
+ return;
+ }
+
+ final ObjectToPack otp = new ObjectToPack(object);
+ try {
+ objectsLists[object.getType()].add(otp);
+ } catch (ArrayIndexOutOfBoundsException x) {
+ throw new IncorrectObjectTypeException(object,
+ "COMMIT nor TREE nor BLOB nor TAG");
+ } catch (UnsupportedOperationException x) {
+ // index pointing to "dummy" empty list
+ throw new IncorrectObjectTypeException(object,
+ "COMMIT nor TREE nor BLOB nor TAG");
+ }
+ objectsMap.add(otp);
+ }
+
+ /**
+ * Class holding information about object that is going to be packed by
+ * {@link PackWriter}. Information include object representation in a
+ * pack-file and object status.
+ *
+ */
+ static class ObjectToPack extends ObjectId {
+ private ObjectId deltaBase;
+
+ private PackedObjectLoader reuseLoader;
+
+ private long offset = -1;
+
+ private int deltaDepth;
+
+ private boolean wantWrite;
+
+ /**
+ * Construct object for specified object id.
By default object is
+ * marked as not written and non-delta packed (as a whole object).
+ *
+ * @param src
+ * object id of object for packing
+ */
+ ObjectToPack(AnyObjectId src) {
+ super(src);
+ }
+
+ /**
+ * @return delta base object id if object is going to be packed in delta
+ * representation; null otherwise - if going to be packed as a
+ * whole object.
+ */
+ ObjectId getDeltaBaseId() {
+ return deltaBase;
+ }
+
+ /**
+ * @return delta base object to pack if object is going to be packed in
+ * delta representation and delta is specified as object to
+ * pack; null otherwise - if going to be packed as a whole
+ * object or delta base is specified only as id.
+ */
+ ObjectToPack getDeltaBase() {
+ if (deltaBase instanceof ObjectToPack)
+ return (ObjectToPack) deltaBase;
+ return null;
+ }
+
+ /**
+ * Set delta base for the object. Delta base set by this method is used
+ * by {@link PackWriter} to write object - determines its representation
+ * in a created pack.
+ *
+ * @param deltaBase
+ * delta base object or null if object should be packed as a
+ * whole object.
+ *
+ */
+ void setDeltaBase(ObjectId deltaBase) {
+ this.deltaBase = deltaBase;
+ }
+
+ void clearDeltaBase() {
+ this.deltaBase = null;
+ }
+
+ /**
+ * @return true if object is going to be written as delta; false
+ * otherwise.
+ */
+ boolean isDeltaRepresentation() {
+ return deltaBase != null;
+ }
+
+ /**
+ * Check if object is already written in a pack. This information is
+ * used to achieve delta-base precedence in a pack file.
+ *
+ * @return true if object is already written; false otherwise.
+ */
+ boolean isWritten() {
+ return offset != -1;
+ }
+
+ /**
+ * @return offset in pack when object has been already written, or -1 if
+ * it has not been written yet
+ */
+ long getOffset() {
+ return offset;
+ }
+
+ /**
+ * Mark object as written. This information is used to achieve
+ * delta-base precedence in a pack file.
+ *
+ * @param offset
+ * offset where written object starts
+ */
+ void markWritten(long offset) {
+ this.offset = offset;
+ }
+
+ PackedObjectLoader getReuseLoader() {
+ return reuseLoader;
+ }
+
+ boolean hasReuseLoader() {
+ return reuseLoader != null;
+ }
+
+ void setReuseLoader(PackedObjectLoader reuseLoader) {
+ this.reuseLoader = reuseLoader;
+ }
+
+ void disposeLoader() {
+ this.reuseLoader = null;
+ }
+
+ int getDeltaDepth() {
+ return deltaDepth;
+ }
+
+ void updateDeltaDepth() {
+ if (deltaBase instanceof ObjectToPack)
+ deltaDepth = ((ObjectToPack) deltaBase).deltaDepth + 1;
+ else if (deltaBase != null)
+ deltaDepth = 1;
+ }
+
+ boolean wantWrite() {
+ return wantWrite;
+ }
+
+ void markWantWrite() {
+ this.wantWrite = true;
+ }
+ }
+}
--
1.5.5.1