GC.java

  1. /*
  2.  * Copyright (C) 2012, Christian Halstrick <christian.halstrick@sap.com>
  3.  * Copyright (C) 2011, Shawn O. Pearce <spearce@spearce.org> and others
  4.  *
  5.  * This program and the accompanying materials are made available under the
  6.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  7.  * https://www.eclipse.org/org/documents/edl-v10.php.
  8.  *
  9.  * SPDX-License-Identifier: BSD-3-Clause
  10.  */
  11. package org.eclipse.jgit.internal.storage.file;

  12. import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
  13. import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
  14. import static org.eclipse.jgit.internal.storage.pack.PackExt.KEEP;
  15. import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;

  16. import java.io.File;
  17. import java.io.FileOutputStream;
  18. import java.io.IOException;
  19. import java.io.OutputStream;
  20. import java.io.PrintWriter;
  21. import java.io.StringWriter;
  22. import java.nio.channels.Channels;
  23. import java.nio.channels.FileChannel;
  24. import java.nio.file.DirectoryNotEmptyException;
  25. import java.nio.file.DirectoryStream;
  26. import java.nio.file.Files;
  27. import java.nio.file.Path;
  28. import java.nio.file.StandardCopyOption;
  29. import java.text.MessageFormat;
  30. import java.text.ParseException;
  31. import java.time.Instant;
  32. import java.time.temporal.ChronoUnit;
  33. import java.util.ArrayList;
  34. import java.util.Collection;
  35. import java.util.Collections;
  36. import java.util.Comparator;
  37. import java.util.Date;
  38. import java.util.HashMap;
  39. import java.util.HashSet;
  40. import java.util.Iterator;
  41. import java.util.LinkedList;
  42. import java.util.List;
  43. import java.util.Map;
  44. import java.util.Objects;
  45. import java.util.Set;
  46. import java.util.TreeMap;
  47. import java.util.concurrent.CompletableFuture;
  48. import java.util.concurrent.ExecutorService;
  49. import java.util.function.Supplier;
  50. import java.util.regex.Pattern;
  51. import java.util.stream.Collectors;
  52. import java.util.stream.Stream;

  53. import org.eclipse.jgit.annotations.NonNull;
  54. import org.eclipse.jgit.dircache.DirCacheIterator;
  55. import org.eclipse.jgit.errors.CancelledException;
  56. import org.eclipse.jgit.errors.CorruptObjectException;
  57. import org.eclipse.jgit.errors.IncorrectObjectTypeException;
  58. import org.eclipse.jgit.errors.MissingObjectException;
  59. import org.eclipse.jgit.errors.NoWorkTreeException;
  60. import org.eclipse.jgit.internal.JGitText;
  61. import org.eclipse.jgit.internal.storage.pack.PackExt;
  62. import org.eclipse.jgit.internal.storage.pack.PackWriter;
  63. import org.eclipse.jgit.lib.ConfigConstants;
  64. import org.eclipse.jgit.lib.Constants;
  65. import org.eclipse.jgit.lib.FileMode;
  66. import org.eclipse.jgit.lib.NullProgressMonitor;
  67. import org.eclipse.jgit.lib.ObjectId;
  68. import org.eclipse.jgit.lib.ObjectIdSet;
  69. import org.eclipse.jgit.lib.ObjectLoader;
  70. import org.eclipse.jgit.lib.ObjectReader;
  71. import org.eclipse.jgit.lib.ProgressMonitor;
  72. import org.eclipse.jgit.lib.Ref;
  73. import org.eclipse.jgit.lib.Ref.Storage;
  74. import org.eclipse.jgit.lib.RefDatabase;
  75. import org.eclipse.jgit.lib.ReflogEntry;
  76. import org.eclipse.jgit.lib.ReflogReader;
  77. import org.eclipse.jgit.lib.internal.WorkQueue;
  78. import org.eclipse.jgit.revwalk.ObjectWalk;
  79. import org.eclipse.jgit.revwalk.RevObject;
  80. import org.eclipse.jgit.revwalk.RevWalk;
  81. import org.eclipse.jgit.storage.pack.PackConfig;
  82. import org.eclipse.jgit.treewalk.TreeWalk;
  83. import org.eclipse.jgit.treewalk.filter.TreeFilter;
  84. import org.eclipse.jgit.util.FileUtils;
  85. import org.eclipse.jgit.util.GitDateParser;
  86. import org.eclipse.jgit.util.SystemReader;
  87. import org.slf4j.Logger;
  88. import org.slf4j.LoggerFactory;

  89. /**
  90.  * A garbage collector for git
  91.  * {@link org.eclipse.jgit.internal.storage.file.FileRepository}. Instances of
  92.  * this class are not thread-safe. Don't use the same instance from multiple
  93.  * threads.
  94.  *
  95.  * This class started as a copy of DfsGarbageCollector from Shawn O. Pearce
  96.  * adapted to FileRepositories.
  97.  */
  98. public class GC {
  99.     private static final Logger LOG = LoggerFactory
  100.             .getLogger(GC.class);

  101.     private static final String PRUNE_EXPIRE_DEFAULT = "2.weeks.ago"; //$NON-NLS-1$

  102.     private static final String PRUNE_PACK_EXPIRE_DEFAULT = "1.hour.ago"; //$NON-NLS-1$

  103.     private static final Pattern PATTERN_LOOSE_OBJECT = Pattern
  104.             .compile("[0-9a-fA-F]{38}"); //$NON-NLS-1$

  105.     private static final String PACK_EXT = "." + PackExt.PACK.getExtension();//$NON-NLS-1$

  106.     private static final String BITMAP_EXT = "." //$NON-NLS-1$
  107.             + PackExt.BITMAP_INDEX.getExtension();

  108.     private static final String INDEX_EXT = "." + PackExt.INDEX.getExtension(); //$NON-NLS-1$

  109.     private static final String KEEP_EXT = "." + PackExt.KEEP.getExtension(); //$NON-NLS-1$

  110.     private static final int DEFAULT_AUTOPACKLIMIT = 50;

  111.     private static final int DEFAULT_AUTOLIMIT = 6700;

  112.     private static volatile ExecutorService executor;

  113.     /**
  114.      * Set the executor for running auto-gc in the background. If no executor is
  115.      * set JGit's own WorkQueue will be used.
  116.      *
  117.      * @param e
  118.      *            the executor to be used for running auto-gc
  119.      */
  120.     public static void setExecutor(ExecutorService e) {
  121.         executor = e;
  122.     }

  123.     private final FileRepository repo;

  124.     private ProgressMonitor pm;

  125.     private long expireAgeMillis = -1;

  126.     private Date expire;

  127.     private long packExpireAgeMillis = -1;

  128.     private Date packExpire;

  129.     private PackConfig pconfig;

  130.     /**
  131.      * the refs which existed during the last call to {@link #repack()}. This is
  132.      * needed during {@link #prune(Set)} where we can optimize by looking at the
  133.      * difference between the current refs and the refs which existed during
  134.      * last {@link #repack()}.
  135.      */
  136.     private Collection<Ref> lastPackedRefs;

  137.     /**
  138.      * Holds the starting time of the last repack() execution. This is needed in
  139.      * prune() to inspect only those reflog entries which have been added since
  140.      * last repack().
  141.      */
  142.     private long lastRepackTime;

  143.     /**
  144.      * Whether gc should do automatic housekeeping
  145.      */
  146.     private boolean automatic;

  147.     /**
  148.      * Whether to run gc in a background thread
  149.      */
  150.     private boolean background;

  151.     /**
  152.      * Creates a new garbage collector with default values. An expirationTime of
  153.      * two weeks and <code>null</code> as progress monitor will be used.
  154.      *
  155.      * @param repo
  156.      *            the repo to work on
  157.      */
  158.     public GC(FileRepository repo) {
  159.         this.repo = repo;
  160.         this.pconfig = new PackConfig(repo);
  161.         this.pm = NullProgressMonitor.INSTANCE;
  162.     }

  163.     /**
  164.      * Runs a garbage collector on a
  165.      * {@link org.eclipse.jgit.internal.storage.file.FileRepository}. It will
  166.      * <ul>
  167.      * <li>pack loose references into packed-refs</li>
  168.      * <li>repack all reachable objects into new pack files and delete the old
  169.      * pack files</li>
  170.      * <li>prune all loose objects which are now reachable by packs</li>
  171.      * </ul>
  172.      *
  173.      * If {@link #setAuto(boolean)} was set to {@code true} {@code gc} will
  174.      * first check whether any housekeeping is required; if not, it exits
  175.      * without performing any work.
  176.      *
  177.      * If {@link #setBackground(boolean)} was set to {@code true}
  178.      * {@code collectGarbage} will start the gc in the background, and then
  179.      * return immediately. In this case, errors will not be reported except in
  180.      * gc.log.
  181.      *
  182.      * @return the collection of
  183.      *         {@link org.eclipse.jgit.internal.storage.file.Pack}'s which
  184.      *         are newly created
  185.      * @throws java.io.IOException
  186.      * @throws java.text.ParseException
  187.      *             If the configuration parameter "gc.pruneexpire" couldn't be
  188.      *             parsed
  189.      */
  190.     public CompletableFuture<Collection<Pack>> gc()
  191.             throws IOException, ParseException {
  192.         if (!background) {
  193.             return CompletableFuture.completedFuture(doGc());
  194.         }
  195.         final GcLog gcLog = new GcLog(repo);
  196.         if (!gcLog.lock()) {
  197.             // there is already a background gc running
  198.             return CompletableFuture.completedFuture(Collections.emptyList());
  199.         }

  200.         Supplier<Collection<Pack>> gcTask = () -> {
  201.             try {
  202.                 Collection<Pack> newPacks = doGc();
  203.                 if (automatic && tooManyLooseObjects()) {
  204.                     String message = JGitText.get().gcTooManyUnpruned;
  205.                     gcLog.write(message);
  206.                     gcLog.commit();
  207.                 }
  208.                 return newPacks;
  209.             } catch (IOException | ParseException e) {
  210.                 try {
  211.                     gcLog.write(e.getMessage());
  212.                     StringWriter sw = new StringWriter();
  213.                     e.printStackTrace(new PrintWriter(sw));
  214.                     gcLog.write(sw.toString());
  215.                     gcLog.commit();
  216.                 } catch (IOException e2) {
  217.                     e2.addSuppressed(e);
  218.                     LOG.error(e2.getMessage(), e2);
  219.                 }
  220.             } finally {
  221.                 gcLog.unlock();
  222.             }
  223.             return Collections.emptyList();
  224.         };
  225.         return CompletableFuture.supplyAsync(gcTask, executor());
  226.     }

  227.     private ExecutorService executor() {
  228.         return (executor != null) ? executor : WorkQueue.getExecutor();
  229.     }

  230.     private Collection<Pack> doGc() throws IOException, ParseException {
  231.         if (automatic && !needGc()) {
  232.             return Collections.emptyList();
  233.         }
  234.         pm.start(6 /* tasks */);
  235.         packRefs();
  236.         // TODO: implement reflog_expire(pm, repo);
  237.         Collection<Pack> newPacks = repack();
  238.         prune(Collections.emptySet());
  239.         // TODO: implement rerere_gc(pm);
  240.         return newPacks;
  241.     }

  242.     /**
  243.      * Loosen objects in a pack file which are not also in the newly-created
  244.      * pack files.
  245.      *
  246.      * @param inserter
  247.      * @param reader
  248.      * @param pack
  249.      * @param existing
  250.      * @throws IOException
  251.      */
  252.     private void loosen(ObjectDirectoryInserter inserter, ObjectReader reader, Pack pack, HashSet<ObjectId> existing)
  253.             throws IOException {
  254.         for (PackIndex.MutableEntry entry : pack) {
  255.             ObjectId oid = entry.toObjectId();
  256.             if (existing.contains(oid)) {
  257.                 continue;
  258.             }
  259.             existing.add(oid);
  260.             ObjectLoader loader = reader.open(oid);
  261.             inserter.insert(loader.getType(),
  262.                     loader.getSize(),
  263.                     loader.openStream(),
  264.                     true /* create this object even though it's a duplicate */);
  265.         }
  266.     }

  267.     /**
  268.      * Delete old pack files. What is 'old' is defined by specifying a set of
  269.      * old pack files and a set of new pack files. Each pack file contained in
  270.      * old pack files but not contained in new pack files will be deleted. If
  271.      * preserveOldPacks is set, keep a copy of the pack file in the preserve
  272.      * directory. If an expirationDate is set then pack files which are younger
  273.      * than the expirationDate will not be deleted nor preserved.
  274.      * <p>
  275.      * If we're not immediately expiring loose objects, loosen any objects
  276.      * in the old pack files which aren't in the new pack files.
  277.      *
  278.      * @param oldPacks
  279.      * @param newPacks
  280.      * @throws ParseException
  281.      * @throws IOException
  282.      */
  283.     private void deleteOldPacks(Collection<Pack> oldPacks,
  284.             Collection<Pack> newPacks) throws ParseException, IOException {
  285.         HashSet<ObjectId> ids = new HashSet<>();
  286.         for (Pack pack : newPacks) {
  287.             for (PackIndex.MutableEntry entry : pack) {
  288.                 ids.add(entry.toObjectId());
  289.             }
  290.         }
  291.         ObjectReader reader = repo.newObjectReader();
  292.         ObjectDirectory dir = repo.getObjectDatabase();
  293.         ObjectDirectoryInserter inserter = dir.newInserter();
  294.         boolean shouldLoosen = !"now".equals(getPruneExpireStr()) && //$NON-NLS-1$
  295.             getExpireDate() < Long.MAX_VALUE;

  296.         prunePreserved();
  297.         long packExpireDate = getPackExpireDate();
  298.         oldPackLoop: for (Pack oldPack : oldPacks) {
  299.             checkCancelled();
  300.             String oldName = oldPack.getPackName();
  301.             // check whether an old pack file is also among the list of new
  302.             // pack files. Then we must not delete it.
  303.             for (Pack newPack : newPacks)
  304.                 if (oldName.equals(newPack.getPackName()))
  305.                     continue oldPackLoop;

  306.             if (!oldPack.shouldBeKept()
  307.                     && repo.getFS()
  308.                             .lastModifiedInstant(oldPack.getPackFile())
  309.                             .toEpochMilli() < packExpireDate) {
  310.                 if (shouldLoosen) {
  311.                     loosen(inserter, reader, oldPack, ids);
  312.                 }
  313.                 oldPack.close();
  314.                 prunePack(oldPack.getPackFile());
  315.             }
  316.         }

  317.         // close the complete object database. That's my only chance to force
  318.         // rescanning and to detect that certain pack files are now deleted.
  319.         repo.getObjectDatabase().close();
  320.     }

  321.     /**
  322.      * Deletes old pack file, unless 'preserve-oldpacks' is set, in which case it
  323.      * moves the pack file to the preserved directory
  324.      *
  325.      * @param packFile
  326.      * @param deleteOptions
  327.      * @throws IOException
  328.      */
  329.     private void removeOldPack(PackFile packFile, int deleteOptions)
  330.             throws IOException {
  331.         if (pconfig.isPreserveOldPacks()) {
  332.             File oldPackDir = repo.getObjectDatabase().getPreservedDirectory();
  333.             FileUtils.mkdir(oldPackDir, true);

  334.             PackFile oldPackFile = packFile
  335.                     .createPreservedForDirectory(oldPackDir);
  336.             FileUtils.rename(packFile, oldPackFile);
  337.         } else {
  338.             FileUtils.delete(packFile, deleteOptions);
  339.         }
  340.     }

  341.     /**
  342.      * Delete the preserved directory including all pack files within
  343.      */
  344.     private void prunePreserved() {
  345.         if (pconfig.isPrunePreserved()) {
  346.             try {
  347.                 FileUtils.delete(repo.getObjectDatabase().getPreservedDirectory(),
  348.                         FileUtils.RECURSIVE | FileUtils.RETRY | FileUtils.SKIP_MISSING);
  349.             } catch (IOException e) {
  350.                 // Deletion of the preserved pack files failed. Silently return.
  351.             }
  352.         }
  353.     }

  354.     /**
  355.      * Delete files associated with a single pack file. First try to delete the
  356.      * ".pack" file because on some platforms the ".pack" file may be locked and
  357.      * can't be deleted. In such a case it is better to detect this early and
  358.      * give up on deleting files for this packfile. Otherwise we may delete the
  359.      * ".index" file and when failing to delete the ".pack" file we are left
  360.      * with a ".pack" file without a ".index" file.
  361.      *
  362.      * @param packFile
  363.      */
  364.     private void prunePack(PackFile packFile) {
  365.         try {
  366.             // Delete the .pack file first and if this fails give up on deleting
  367.             // the other files
  368.             int deleteOptions = FileUtils.RETRY | FileUtils.SKIP_MISSING;
  369.             removeOldPack(packFile.create(PackExt.PACK), deleteOptions);

  370.             // The .pack file has been deleted. Delete as many as the other
  371.             // files as you can.
  372.             deleteOptions |= FileUtils.IGNORE_ERRORS;
  373.             for (PackExt ext : PackExt.values()) {
  374.                 if (!PackExt.PACK.equals(ext)) {
  375.                     removeOldPack(packFile.create(ext), deleteOptions);
  376.                 }
  377.             }
  378.         } catch (IOException e) {
  379.             // Deletion of the .pack file failed. Silently return.
  380.         }
  381.     }

  382.     /**
  383.      * Like "git prune-packed" this method tries to prune all loose objects
  384.      * which can be found in packs. If certain objects can't be pruned (e.g.
  385.      * because the filesystem delete operation fails) this is silently ignored.
  386.      *
  387.      * @throws java.io.IOException
  388.      */
  389.     public void prunePacked() throws IOException {
  390.         ObjectDirectory objdb = repo.getObjectDatabase();
  391.         Collection<Pack> packs = objdb.getPacks();
  392.         File objects = repo.getObjectsDirectory();
  393.         String[] fanout = objects.list();

  394.         if (fanout != null && fanout.length > 0) {
  395.             pm.beginTask(JGitText.get().pruneLoosePackedObjects, fanout.length);
  396.             try {
  397.                 for (String d : fanout) {
  398.                     checkCancelled();
  399.                     pm.update(1);
  400.                     if (d.length() != 2)
  401.                         continue;
  402.                     String[] entries = new File(objects, d).list();
  403.                     if (entries == null)
  404.                         continue;
  405.                     for (String e : entries) {
  406.                         checkCancelled();
  407.                         if (e.length() != Constants.OBJECT_ID_STRING_LENGTH - 2)
  408.                             continue;
  409.                         ObjectId id;
  410.                         try {
  411.                             id = ObjectId.fromString(d + e);
  412.                         } catch (IllegalArgumentException notAnObject) {
  413.                             // ignoring the file that does not represent loose
  414.                             // object
  415.                             continue;
  416.                         }
  417.                         boolean found = false;
  418.                         for (Pack p : packs) {
  419.                             checkCancelled();
  420.                             if (p.hasObject(id)) {
  421.                                 found = true;
  422.                                 break;
  423.                             }
  424.                         }
  425.                         if (found)
  426.                             FileUtils.delete(objdb.fileFor(id), FileUtils.RETRY
  427.                                     | FileUtils.SKIP_MISSING
  428.                                     | FileUtils.IGNORE_ERRORS);
  429.                     }
  430.                 }
  431.             } finally {
  432.                 pm.endTask();
  433.             }
  434.         }
  435.     }

  436.     /**
  437.      * Like "git prune" this method tries to prune all loose objects which are
  438.      * unreferenced. If certain objects can't be pruned (e.g. because the
  439.      * filesystem delete operation fails) this is silently ignored.
  440.      *
  441.      * @param objectsToKeep
  442.      *            a set of objects which should explicitly not be pruned
  443.      * @throws java.io.IOException
  444.      * @throws java.text.ParseException
  445.      *             If the configuration parameter "gc.pruneexpire" couldn't be
  446.      *             parsed
  447.      */
  448.     public void prune(Set<ObjectId> objectsToKeep) throws IOException,
  449.             ParseException {
  450.         long expireDate = getExpireDate();

  451.         // Collect all loose objects which are old enough, not referenced from
  452.         // the index and not in objectsToKeep
  453.         Map<ObjectId, File> deletionCandidates = new HashMap<>();
  454.         Set<ObjectId> indexObjects = null;
  455.         File objects = repo.getObjectsDirectory();
  456.         String[] fanout = objects.list();
  457.         if (fanout == null || fanout.length == 0) {
  458.             return;
  459.         }
  460.         pm.beginTask(JGitText.get().pruneLooseUnreferencedObjects,
  461.                 fanout.length);
  462.         try {
  463.             for (String d : fanout) {
  464.                 checkCancelled();
  465.                 pm.update(1);
  466.                 if (d.length() != 2)
  467.                     continue;
  468.                 File dir = new File(objects, d);
  469.                 File[] entries = dir.listFiles();
  470.                 if (entries == null || entries.length == 0) {
  471.                     FileUtils.delete(dir, FileUtils.IGNORE_ERRORS);
  472.                     continue;
  473.                 }
  474.                 for (File f : entries) {
  475.                     checkCancelled();
  476.                     String fName = f.getName();
  477.                     if (fName.length() != Constants.OBJECT_ID_STRING_LENGTH - 2)
  478.                         continue;
  479.                     if (repo.getFS().lastModifiedInstant(f)
  480.                             .toEpochMilli() >= expireDate) {
  481.                         continue;
  482.                     }
  483.                     try {
  484.                         ObjectId id = ObjectId.fromString(d + fName);
  485.                         if (objectsToKeep.contains(id))
  486.                             continue;
  487.                         if (indexObjects == null)
  488.                             indexObjects = listNonHEADIndexObjects();
  489.                         if (indexObjects.contains(id))
  490.                             continue;
  491.                         deletionCandidates.put(id, f);
  492.                     } catch (IllegalArgumentException notAnObject) {
  493.                         // ignoring the file that does not represent loose
  494.                         // object
  495.                     }
  496.                 }
  497.             }
  498.         } finally {
  499.             pm.endTask();
  500.         }

  501.         if (deletionCandidates.isEmpty()) {
  502.             return;
  503.         }

  504.         checkCancelled();

  505.         // From the set of current refs remove all those which have been handled
  506.         // during last repack(). Only those refs will survive which have been
  507.         // added or modified since the last repack. Only these can save existing
  508.         // loose refs from being pruned.
  509.         Collection<Ref> newRefs;
  510.         if (lastPackedRefs == null || lastPackedRefs.isEmpty())
  511.             newRefs = getAllRefs();
  512.         else {
  513.             Map<String, Ref> last = new HashMap<>();
  514.             for (Ref r : lastPackedRefs) {
  515.                 last.put(r.getName(), r);
  516.             }
  517.             newRefs = new ArrayList<>();
  518.             for (Ref r : getAllRefs()) {
  519.                 Ref old = last.get(r.getName());
  520.                 if (!equals(r, old)) {
  521.                     newRefs.add(r);
  522.                 }
  523.             }
  524.         }

  525.         if (!newRefs.isEmpty()) {
  526.             // There are new/modified refs! Check which loose objects are now
  527.             // referenced by these modified refs (or their reflogentries).
  528.             // Remove these loose objects
  529.             // from the deletionCandidates. When the last candidate is removed
  530.             // leave this method.
  531.             ObjectWalk w = new ObjectWalk(repo);
  532.             try {
  533.                 for (Ref cr : newRefs) {
  534.                     checkCancelled();
  535.                     w.markStart(w.parseAny(cr.getObjectId()));
  536.                 }
  537.                 if (lastPackedRefs != null)
  538.                     for (Ref lpr : lastPackedRefs) {
  539.                         w.markUninteresting(w.parseAny(lpr.getObjectId()));
  540.                     }
  541.                 removeReferenced(deletionCandidates, w);
  542.             } finally {
  543.                 w.dispose();
  544.             }
  545.         }

  546.         if (deletionCandidates.isEmpty())
  547.             return;

  548.         // Since we have not left the method yet there are still
  549.         // deletionCandidates. Last chance for these objects not to be pruned is
  550.         // that they are referenced by reflog entries. Even refs which currently
  551.         // point to the same object as during last repack() may have
  552.         // additional reflog entries not handled during last repack()
  553.         ObjectWalk w = new ObjectWalk(repo);
  554.         try {
  555.             for (Ref ar : getAllRefs())
  556.                 for (ObjectId id : listRefLogObjects(ar, lastRepackTime)) {
  557.                     checkCancelled();
  558.                     w.markStart(w.parseAny(id));
  559.                 }
  560.             if (lastPackedRefs != null)
  561.                 for (Ref lpr : lastPackedRefs) {
  562.                     checkCancelled();
  563.                     w.markUninteresting(w.parseAny(lpr.getObjectId()));
  564.                 }
  565.             removeReferenced(deletionCandidates, w);
  566.         } finally {
  567.             w.dispose();
  568.         }

  569.         if (deletionCandidates.isEmpty())
  570.             return;

  571.         checkCancelled();

  572.         // delete all candidates which have survived: these are unreferenced
  573.         // loose objects. Make a last check, though, to avoid deleting objects
  574.         // that could have been referenced while the candidates list was being
  575.         // built (by an incoming push, for example).
  576.         Set<File> touchedFanout = new HashSet<>();
  577.         for (File f : deletionCandidates.values()) {
  578.             if (f.lastModified() < expireDate) {
  579.                 f.delete();
  580.                 touchedFanout.add(f.getParentFile());
  581.             }
  582.         }

  583.         for (File f : touchedFanout) {
  584.             FileUtils.delete(f,
  585.                     FileUtils.EMPTY_DIRECTORIES_ONLY | FileUtils.IGNORE_ERRORS);
  586.         }

  587.         repo.getObjectDatabase().close();
  588.     }

  589.     private long getExpireDate() throws ParseException {
  590.         long expireDate = Long.MAX_VALUE;

  591.         if (expire == null && expireAgeMillis == -1) {
  592.             String pruneExpireStr = getPruneExpireStr();
  593.             if (pruneExpireStr == null)
  594.                 pruneExpireStr = PRUNE_EXPIRE_DEFAULT;
  595.             expire = GitDateParser.parse(pruneExpireStr, null, SystemReader
  596.                     .getInstance().getLocale());
  597.             expireAgeMillis = -1;
  598.         }
  599.         if (expire != null)
  600.             expireDate = expire.getTime();
  601.         if (expireAgeMillis != -1)
  602.             expireDate = System.currentTimeMillis() - expireAgeMillis;
  603.         return expireDate;
  604.     }

  605.     private String getPruneExpireStr() {
  606.         return repo.getConfig().getString(
  607.                         ConfigConstants.CONFIG_GC_SECTION, null,
  608.                         ConfigConstants.CONFIG_KEY_PRUNEEXPIRE);
  609.     }

  610.     private long getPackExpireDate() throws ParseException {
  611.         long packExpireDate = Long.MAX_VALUE;

  612.         if (packExpire == null && packExpireAgeMillis == -1) {
  613.             String prunePackExpireStr = repo.getConfig().getString(
  614.                     ConfigConstants.CONFIG_GC_SECTION, null,
  615.                     ConfigConstants.CONFIG_KEY_PRUNEPACKEXPIRE);
  616.             if (prunePackExpireStr == null)
  617.                 prunePackExpireStr = PRUNE_PACK_EXPIRE_DEFAULT;
  618.             packExpire = GitDateParser.parse(prunePackExpireStr, null,
  619.                     SystemReader.getInstance().getLocale());
  620.             packExpireAgeMillis = -1;
  621.         }
  622.         if (packExpire != null)
  623.             packExpireDate = packExpire.getTime();
  624.         if (packExpireAgeMillis != -1)
  625.             packExpireDate = System.currentTimeMillis() - packExpireAgeMillis;
  626.         return packExpireDate;
  627.     }

  628.     /**
  629.      * Remove all entries from a map which key is the id of an object referenced
  630.      * by the given ObjectWalk
  631.      *
  632.      * @param id2File
  633.      * @param w
  634.      * @throws MissingObjectException
  635.      * @throws IncorrectObjectTypeException
  636.      * @throws IOException
  637.      */
  638.     private void removeReferenced(Map<ObjectId, File> id2File,
  639.             ObjectWalk w) throws MissingObjectException,
  640.             IncorrectObjectTypeException, IOException {
  641.         RevObject ro = w.next();
  642.         while (ro != null) {
  643.             checkCancelled();
  644.             if (id2File.remove(ro.getId()) != null && id2File.isEmpty()) {
  645.                 return;
  646.             }
  647.             ro = w.next();
  648.         }
  649.         ro = w.nextObject();
  650.         while (ro != null) {
  651.             checkCancelled();
  652.             if (id2File.remove(ro.getId()) != null && id2File.isEmpty()) {
  653.                 return;
  654.             }
  655.             ro = w.nextObject();
  656.         }
  657.     }

  658.     private static boolean equals(Ref r1, Ref r2) {
  659.         if (r1 == null || r2 == null) {
  660.             return false;
  661.         }
  662.         if (r1.isSymbolic()) {
  663.             return r2.isSymbolic() && r1.getTarget().getName()
  664.                     .equals(r2.getTarget().getName());
  665.         }
  666.         return !r2.isSymbolic()
  667.                 && Objects.equals(r1.getObjectId(), r2.getObjectId());
  668.     }

  669.     /**
  670.      * Pack ref storage. For a RefDirectory database, this packs all
  671.      * non-symbolic, loose refs into packed-refs. For Reftable, all of the data
  672.      * is compacted into a single table.
  673.      *
  674.      * @throws java.io.IOException
  675.      */
  676.     public void packRefs() throws IOException {
  677.         RefDatabase refDb = repo.getRefDatabase();
  678.         if (refDb instanceof FileReftableDatabase) {
  679.             // TODO: abstract this more cleanly.
  680.             pm.beginTask(JGitText.get().packRefs, 1);
  681.             try {
  682.                 ((FileReftableDatabase) refDb).compactFully();
  683.             } finally {
  684.                 pm.endTask();
  685.             }
  686.             return;
  687.         }

  688.         Collection<Ref> refs = refDb.getRefsByPrefix(Constants.R_REFS);
  689.         List<String> refsToBePacked = new ArrayList<>(refs.size());
  690.         pm.beginTask(JGitText.get().packRefs, refs.size());
  691.         try {
  692.             for (Ref ref : refs) {
  693.                 checkCancelled();
  694.                 if (!ref.isSymbolic() && ref.getStorage().isLoose())
  695.                     refsToBePacked.add(ref.getName());
  696.                 pm.update(1);
  697.             }
  698.             ((RefDirectory) repo.getRefDatabase()).pack(refsToBePacked);
  699.         } finally {
  700.             pm.endTask();
  701.         }
  702.     }

  703.     /**
  704.      * Packs all objects which reachable from any of the heads into one pack
  705.      * file. Additionally all objects which are not reachable from any head but
  706.      * which are reachable from any of the other refs (e.g. tags), special refs
  707.      * (e.g. FETCH_HEAD) or index are packed into a separate pack file. Objects
  708.      * included in pack files which have a .keep file associated are never
  709.      * repacked. All old pack files which existed before are deleted.
  710.      *
  711.      * @return a collection of the newly created pack files
  712.      * @throws java.io.IOException
  713.      *             when during reading of refs, index, packfiles, objects,
  714.      *             reflog-entries or during writing to the packfiles
  715.      *             {@link java.io.IOException} occurs
  716.      */
  717.     public Collection<Pack> repack() throws IOException {
  718.         Collection<Pack> toBeDeleted = repo.getObjectDatabase().getPacks();

  719.         long time = System.currentTimeMillis();
  720.         Collection<Ref> refsBefore = getAllRefs();

  721.         Set<ObjectId> allHeadsAndTags = new HashSet<>();
  722.         Set<ObjectId> allHeads = new HashSet<>();
  723.         Set<ObjectId> allTags = new HashSet<>();
  724.         Set<ObjectId> nonHeads = new HashSet<>();
  725.         Set<ObjectId> txnHeads = new HashSet<>();
  726.         Set<ObjectId> tagTargets = new HashSet<>();
  727.         Set<ObjectId> indexObjects = listNonHEADIndexObjects();

  728.         for (Ref ref : refsBefore) {
  729.             checkCancelled();
  730.             nonHeads.addAll(listRefLogObjects(ref, 0));
  731.             if (ref.isSymbolic() || ref.getObjectId() == null) {
  732.                 continue;
  733.             }
  734.             if (isHead(ref)) {
  735.                 allHeads.add(ref.getObjectId());
  736.             } else if (isTag(ref)) {
  737.                 allTags.add(ref.getObjectId());
  738.             } else {
  739.                 nonHeads.add(ref.getObjectId());
  740.             }
  741.             if (ref.getPeeledObjectId() != null) {
  742.                 tagTargets.add(ref.getPeeledObjectId());
  743.             }
  744.         }

  745.         List<ObjectIdSet> excluded = new LinkedList<>();
  746.         for (Pack p : repo.getObjectDatabase().getPacks()) {
  747.             checkCancelled();
  748.             if (p.shouldBeKept())
  749.                 excluded.add(p.getIndex());
  750.         }

  751.         // Don't exclude tags that are also branch tips
  752.         allTags.removeAll(allHeads);
  753.         allHeadsAndTags.addAll(allHeads);
  754.         allHeadsAndTags.addAll(allTags);

  755.         // Hoist all branch tips and tags earlier in the pack file
  756.         tagTargets.addAll(allHeadsAndTags);
  757.         nonHeads.addAll(indexObjects);

  758.         // Combine the GC_REST objects into the GC pack if requested
  759.         if (pconfig.getSinglePack()) {
  760.             allHeadsAndTags.addAll(nonHeads);
  761.             nonHeads.clear();
  762.         }

  763.         List<Pack> ret = new ArrayList<>(2);
  764.         Pack heads = null;
  765.         if (!allHeadsAndTags.isEmpty()) {
  766.             heads = writePack(allHeadsAndTags, PackWriter.NONE, allTags,
  767.                     tagTargets, excluded);
  768.             if (heads != null) {
  769.                 ret.add(heads);
  770.                 excluded.add(0, heads.getIndex());
  771.             }
  772.         }
  773.         if (!nonHeads.isEmpty()) {
  774.             Pack rest = writePack(nonHeads, allHeadsAndTags, PackWriter.NONE,
  775.                     tagTargets, excluded);
  776.             if (rest != null)
  777.                 ret.add(rest);
  778.         }
  779.         if (!txnHeads.isEmpty()) {
  780.             Pack txn = writePack(txnHeads, PackWriter.NONE, PackWriter.NONE,
  781.                     null, excluded);
  782.             if (txn != null)
  783.                 ret.add(txn);
  784.         }
  785.         try {
  786.             deleteOldPacks(toBeDeleted, ret);
  787.         } catch (ParseException e) {
  788.             // TODO: the exception has to be wrapped into an IOException because
  789.             // throwing the ParseException directly would break the API, instead
  790.             // we should throw a ConfigInvalidException
  791.             throw new IOException(e);
  792.         }
  793.         prunePacked();
  794.         if (repo.getRefDatabase() instanceof RefDirectory) {
  795.             // TODO: abstract this more cleanly.
  796.             deleteEmptyRefsFolders();
  797.         }
  798.         deleteOrphans();
  799.         deleteTempPacksIdx();

  800.         lastPackedRefs = refsBefore;
  801.         lastRepackTime = time;
  802.         return ret;
  803.     }

  804.     private static boolean isHead(Ref ref) {
  805.         return ref.getName().startsWith(Constants.R_HEADS);
  806.     }

  807.     private static boolean isTag(Ref ref) {
  808.         return ref.getName().startsWith(Constants.R_TAGS);
  809.     }

  810.     private void deleteEmptyRefsFolders() throws IOException {
  811.         Path refs = repo.getDirectory().toPath().resolve(Constants.R_REFS);
  812.         // Avoid deleting a folder that was created after the threshold so that concurrent
  813.         // operations trying to create a reference are not impacted
  814.         Instant threshold = Instant.now().minus(30, ChronoUnit.SECONDS);
  815.         try (Stream<Path> entries = Files.list(refs)
  816.                 .filter(Files::isDirectory)) {
  817.             Iterator<Path> iterator = entries.iterator();
  818.             while (iterator.hasNext()) {
  819.                 try (Stream<Path> s = Files.list(iterator.next())) {
  820.                     s.filter(path -> canBeSafelyDeleted(path, threshold)).forEach(this::deleteDir);
  821.                 }
  822.             }
  823.         }
  824.     }

  825.     private boolean canBeSafelyDeleted(Path path, Instant threshold) {
  826.         try {
  827.             return Files.getLastModifiedTime(path).toInstant().isBefore(threshold);
  828.         }
  829.         catch (IOException e) {
  830.             LOG.warn(MessageFormat.format(
  831.                     JGitText.get().cannotAccessLastModifiedForSafeDeletion,
  832.                     path), e);
  833.             return false;
  834.         }
  835.     }

  836.     private void deleteDir(Path dir) {
  837.         try (Stream<Path> dirs = Files.walk(dir)) {
  838.             dirs.filter(this::isDirectory).sorted(Comparator.reverseOrder())
  839.                     .forEach(this::delete);
  840.         } catch (IOException e) {
  841.             LOG.error(e.getMessage(), e);
  842.         }
  843.     }

  844.     private boolean isDirectory(Path p) {
  845.         return p.toFile().isDirectory();
  846.     }

  847.     private void delete(Path d) {
  848.         try {
  849.             Files.delete(d);
  850.         } catch (DirectoryNotEmptyException e) {
  851.             // Don't log
  852.         } catch (IOException e) {
  853.             LOG.error(MessageFormat.format(JGitText.get().cannotDeleteFile, d),
  854.                     e);
  855.         }
  856.     }

  857.     /**
  858.      * Deletes orphans
  859.      * <p>
  860.      * A file is considered an orphan if it is either a "bitmap" or an index
  861.      * file, and its corresponding pack file is missing in the list.
  862.      * </p>
  863.      */
  864.     private void deleteOrphans() {
  865.         Path packDir = repo.getObjectDatabase().getPackDirectory().toPath();
  866.         List<String> fileNames = null;
  867.         try (Stream<Path> files = Files.list(packDir)) {
  868.             fileNames = files.map(path -> path.getFileName().toString())
  869.                     .filter(name -> (name.endsWith(PACK_EXT)
  870.                             || name.endsWith(BITMAP_EXT)
  871.                             || name.endsWith(INDEX_EXT)
  872.                             || name.endsWith(KEEP_EXT)))
  873.                     // sort files with same base name in the order:
  874.                     // .pack, .keep, .index, .bitmap to avoid look ahead
  875.                     .sorted(Collections.reverseOrder())
  876.                     .collect(Collectors.toList());
  877.         } catch (IOException e) {
  878.             LOG.error(e.getMessage(), e);
  879.             return;
  880.         }
  881.         if (fileNames == null) {
  882.             return;
  883.         }

  884.         String latestId = null;
  885.         for (String n : fileNames) {
  886.             PackFile pf = new PackFile(packDir.toFile(), n);
  887.             PackExt ext = pf.getPackExt();
  888.             if (ext.equals(PACK) || ext.equals(KEEP)) {
  889.                 latestId = pf.getId();
  890.             }
  891.             if (latestId == null || !pf.getId().equals(latestId)) {
  892.                 // no pack or keep for this id
  893.                 try {
  894.                     FileUtils.delete(pf,
  895.                             FileUtils.RETRY | FileUtils.SKIP_MISSING);
  896.                     LOG.warn(JGitText.get().deletedOrphanInPackDir, pf);
  897.                 } catch (IOException e) {
  898.                     LOG.error(e.getMessage(), e);
  899.                 }
  900.             }
  901.         }
  902.     }

  903.     private void deleteTempPacksIdx() {
  904.         Path packDir = repo.getObjectDatabase().getPackDirectory().toPath();
  905.         Instant threshold = Instant.now().minus(1, ChronoUnit.DAYS);
  906.         if (!Files.exists(packDir)) {
  907.             return;
  908.         }
  909.         try (DirectoryStream<Path> stream =
  910.                 Files.newDirectoryStream(packDir, "gc_*_tmp")) { //$NON-NLS-1$
  911.             stream.forEach(t -> {
  912.                 try {
  913.                     Instant lastModified = Files.getLastModifiedTime(t)
  914.                             .toInstant();
  915.                     if (lastModified.isBefore(threshold)) {
  916.                         Files.deleteIfExists(t);
  917.                     }
  918.                 } catch (IOException e) {
  919.                     LOG.error(e.getMessage(), e);
  920.                 }
  921.             });
  922.         } catch (IOException e) {
  923.             LOG.error(e.getMessage(), e);
  924.         }
  925.     }

  926.     /**
  927.      * @param ref
  928.      *            the ref which log should be inspected
  929.      * @param minTime only reflog entries not older then this time are processed
  930.      * @return the {@link ObjectId}s contained in the reflog
  931.      * @throws IOException
  932.      */
  933.     private Set<ObjectId> listRefLogObjects(Ref ref, long minTime) throws IOException {
  934.         ReflogReader reflogReader = repo.getReflogReader(ref.getName());
  935.         if (reflogReader == null) {
  936.             return Collections.emptySet();
  937.         }
  938.         List<ReflogEntry> rlEntries = reflogReader
  939.                 .getReverseEntries();
  940.         if (rlEntries == null || rlEntries.isEmpty())
  941.             return Collections.emptySet();
  942.         Set<ObjectId> ret = new HashSet<>();
  943.         for (ReflogEntry e : rlEntries) {
  944.             if (e.getWho().getWhen().getTime() < minTime)
  945.                 break;
  946.             ObjectId newId = e.getNewId();
  947.             if (newId != null && !ObjectId.zeroId().equals(newId))
  948.                 ret.add(newId);
  949.             ObjectId oldId = e.getOldId();
  950.             if (oldId != null && !ObjectId.zeroId().equals(oldId))
  951.                 ret.add(oldId);
  952.         }
  953.         return ret;
  954.     }

  955.     /**
  956.      * Returns a collection of all refs and additional refs.
  957.      *
  958.      * Additional refs which don't start with "refs/" are not returned because
  959.      * they should not save objects from being garbage collected. Examples for
  960.      * such references are ORIG_HEAD, MERGE_HEAD, FETCH_HEAD and
  961.      * CHERRY_PICK_HEAD.
  962.      *
  963.      * @return a collection of refs pointing to live objects.
  964.      * @throws IOException
  965.      */
  966.     private Collection<Ref> getAllRefs() throws IOException {
  967.         RefDatabase refdb = repo.getRefDatabase();
  968.         Collection<Ref> refs = refdb.getRefs();
  969.         List<Ref> addl = refdb.getAdditionalRefs();
  970.         if (!addl.isEmpty()) {
  971.             List<Ref> all = new ArrayList<>(refs.size() + addl.size());
  972.             all.addAll(refs);
  973.             // add additional refs which start with refs/
  974.             for (Ref r : addl) {
  975.                 checkCancelled();
  976.                 if (r.getName().startsWith(Constants.R_REFS)) {
  977.                     all.add(r);
  978.                 }
  979.             }
  980.             return all;
  981.         }
  982.         return refs;
  983.     }

  984.     /**
  985.      * Return a list of those objects in the index which differ from whats in
  986.      * HEAD
  987.      *
  988.      * @return a set of ObjectIds of changed objects in the index
  989.      * @throws IOException
  990.      * @throws CorruptObjectException
  991.      * @throws NoWorkTreeException
  992.      */
  993.     private Set<ObjectId> listNonHEADIndexObjects()
  994.             throws CorruptObjectException, IOException {
  995.         if (repo.isBare()) {
  996.             return Collections.emptySet();
  997.         }
  998.         try (TreeWalk treeWalk = new TreeWalk(repo)) {
  999.             treeWalk.addTree(new DirCacheIterator(repo.readDirCache()));
  1000.             ObjectId headID = repo.resolve(Constants.HEAD);
  1001.             if (headID != null) {
  1002.                 try (RevWalk revWalk = new RevWalk(repo)) {
  1003.                     treeWalk.addTree(revWalk.parseTree(headID));
  1004.                 }
  1005.             }

  1006.             treeWalk.setFilter(TreeFilter.ANY_DIFF);
  1007.             treeWalk.setRecursive(true);
  1008.             Set<ObjectId> ret = new HashSet<>();

  1009.             while (treeWalk.next()) {
  1010.                 checkCancelled();
  1011.                 ObjectId objectId = treeWalk.getObjectId(0);
  1012.                 switch (treeWalk.getRawMode(0) & FileMode.TYPE_MASK) {
  1013.                 case FileMode.TYPE_MISSING:
  1014.                 case FileMode.TYPE_GITLINK:
  1015.                     continue;
  1016.                 case FileMode.TYPE_TREE:
  1017.                 case FileMode.TYPE_FILE:
  1018.                 case FileMode.TYPE_SYMLINK:
  1019.                     ret.add(objectId);
  1020.                     continue;
  1021.                 default:
  1022.                     throw new IOException(MessageFormat.format(
  1023.                             JGitText.get().corruptObjectInvalidMode3,
  1024.                             String.format("%o", //$NON-NLS-1$
  1025.                                     Integer.valueOf(treeWalk.getRawMode(0))),
  1026.                             (objectId == null) ? "null" : objectId.name(), //$NON-NLS-1$
  1027.                             treeWalk.getPathString(), //
  1028.                             repo.getIndexFile()));
  1029.                 }
  1030.             }
  1031.             return ret;
  1032.         }
  1033.     }

  1034.     private Pack writePack(@NonNull Set<? extends ObjectId> want,
  1035.             @NonNull Set<? extends ObjectId> have, @NonNull Set<ObjectId> tags,
  1036.             Set<ObjectId> tagTargets, List<ObjectIdSet> excludeObjects)
  1037.             throws IOException {
  1038.         checkCancelled();
  1039.         File tmpPack = null;
  1040.         Map<PackExt, File> tmpExts = new TreeMap<>((o1, o2) -> {
  1041.             // INDEX entries must be returned last, so the pack
  1042.             // scanner does pick up the new pack until all the
  1043.             // PackExt entries have been written.
  1044.             if (o1 == o2) {
  1045.                 return 0;
  1046.             }
  1047.             if (o1 == PackExt.INDEX) {
  1048.                 return 1;
  1049.             }
  1050.             if (o2 == PackExt.INDEX) {
  1051.                 return -1;
  1052.             }
  1053.             return Integer.signum(o1.hashCode() - o2.hashCode());
  1054.         });
  1055.         try (PackWriter pw = new PackWriter(
  1056.                 pconfig,
  1057.                 repo.newObjectReader())) {
  1058.             // prepare the PackWriter
  1059.             pw.setDeltaBaseAsOffset(true);
  1060.             pw.setReuseDeltaCommits(false);
  1061.             if (tagTargets != null) {
  1062.                 pw.setTagTargets(tagTargets);
  1063.             }
  1064.             if (excludeObjects != null)
  1065.                 for (ObjectIdSet idx : excludeObjects)
  1066.                     pw.excludeObjects(idx);
  1067.             pw.preparePack(pm, want, have, PackWriter.NONE, tags);
  1068.             if (pw.getObjectCount() == 0)
  1069.                 return null;
  1070.             checkCancelled();

  1071.             // create temporary files
  1072.             ObjectId id = pw.computeName();
  1073.             File packdir = repo.getObjectDatabase().getPackDirectory();
  1074.             packdir.mkdirs();
  1075.             tmpPack = File.createTempFile("gc_", ".pack_tmp", packdir); //$NON-NLS-1$ //$NON-NLS-2$
  1076.             final String tmpBase = tmpPack.getName()
  1077.                     .substring(0, tmpPack.getName().lastIndexOf('.'));
  1078.             File tmpIdx = new File(packdir, tmpBase + ".idx_tmp"); //$NON-NLS-1$
  1079.             tmpExts.put(INDEX, tmpIdx);

  1080.             if (!tmpIdx.createNewFile())
  1081.                 throw new IOException(MessageFormat.format(
  1082.                         JGitText.get().cannotCreateIndexfile, tmpIdx.getPath()));

  1083.             // write the packfile
  1084.             try (FileOutputStream fos = new FileOutputStream(tmpPack);
  1085.                     FileChannel channel = fos.getChannel();
  1086.                     OutputStream channelStream = Channels
  1087.                             .newOutputStream(channel)) {
  1088.                 pw.writePack(pm, pm, channelStream);
  1089.                 channel.force(true);
  1090.             }

  1091.             // write the packindex
  1092.             try (FileOutputStream fos = new FileOutputStream(tmpIdx);
  1093.                     FileChannel idxChannel = fos.getChannel();
  1094.                     OutputStream idxStream = Channels
  1095.                             .newOutputStream(idxChannel)) {
  1096.                 pw.writeIndex(idxStream);
  1097.                 idxChannel.force(true);
  1098.             }

  1099.             if (pw.prepareBitmapIndex(pm)) {
  1100.                 File tmpBitmapIdx = new File(packdir, tmpBase + ".bitmap_tmp"); //$NON-NLS-1$
  1101.                 tmpExts.put(BITMAP_INDEX, tmpBitmapIdx);

  1102.                 if (!tmpBitmapIdx.createNewFile())
  1103.                     throw new IOException(MessageFormat.format(
  1104.                             JGitText.get().cannotCreateIndexfile,
  1105.                             tmpBitmapIdx.getPath()));

  1106.                 try (FileOutputStream fos = new FileOutputStream(tmpBitmapIdx);
  1107.                         FileChannel idxChannel = fos.getChannel();
  1108.                         OutputStream idxStream = Channels
  1109.                                 .newOutputStream(idxChannel)) {
  1110.                     pw.writeBitmapIndex(idxStream);
  1111.                     idxChannel.force(true);
  1112.                 }
  1113.             }

  1114.             // rename the temporary files to real files
  1115.             File packDir = repo.getObjectDatabase().getPackDirectory();
  1116.             PackFile realPack = new PackFile(packDir, id, PackExt.PACK);

  1117.             repo.getObjectDatabase().closeAllPackHandles(realPack);
  1118.             tmpPack.setReadOnly();

  1119.             FileUtils.rename(tmpPack, realPack, StandardCopyOption.ATOMIC_MOVE);
  1120.             for (Map.Entry<PackExt, File> tmpEntry : tmpExts.entrySet()) {
  1121.                 File tmpExt = tmpEntry.getValue();
  1122.                 tmpExt.setReadOnly();

  1123.                 PackFile realExt = new PackFile(packDir, id, tmpEntry.getKey());
  1124.                 try {
  1125.                     FileUtils.rename(tmpExt, realExt,
  1126.                             StandardCopyOption.ATOMIC_MOVE);
  1127.                 } catch (IOException e) {
  1128.                     File newExt = new File(realExt.getParentFile(),
  1129.                             realExt.getName() + ".new"); //$NON-NLS-1$
  1130.                     try {
  1131.                         FileUtils.rename(tmpExt, newExt,
  1132.                                 StandardCopyOption.ATOMIC_MOVE);
  1133.                     } catch (IOException e2) {
  1134.                         newExt = tmpExt;
  1135.                         e = e2;
  1136.                     }
  1137.                     throw new IOException(MessageFormat.format(
  1138.                             JGitText.get().panicCantRenameIndexFile, newExt,
  1139.                             realExt), e);
  1140.                 }
  1141.             }
  1142.             boolean interrupted = false;
  1143.             try {
  1144.                 FileSnapshot snapshot = FileSnapshot.save(realPack);
  1145.                 if (pconfig.doWaitPreventRacyPack(snapshot.size())) {
  1146.                     snapshot.waitUntilNotRacy();
  1147.                 }
  1148.             } catch (InterruptedException e) {
  1149.                 interrupted = true;
  1150.             }
  1151.             try {
  1152.                 return repo.getObjectDatabase().openPack(realPack);
  1153.             } finally {
  1154.                 if (interrupted) {
  1155.                     // Re-set interrupted flag
  1156.                     Thread.currentThread().interrupt();
  1157.                 }
  1158.             }
  1159.         } finally {
  1160.             if (tmpPack != null && tmpPack.exists())
  1161.                 tmpPack.delete();
  1162.             for (File tmpExt : tmpExts.values()) {
  1163.                 if (tmpExt.exists())
  1164.                     tmpExt.delete();
  1165.             }
  1166.         }
  1167.     }

  1168.     private void checkCancelled() throws CancelledException {
  1169.         if (pm.isCancelled() || Thread.currentThread().isInterrupted()) {
  1170.             throw new CancelledException(JGitText.get().operationCanceled);
  1171.         }
  1172.     }

  1173.     /**
  1174.      * A class holding statistical data for a FileRepository regarding how many
  1175.      * objects are stored as loose or packed objects
  1176.      */
  1177.     public static class RepoStatistics {
  1178.         /**
  1179.          * The number of objects stored in pack files. If the same object is
  1180.          * stored in multiple pack files then it is counted as often as it
  1181.          * occurs in pack files.
  1182.          */
  1183.         public long numberOfPackedObjects;

  1184.         /**
  1185.          * The number of pack files
  1186.          */
  1187.         public long numberOfPackFiles;

  1188.         /**
  1189.          * The number of objects stored as loose objects.
  1190.          */
  1191.         public long numberOfLooseObjects;

  1192.         /**
  1193.          * The sum of the sizes of all files used to persist loose objects.
  1194.          */
  1195.         public long sizeOfLooseObjects;

  1196.         /**
  1197.          * The sum of the sizes of all pack files.
  1198.          */
  1199.         public long sizeOfPackedObjects;

  1200.         /**
  1201.          * The number of loose refs.
  1202.          */
  1203.         public long numberOfLooseRefs;

  1204.         /**
  1205.          * The number of refs stored in pack files.
  1206.          */
  1207.         public long numberOfPackedRefs;

  1208.         /**
  1209.          * The number of bitmaps in the bitmap indices.
  1210.          */
  1211.         public long numberOfBitmaps;

  1212.         @Override
  1213.         public String toString() {
  1214.             final StringBuilder b = new StringBuilder();
  1215.             b.append("numberOfPackedObjects=").append(numberOfPackedObjects); //$NON-NLS-1$
  1216.             b.append(", numberOfPackFiles=").append(numberOfPackFiles); //$NON-NLS-1$
  1217.             b.append(", numberOfLooseObjects=").append(numberOfLooseObjects); //$NON-NLS-1$
  1218.             b.append(", numberOfLooseRefs=").append(numberOfLooseRefs); //$NON-NLS-1$
  1219.             b.append(", numberOfPackedRefs=").append(numberOfPackedRefs); //$NON-NLS-1$
  1220.             b.append(", sizeOfLooseObjects=").append(sizeOfLooseObjects); //$NON-NLS-1$
  1221.             b.append(", sizeOfPackedObjects=").append(sizeOfPackedObjects); //$NON-NLS-1$
  1222.             b.append(", numberOfBitmaps=").append(numberOfBitmaps); //$NON-NLS-1$
  1223.             return b.toString();
  1224.         }
  1225.     }

  1226.     /**
  1227.      * Returns information about objects and pack files for a FileRepository.
  1228.      *
  1229.      * @return information about objects and pack files for a FileRepository
  1230.      * @throws java.io.IOException
  1231.      */
  1232.     public RepoStatistics getStatistics() throws IOException {
  1233.         RepoStatistics ret = new RepoStatistics();
  1234.         Collection<Pack> packs = repo.getObjectDatabase().getPacks();
  1235.         for (Pack p : packs) {
  1236.             ret.numberOfPackedObjects += p.getIndex().getObjectCount();
  1237.             ret.numberOfPackFiles++;
  1238.             ret.sizeOfPackedObjects += p.getPackFile().length();
  1239.             if (p.getBitmapIndex() != null)
  1240.                 ret.numberOfBitmaps += p.getBitmapIndex().getBitmapCount();
  1241.         }
  1242.         File objDir = repo.getObjectsDirectory();
  1243.         String[] fanout = objDir.list();
  1244.         if (fanout != null && fanout.length > 0) {
  1245.             for (String d : fanout) {
  1246.                 if (d.length() != 2)
  1247.                     continue;
  1248.                 File[] entries = new File(objDir, d).listFiles();
  1249.                 if (entries == null)
  1250.                     continue;
  1251.                 for (File f : entries) {
  1252.                     if (f.getName().length() != Constants.OBJECT_ID_STRING_LENGTH - 2)
  1253.                         continue;
  1254.                     ret.numberOfLooseObjects++;
  1255.                     ret.sizeOfLooseObjects += f.length();
  1256.                 }
  1257.             }
  1258.         }

  1259.         RefDatabase refDb = repo.getRefDatabase();
  1260.         for (Ref r : refDb.getRefs()) {
  1261.             Storage storage = r.getStorage();
  1262.             if (storage == Storage.LOOSE || storage == Storage.LOOSE_PACKED)
  1263.                 ret.numberOfLooseRefs++;
  1264.             if (storage == Storage.PACKED || storage == Storage.LOOSE_PACKED)
  1265.                 ret.numberOfPackedRefs++;
  1266.         }

  1267.         return ret;
  1268.     }

  1269.     /**
  1270.      * Set the progress monitor used for garbage collection methods.
  1271.      *
  1272.      * @param pm a {@link org.eclipse.jgit.lib.ProgressMonitor} object.
  1273.      * @return this
  1274.      */
  1275.     public GC setProgressMonitor(ProgressMonitor pm) {
  1276.         this.pm = (pm == null) ? NullProgressMonitor.INSTANCE : pm;
  1277.         return this;
  1278.     }

  1279.     /**
  1280.      * During gc() or prune() each unreferenced, loose object which has been
  1281.      * created or modified in the last <code>expireAgeMillis</code> milliseconds
  1282.      * will not be pruned. Only older objects may be pruned. If set to 0 then
  1283.      * every object is a candidate for pruning.
  1284.      *
  1285.      * @param expireAgeMillis
  1286.      *            minimal age of objects to be pruned in milliseconds.
  1287.      */
  1288.     public void setExpireAgeMillis(long expireAgeMillis) {
  1289.         this.expireAgeMillis = expireAgeMillis;
  1290.         expire = null;
  1291.     }

  1292.     /**
  1293.      * During gc() or prune() packfiles which are created or modified in the
  1294.      * last <code>packExpireAgeMillis</code> milliseconds will not be deleted.
  1295.      * Only older packfiles may be deleted. If set to 0 then every packfile is a
  1296.      * candidate for deletion.
  1297.      *
  1298.      * @param packExpireAgeMillis
  1299.      *            minimal age of packfiles to be deleted in milliseconds.
  1300.      */
  1301.     public void setPackExpireAgeMillis(long packExpireAgeMillis) {
  1302.         this.packExpireAgeMillis = packExpireAgeMillis;
  1303.         expire = null;
  1304.     }

  1305.     /**
  1306.      * Set the PackConfig used when (re-)writing packfiles. This allows to
  1307.      * influence how packs are written and to implement something similar to
  1308.      * "git gc --aggressive"
  1309.      *
  1310.      * @param pconfig
  1311.      *            the {@link org.eclipse.jgit.storage.pack.PackConfig} used when
  1312.      *            writing packs
  1313.      */
  1314.     public void setPackConfig(@NonNull PackConfig pconfig) {
  1315.         this.pconfig = pconfig;
  1316.     }

  1317.     /**
  1318.      * During gc() or prune() each unreferenced, loose object which has been
  1319.      * created or modified after or at <code>expire</code> will not be pruned.
  1320.      * Only older objects may be pruned. If set to null then every object is a
  1321.      * candidate for pruning.
  1322.      *
  1323.      * @param expire
  1324.      *            instant in time which defines object expiration
  1325.      *            objects with modification time before this instant are expired
  1326.      *            objects with modification time newer or equal to this instant
  1327.      *            are not expired
  1328.      */
  1329.     public void setExpire(Date expire) {
  1330.         this.expire = expire;
  1331.         expireAgeMillis = -1;
  1332.     }

  1333.     /**
  1334.      * During gc() or prune() packfiles which are created or modified after or
  1335.      * at <code>packExpire</code> will not be deleted. Only older packfiles may
  1336.      * be deleted. If set to null then every packfile is a candidate for
  1337.      * deletion.
  1338.      *
  1339.      * @param packExpire
  1340.      *            instant in time which defines packfile expiration
  1341.      */
  1342.     public void setPackExpire(Date packExpire) {
  1343.         this.packExpire = packExpire;
  1344.         packExpireAgeMillis = -1;
  1345.     }

  1346.     /**
  1347.      * Set the {@code gc --auto} option.
  1348.      *
  1349.      * With this option, gc checks whether any housekeeping is required; if not,
  1350.      * it exits without performing any work. Some JGit commands run
  1351.      * {@code gc --auto} after performing operations that could create many
  1352.      * loose objects.
  1353.      * <p>
  1354.      * Housekeeping is required if there are too many loose objects or too many
  1355.      * packs in the repository. If the number of loose objects exceeds the value
  1356.      * of the gc.auto option JGit GC consolidates all existing packs into a
  1357.      * single pack (equivalent to {@code -A} option), whereas git-core would
  1358.      * combine all loose objects into a single pack using {@code repack -d -l}.
  1359.      * Setting the value of {@code gc.auto} to 0 disables automatic packing of
  1360.      * loose objects.
  1361.      * <p>
  1362.      * If the number of packs exceeds the value of {@code gc.autoPackLimit},
  1363.      * then existing packs (except those marked with a .keep file) are
  1364.      * consolidated into a single pack by using the {@code -A} option of repack.
  1365.      * Setting {@code gc.autoPackLimit} to 0 disables automatic consolidation of
  1366.      * packs.
  1367.      * <p>
  1368.      * Like git the following jgit commands run auto gc:
  1369.      * <ul>
  1370.      * <li>fetch</li>
  1371.      * <li>merge</li>
  1372.      * <li>rebase</li>
  1373.      * <li>receive-pack</li>
  1374.      * </ul>
  1375.      * The auto gc for receive-pack can be suppressed by setting the config
  1376.      * option {@code receive.autogc = false}
  1377.      *
  1378.      * @param auto
  1379.      *            defines whether gc should do automatic housekeeping
  1380.      */
  1381.     public void setAuto(boolean auto) {
  1382.         this.automatic = auto;
  1383.     }

  1384.     /**
  1385.      * @param background
  1386.      *            whether to run the gc in a background thread.
  1387.      */
  1388.     void setBackground(boolean background) {
  1389.         this.background = background;
  1390.     }

  1391.     private boolean needGc() {
  1392.         if (tooManyPacks()) {
  1393.             addRepackAllOption();
  1394.         } else {
  1395.             return tooManyLooseObjects();
  1396.         }
  1397.         // TODO run pre-auto-gc hook, if it fails return false
  1398.         return true;
  1399.     }

  1400.     private void addRepackAllOption() {
  1401.         // TODO: if JGit GC is enhanced to support repack's option -l this
  1402.         // method needs to be implemented
  1403.     }

  1404.     /**
  1405.      * @return {@code true} if number of packs &gt; gc.autopacklimit (default
  1406.      *         50)
  1407.      */
  1408.     boolean tooManyPacks() {
  1409.         int autopacklimit = repo.getConfig().getInt(
  1410.                 ConfigConstants.CONFIG_GC_SECTION,
  1411.                 ConfigConstants.CONFIG_KEY_AUTOPACKLIMIT,
  1412.                 DEFAULT_AUTOPACKLIMIT);
  1413.         if (autopacklimit <= 0) {
  1414.             return false;
  1415.         }
  1416.         // JGit always creates two packfiles, one for the objects reachable from
  1417.         // branches, and another one for the rest
  1418.         return repo.getObjectDatabase().getPacks().size() > (autopacklimit + 1);
  1419.     }

  1420.     /**
  1421.      * Quickly estimate number of loose objects, SHA1 is distributed evenly so
  1422.      * counting objects in one directory (bucket 17) is sufficient
  1423.      *
  1424.      * @return {@code true} if number of loose objects &gt; gc.auto (default
  1425.      *         6700)
  1426.      */
  1427.     boolean tooManyLooseObjects() {
  1428.         int auto = getLooseObjectLimit();
  1429.         if (auto <= 0) {
  1430.             return false;
  1431.         }
  1432.         int n = 0;
  1433.         int threshold = (auto + 255) / 256;
  1434.         Path dir = repo.getObjectsDirectory().toPath().resolve("17"); //$NON-NLS-1$
  1435.         if (!dir.toFile().exists()) {
  1436.             return false;
  1437.         }
  1438.         try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, file -> {
  1439.                     Path fileName = file.getFileName();
  1440.                     return file.toFile().isFile() && fileName != null
  1441.                             && PATTERN_LOOSE_OBJECT.matcher(fileName.toString())
  1442.                                     .matches();
  1443.                 })) {
  1444.             for (Iterator<Path> iter = stream.iterator(); iter.hasNext(); iter
  1445.                     .next()) {
  1446.                 if (++n > threshold) {
  1447.                     return true;
  1448.                 }
  1449.             }
  1450.         } catch (IOException e) {
  1451.             LOG.error(e.getMessage(), e);
  1452.         }
  1453.         return false;
  1454.     }

  1455.     private int getLooseObjectLimit() {
  1456.         return repo.getConfig().getInt(ConfigConstants.CONFIG_GC_SECTION,
  1457.                 ConfigConstants.CONFIG_KEY_AUTO, DEFAULT_AUTOLIMIT);
  1458.     }
  1459. }