You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

GC.java 48KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555
  1. /*
  2. * Copyright (C) 2012, Christian Halstrick <christian.halstrick@sap.com>
  3. * Copyright (C) 2011, Shawn O. Pearce <spearce@spearce.org>
  4. * and other copyright owners as documented in the project's IP log.
  5. *
  6. * This program and the accompanying materials are made available
  7. * under the terms of the Eclipse Distribution License v1.0 which
  8. * accompanies this distribution, is reproduced below, and is
  9. * available at http://www.eclipse.org/org/documents/edl-v10.php
  10. *
  11. * All rights reserved.
  12. *
  13. * Redistribution and use in source and binary forms, with or
  14. * without modification, are permitted provided that the following
  15. * conditions are met:
  16. *
  17. * - Redistributions of source code must retain the above copyright
  18. * notice, this list of conditions and the following disclaimer.
  19. *
  20. * - Redistributions in binary form must reproduce the above
  21. * copyright notice, this list of conditions and the following
  22. * disclaimer in the documentation and/or other materials provided
  23. * with the distribution.
  24. *
  25. * - Neither the name of the Eclipse Foundation, Inc. nor the
  26. * names of its contributors may be used to endorse or promote
  27. * products derived from this software without specific prior
  28. * written permission.
  29. *
  30. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  31. * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  32. * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  33. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  34. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  35. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  36. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  37. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  38. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  39. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  40. * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  41. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  42. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  43. */
  44. package org.eclipse.jgit.internal.storage.file;
  45. import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
  46. import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
  47. import java.io.File;
  48. import java.io.FileOutputStream;
  49. import java.io.IOException;
  50. import java.io.OutputStream;
  51. import java.io.PrintWriter;
  52. import java.io.StringWriter;
  53. import java.nio.channels.Channels;
  54. import java.nio.channels.FileChannel;
  55. import java.nio.file.DirectoryStream;
  56. import java.nio.file.Files;
  57. import java.nio.file.Path;
  58. import java.nio.file.Paths;
  59. import java.nio.file.StandardCopyOption;
  60. import java.text.MessageFormat;
  61. import java.text.ParseException;
  62. import java.time.Instant;
  63. import java.time.temporal.ChronoUnit;
  64. import java.util.ArrayList;
  65. import java.util.Collection;
  66. import java.util.Collections;
  67. import java.util.Comparator;
  68. import java.util.Date;
  69. import java.util.HashMap;
  70. import java.util.HashSet;
  71. import java.util.Iterator;
  72. import java.util.LinkedList;
  73. import java.util.List;
  74. import java.util.Map;
  75. import java.util.Objects;
  76. import java.util.Set;
  77. import java.util.TreeMap;
  78. import java.util.concurrent.Callable;
  79. import java.util.concurrent.ExecutionException;
  80. import java.util.concurrent.ExecutorService;
  81. import java.util.concurrent.Executors;
  82. import java.util.concurrent.Future;
  83. import java.util.regex.Pattern;
  84. import java.util.stream.Collectors;
  85. import java.util.stream.Stream;
  86. import org.eclipse.jgit.annotations.NonNull;
  87. import org.eclipse.jgit.api.errors.JGitInternalException;
  88. import org.eclipse.jgit.dircache.DirCacheIterator;
  89. import org.eclipse.jgit.errors.CancelledException;
  90. import org.eclipse.jgit.errors.CorruptObjectException;
  91. import org.eclipse.jgit.errors.IncorrectObjectTypeException;
  92. import org.eclipse.jgit.errors.MissingObjectException;
  93. import org.eclipse.jgit.errors.NoWorkTreeException;
  94. import org.eclipse.jgit.internal.JGitText;
  95. import org.eclipse.jgit.internal.storage.pack.PackExt;
  96. import org.eclipse.jgit.internal.storage.pack.PackWriter;
  97. import org.eclipse.jgit.internal.storage.reftree.RefTreeNames;
  98. import org.eclipse.jgit.lib.ConfigConstants;
  99. import org.eclipse.jgit.lib.Constants;
  100. import org.eclipse.jgit.lib.FileMode;
  101. import org.eclipse.jgit.lib.NullProgressMonitor;
  102. import org.eclipse.jgit.lib.ObjectId;
  103. import org.eclipse.jgit.lib.ObjectIdSet;
  104. import org.eclipse.jgit.lib.ObjectLoader;
  105. import org.eclipse.jgit.lib.ObjectReader;
  106. import org.eclipse.jgit.lib.ProgressMonitor;
  107. import org.eclipse.jgit.lib.Ref;
  108. import org.eclipse.jgit.lib.Ref.Storage;
  109. import org.eclipse.jgit.lib.RefDatabase;
  110. import org.eclipse.jgit.lib.ReflogEntry;
  111. import org.eclipse.jgit.lib.ReflogReader;
  112. import org.eclipse.jgit.revwalk.ObjectWalk;
  113. import org.eclipse.jgit.revwalk.RevObject;
  114. import org.eclipse.jgit.revwalk.RevWalk;
  115. import org.eclipse.jgit.storage.pack.PackConfig;
  116. import org.eclipse.jgit.treewalk.TreeWalk;
  117. import org.eclipse.jgit.treewalk.filter.TreeFilter;
  118. import org.eclipse.jgit.util.FileUtils;
  119. import org.eclipse.jgit.util.GitDateParser;
  120. import org.eclipse.jgit.util.SystemReader;
  121. import org.slf4j.Logger;
  122. import org.slf4j.LoggerFactory;
  123. /**
  124. * A garbage collector for git {@link FileRepository}. Instances of this class
  125. * are not thread-safe. Don't use the same instance from multiple threads.
  126. *
  127. * This class started as a copy of DfsGarbageCollector from Shawn O. Pearce
  128. * adapted to FileRepositories.
  129. */
  130. public class GC {
  131. private final static Logger LOG = LoggerFactory
  132. .getLogger(GC.class);
  133. private static final String PRUNE_EXPIRE_DEFAULT = "2.weeks.ago"; //$NON-NLS-1$
  134. private static final String PRUNE_PACK_EXPIRE_DEFAULT = "1.hour.ago"; //$NON-NLS-1$
  135. private static final Pattern PATTERN_LOOSE_OBJECT = Pattern
  136. .compile("[0-9a-fA-F]{38}"); //$NON-NLS-1$
  137. private static final String PACK_EXT = "." + PackExt.PACK.getExtension();//$NON-NLS-1$
  138. private static final String BITMAP_EXT = "." //$NON-NLS-1$
  139. + PackExt.BITMAP_INDEX.getExtension();
  140. private static final String INDEX_EXT = "." + PackExt.INDEX.getExtension(); //$NON-NLS-1$
  141. private static final int DEFAULT_AUTOPACKLIMIT = 50;
  142. private static final int DEFAULT_AUTOLIMIT = 6700;
  143. private static ExecutorService executor = Executors.newFixedThreadPool(1);
  144. private final FileRepository repo;
  145. private ProgressMonitor pm;
  146. private long expireAgeMillis = -1;
  147. private Date expire;
  148. private long packExpireAgeMillis = -1;
  149. private Date packExpire;
  150. private PackConfig pconfig = null;
  151. /**
  152. * the refs which existed during the last call to {@link #repack()}. This is
  153. * needed during {@link #prune(Set)} where we can optimize by looking at the
  154. * difference between the current refs and the refs which existed during
  155. * last {@link #repack()}.
  156. */
  157. private Collection<Ref> lastPackedRefs;
  158. /**
  159. * Holds the starting time of the last repack() execution. This is needed in
  160. * prune() to inspect only those reflog entries which have been added since
  161. * last repack().
  162. */
  163. private long lastRepackTime;
  164. /**
  165. * Whether gc should do automatic housekeeping
  166. */
  167. private boolean automatic;
  168. /**
  169. * Whether to run gc in a background thread
  170. */
  171. private boolean background;
  172. /**
  173. * Creates a new garbage collector with default values. An expirationTime of
  174. * two weeks and <code>null</code> as progress monitor will be used.
  175. *
  176. * @param repo
  177. * the repo to work on
  178. */
  179. public GC(FileRepository repo) {
  180. this.repo = repo;
  181. this.pm = NullProgressMonitor.INSTANCE;
  182. }
  183. /**
  184. * Runs a garbage collector on a {@link FileRepository}. It will
  185. * <ul>
  186. * <li>pack loose references into packed-refs</li>
  187. * <li>repack all reachable objects into new pack files and delete the old
  188. * pack files</li>
  189. * <li>prune all loose objects which are now reachable by packs</li>
  190. * </ul>
  191. *
  192. * If {@link #setAuto(boolean)} was set to {@code true} {@code gc} will
  193. * first check whether any housekeeping is required; if not, it exits
  194. * without performing any work.
  195. *
  196. * If {@link #setBackground(boolean)} was set to {@code true}
  197. * {@code collectGarbage} will start the gc in the background, and then
  198. * return immediately. In this case, errors will not be reported except in
  199. * gc.log.
  200. *
  201. * @return the collection of {@link PackFile}'s which are newly created
  202. * @throws IOException
  203. * @throws ParseException
  204. * If the configuration parameter "gc.pruneexpire" couldn't be
  205. * parsed
  206. */
  207. // TODO(ms): in 5.0 change signature and return Future<Collection<PackFile>>
  208. public Collection<PackFile> gc() throws IOException, ParseException {
  209. final GcLog gcLog = background ? new GcLog(repo) : null;
  210. if (gcLog != null && !gcLog.lock(background)) {
  211. // there is already a background gc running
  212. return Collections.emptyList();
  213. }
  214. Callable<Collection<PackFile>> gcTask = () -> {
  215. try {
  216. Collection<PackFile> newPacks = doGc();
  217. if (automatic && tooManyLooseObjects() && gcLog != null) {
  218. String message = JGitText.get().gcTooManyUnpruned;
  219. gcLog.write(message);
  220. gcLog.commit();
  221. }
  222. return newPacks;
  223. } catch (IOException | ParseException e) {
  224. if (background) {
  225. if (gcLog == null) {
  226. // Lacking a log, there's no way to report this.
  227. return Collections.emptyList();
  228. }
  229. try {
  230. gcLog.write(e.getMessage());
  231. StringWriter sw = new StringWriter();
  232. e.printStackTrace(new PrintWriter(sw));
  233. gcLog.write(sw.toString());
  234. gcLog.commit();
  235. } catch (IOException e2) {
  236. e2.addSuppressed(e);
  237. LOG.error(e2.getMessage(), e2);
  238. }
  239. } else {
  240. throw new JGitInternalException(e.getMessage(), e);
  241. }
  242. } finally {
  243. if (gcLog != null) {
  244. gcLog.unlock();
  245. }
  246. }
  247. return Collections.emptyList();
  248. };
  249. Future<Collection<PackFile>> result = executor.submit(gcTask);
  250. if (background) {
  251. // TODO(ms): in 5.0 change signature and return the Future
  252. return Collections.emptyList();
  253. }
  254. try {
  255. return result.get();
  256. } catch (InterruptedException | ExecutionException e) {
  257. throw new IOException(e);
  258. }
  259. }
  260. private Collection<PackFile> doGc() throws IOException, ParseException {
  261. if (automatic && !needGc()) {
  262. return Collections.emptyList();
  263. }
  264. pm.start(6 /* tasks */);
  265. packRefs();
  266. // TODO: implement reflog_expire(pm, repo);
  267. Collection<PackFile> newPacks = repack();
  268. prune(Collections.<ObjectId> emptySet());
  269. // TODO: implement rerere_gc(pm);
  270. return newPacks;
  271. }
  272. /**
  273. * Loosen objects in a pack file which are not also in the newly-created
  274. * pack files.
  275. *
  276. * @param inserter
  277. * @param reader
  278. * @param pack
  279. * @param existing
  280. * @throws IOException
  281. */
  282. private void loosen(ObjectDirectoryInserter inserter, ObjectReader reader, PackFile pack, HashSet<ObjectId> existing)
  283. throws IOException {
  284. for (PackIndex.MutableEntry entry : pack) {
  285. ObjectId oid = entry.toObjectId();
  286. if (existing.contains(oid)) {
  287. continue;
  288. }
  289. existing.add(oid);
  290. ObjectLoader loader = reader.open(oid);
  291. inserter.insert(loader.getType(),
  292. loader.getSize(),
  293. loader.openStream(),
  294. true /* create this object even though it's a duplicate */);
  295. }
  296. }
  297. /**
  298. * Delete old pack files. What is 'old' is defined by specifying a set of
  299. * old pack files and a set of new pack files. Each pack file contained in
  300. * old pack files but not contained in new pack files will be deleted. If
  301. * preserveOldPacks is set, keep a copy of the pack file in the preserve
  302. * directory. If an expirationDate is set then pack files which are younger
  303. * than the expirationDate will not be deleted nor preserved.
  304. * <p>
  305. * If we're not immediately expiring loose objects, loosen any objects
  306. * in the old pack files which aren't in the new pack files.
  307. *
  308. * @param oldPacks
  309. * @param newPacks
  310. * @throws ParseException
  311. * @throws IOException
  312. */
  313. private void deleteOldPacks(Collection<PackFile> oldPacks,
  314. Collection<PackFile> newPacks) throws ParseException, IOException {
  315. HashSet<ObjectId> ids = new HashSet<>();
  316. for (PackFile pack : newPacks) {
  317. for (PackIndex.MutableEntry entry : pack) {
  318. ids.add(entry.toObjectId());
  319. }
  320. }
  321. ObjectReader reader = repo.newObjectReader();
  322. ObjectDirectory dir = repo.getObjectDatabase();
  323. ObjectDirectoryInserter inserter = dir.newInserter();
  324. boolean shouldLoosen = !"now".equals(getPruneExpireStr()) && //$NON-NLS-1$
  325. getExpireDate() < Long.MAX_VALUE;
  326. prunePreserved();
  327. long packExpireDate = getPackExpireDate();
  328. oldPackLoop: for (PackFile oldPack : oldPacks) {
  329. checkCancelled();
  330. String oldName = oldPack.getPackName();
  331. // check whether an old pack file is also among the list of new
  332. // pack files. Then we must not delete it.
  333. for (PackFile newPack : newPacks)
  334. if (oldName.equals(newPack.getPackName()))
  335. continue oldPackLoop;
  336. if (!oldPack.shouldBeKept()
  337. && repo.getFS().lastModified(
  338. oldPack.getPackFile()) < packExpireDate) {
  339. oldPack.close();
  340. if (shouldLoosen) {
  341. loosen(inserter, reader, oldPack, ids);
  342. }
  343. prunePack(oldName);
  344. }
  345. }
  346. // close the complete object database. That's my only chance to force
  347. // rescanning and to detect that certain pack files are now deleted.
  348. repo.getObjectDatabase().close();
  349. }
  350. /**
  351. * Deletes old pack file, unless 'preserve-oldpacks' is set, in which case it
  352. * moves the pack file to the preserved directory
  353. *
  354. * @param packFile
  355. * @param packName
  356. * @param ext
  357. * @param deleteOptions
  358. * @throws IOException
  359. */
  360. private void removeOldPack(File packFile, String packName, PackExt ext,
  361. int deleteOptions) throws IOException {
  362. if (pconfig != null && pconfig.isPreserveOldPacks()) {
  363. File oldPackDir = repo.getObjectDatabase().getPreservedDirectory();
  364. FileUtils.mkdir(oldPackDir, true);
  365. String oldPackName = "pack-" + packName + ".old-" + ext.getExtension(); //$NON-NLS-1$ //$NON-NLS-2$
  366. File oldPackFile = new File(oldPackDir, oldPackName);
  367. FileUtils.rename(packFile, oldPackFile);
  368. } else {
  369. FileUtils.delete(packFile, deleteOptions);
  370. }
  371. }
  372. /**
  373. * Delete the preserved directory including all pack files within
  374. */
  375. private void prunePreserved() {
  376. if (pconfig != null && pconfig.isPrunePreserved()) {
  377. try {
  378. FileUtils.delete(repo.getObjectDatabase().getPreservedDirectory(),
  379. FileUtils.RECURSIVE | FileUtils.RETRY | FileUtils.SKIP_MISSING);
  380. } catch (IOException e) {
  381. // Deletion of the preserved pack files failed. Silently return.
  382. }
  383. }
  384. }
  385. /**
  386. * Delete files associated with a single pack file. First try to delete the
  387. * ".pack" file because on some platforms the ".pack" file may be locked and
  388. * can't be deleted. In such a case it is better to detect this early and
  389. * give up on deleting files for this packfile. Otherwise we may delete the
  390. * ".index" file and when failing to delete the ".pack" file we are left
  391. * with a ".pack" file without a ".index" file.
  392. *
  393. * @param packName
  394. */
  395. private void prunePack(String packName) {
  396. PackExt[] extensions = PackExt.values();
  397. try {
  398. // Delete the .pack file first and if this fails give up on deleting
  399. // the other files
  400. int deleteOptions = FileUtils.RETRY | FileUtils.SKIP_MISSING;
  401. for (PackExt ext : extensions)
  402. if (PackExt.PACK.equals(ext)) {
  403. File f = nameFor(packName, "." + ext.getExtension()); //$NON-NLS-1$
  404. removeOldPack(f, packName, ext, deleteOptions);
  405. break;
  406. }
  407. // The .pack file has been deleted. Delete as many as the other
  408. // files as you can.
  409. deleteOptions |= FileUtils.IGNORE_ERRORS;
  410. for (PackExt ext : extensions) {
  411. if (!PackExt.PACK.equals(ext)) {
  412. File f = nameFor(packName, "." + ext.getExtension()); //$NON-NLS-1$
  413. removeOldPack(f, packName, ext, deleteOptions);
  414. }
  415. }
  416. } catch (IOException e) {
  417. // Deletion of the .pack file failed. Silently return.
  418. }
  419. }
  420. /**
  421. * Like "git prune-packed" this method tries to prune all loose objects
  422. * which can be found in packs. If certain objects can't be pruned (e.g.
  423. * because the filesystem delete operation fails) this is silently ignored.
  424. *
  425. * @throws IOException
  426. */
  427. public void prunePacked() throws IOException {
  428. ObjectDirectory objdb = repo.getObjectDatabase();
  429. Collection<PackFile> packs = objdb.getPacks();
  430. File objects = repo.getObjectsDirectory();
  431. String[] fanout = objects.list();
  432. if (fanout != null && fanout.length > 0) {
  433. pm.beginTask(JGitText.get().pruneLoosePackedObjects, fanout.length);
  434. try {
  435. for (String d : fanout) {
  436. checkCancelled();
  437. pm.update(1);
  438. if (d.length() != 2)
  439. continue;
  440. String[] entries = new File(objects, d).list();
  441. if (entries == null)
  442. continue;
  443. for (String e : entries) {
  444. checkCancelled();
  445. if (e.length() != Constants.OBJECT_ID_STRING_LENGTH - 2)
  446. continue;
  447. ObjectId id;
  448. try {
  449. id = ObjectId.fromString(d + e);
  450. } catch (IllegalArgumentException notAnObject) {
  451. // ignoring the file that does not represent loose
  452. // object
  453. continue;
  454. }
  455. boolean found = false;
  456. for (PackFile p : packs) {
  457. checkCancelled();
  458. if (p.hasObject(id)) {
  459. found = true;
  460. break;
  461. }
  462. }
  463. if (found)
  464. FileUtils.delete(objdb.fileFor(id), FileUtils.RETRY
  465. | FileUtils.SKIP_MISSING
  466. | FileUtils.IGNORE_ERRORS);
  467. }
  468. }
  469. } finally {
  470. pm.endTask();
  471. }
  472. }
  473. }
  474. /**
  475. * Like "git prune" this method tries to prune all loose objects which are
  476. * unreferenced. If certain objects can't be pruned (e.g. because the
  477. * filesystem delete operation fails) this is silently ignored.
  478. *
  479. * @param objectsToKeep
  480. * a set of objects which should explicitly not be pruned
  481. *
  482. * @throws IOException
  483. * @throws ParseException
  484. * If the configuration parameter "gc.pruneexpire" couldn't be
  485. * parsed
  486. */
  487. public void prune(Set<ObjectId> objectsToKeep) throws IOException,
  488. ParseException {
  489. long expireDate = getExpireDate();
  490. // Collect all loose objects which are old enough, not referenced from
  491. // the index and not in objectsToKeep
  492. Map<ObjectId, File> deletionCandidates = new HashMap<>();
  493. Set<ObjectId> indexObjects = null;
  494. File objects = repo.getObjectsDirectory();
  495. String[] fanout = objects.list();
  496. if (fanout == null || fanout.length == 0) {
  497. return;
  498. }
  499. pm.beginTask(JGitText.get().pruneLooseUnreferencedObjects,
  500. fanout.length);
  501. try {
  502. for (String d : fanout) {
  503. checkCancelled();
  504. pm.update(1);
  505. if (d.length() != 2)
  506. continue;
  507. File[] entries = new File(objects, d).listFiles();
  508. if (entries == null)
  509. continue;
  510. for (File f : entries) {
  511. checkCancelled();
  512. String fName = f.getName();
  513. if (fName.length() != Constants.OBJECT_ID_STRING_LENGTH - 2)
  514. continue;
  515. if (repo.getFS().lastModified(f) >= expireDate)
  516. continue;
  517. try {
  518. ObjectId id = ObjectId.fromString(d + fName);
  519. if (objectsToKeep.contains(id))
  520. continue;
  521. if (indexObjects == null)
  522. indexObjects = listNonHEADIndexObjects();
  523. if (indexObjects.contains(id))
  524. continue;
  525. deletionCandidates.put(id, f);
  526. } catch (IllegalArgumentException notAnObject) {
  527. // ignoring the file that does not represent loose
  528. // object
  529. continue;
  530. }
  531. }
  532. }
  533. } finally {
  534. pm.endTask();
  535. }
  536. if (deletionCandidates.isEmpty()) {
  537. return;
  538. }
  539. checkCancelled();
  540. // From the set of current refs remove all those which have been handled
  541. // during last repack(). Only those refs will survive which have been
  542. // added or modified since the last repack. Only these can save existing
  543. // loose refs from being pruned.
  544. Collection<Ref> newRefs;
  545. if (lastPackedRefs == null || lastPackedRefs.isEmpty())
  546. newRefs = getAllRefs();
  547. else {
  548. Map<String, Ref> last = new HashMap<>();
  549. for (Ref r : lastPackedRefs) {
  550. last.put(r.getName(), r);
  551. }
  552. newRefs = new ArrayList<>();
  553. for (Ref r : getAllRefs()) {
  554. Ref old = last.get(r.getName());
  555. if (!equals(r, old)) {
  556. newRefs.add(r);
  557. }
  558. }
  559. }
  560. if (!newRefs.isEmpty()) {
  561. // There are new/modified refs! Check which loose objects are now
  562. // referenced by these modified refs (or their reflogentries).
  563. // Remove these loose objects
  564. // from the deletionCandidates. When the last candidate is removed
  565. // leave this method.
  566. ObjectWalk w = new ObjectWalk(repo);
  567. try {
  568. for (Ref cr : newRefs) {
  569. checkCancelled();
  570. w.markStart(w.parseAny(cr.getObjectId()));
  571. }
  572. if (lastPackedRefs != null)
  573. for (Ref lpr : lastPackedRefs) {
  574. w.markUninteresting(w.parseAny(lpr.getObjectId()));
  575. }
  576. removeReferenced(deletionCandidates, w);
  577. } finally {
  578. w.dispose();
  579. }
  580. }
  581. if (deletionCandidates.isEmpty())
  582. return;
  583. // Since we have not left the method yet there are still
  584. // deletionCandidates. Last chance for these objects not to be pruned is
  585. // that they are referenced by reflog entries. Even refs which currently
  586. // point to the same object as during last repack() may have
  587. // additional reflog entries not handled during last repack()
  588. ObjectWalk w = new ObjectWalk(repo);
  589. try {
  590. for (Ref ar : getAllRefs())
  591. for (ObjectId id : listRefLogObjects(ar, lastRepackTime)) {
  592. checkCancelled();
  593. w.markStart(w.parseAny(id));
  594. }
  595. if (lastPackedRefs != null)
  596. for (Ref lpr : lastPackedRefs) {
  597. checkCancelled();
  598. w.markUninteresting(w.parseAny(lpr.getObjectId()));
  599. }
  600. removeReferenced(deletionCandidates, w);
  601. } finally {
  602. w.dispose();
  603. }
  604. if (deletionCandidates.isEmpty())
  605. return;
  606. checkCancelled();
  607. // delete all candidates which have survived: these are unreferenced
  608. // loose objects. Make a last check, though, to avoid deleting objects
  609. // that could have been referenced while the candidates list was being
  610. // built (by an incoming push, for example).
  611. Set<File> touchedFanout = new HashSet<>();
  612. for (File f : deletionCandidates.values()) {
  613. if (f.lastModified() < expireDate) {
  614. f.delete();
  615. touchedFanout.add(f.getParentFile());
  616. }
  617. }
  618. for (File f : touchedFanout) {
  619. FileUtils.delete(f,
  620. FileUtils.EMPTY_DIRECTORIES_ONLY | FileUtils.IGNORE_ERRORS);
  621. }
  622. repo.getObjectDatabase().close();
  623. }
  624. private long getExpireDate() throws ParseException {
  625. long expireDate = Long.MAX_VALUE;
  626. if (expire == null && expireAgeMillis == -1) {
  627. String pruneExpireStr = getPruneExpireStr();
  628. if (pruneExpireStr == null)
  629. pruneExpireStr = PRUNE_EXPIRE_DEFAULT;
  630. expire = GitDateParser.parse(pruneExpireStr, null, SystemReader
  631. .getInstance().getLocale());
  632. expireAgeMillis = -1;
  633. }
  634. if (expire != null)
  635. expireDate = expire.getTime();
  636. if (expireAgeMillis != -1)
  637. expireDate = System.currentTimeMillis() - expireAgeMillis;
  638. return expireDate;
  639. }
  640. private String getPruneExpireStr() {
  641. return repo.getConfig().getString(
  642. ConfigConstants.CONFIG_GC_SECTION, null,
  643. ConfigConstants.CONFIG_KEY_PRUNEEXPIRE);
  644. }
  645. private long getPackExpireDate() throws ParseException {
  646. long packExpireDate = Long.MAX_VALUE;
  647. if (packExpire == null && packExpireAgeMillis == -1) {
  648. String prunePackExpireStr = repo.getConfig().getString(
  649. ConfigConstants.CONFIG_GC_SECTION, null,
  650. ConfigConstants.CONFIG_KEY_PRUNEPACKEXPIRE);
  651. if (prunePackExpireStr == null)
  652. prunePackExpireStr = PRUNE_PACK_EXPIRE_DEFAULT;
  653. packExpire = GitDateParser.parse(prunePackExpireStr, null,
  654. SystemReader.getInstance().getLocale());
  655. packExpireAgeMillis = -1;
  656. }
  657. if (packExpire != null)
  658. packExpireDate = packExpire.getTime();
  659. if (packExpireAgeMillis != -1)
  660. packExpireDate = System.currentTimeMillis() - packExpireAgeMillis;
  661. return packExpireDate;
  662. }
  663. /**
  664. * Remove all entries from a map which key is the id of an object referenced
  665. * by the given ObjectWalk
  666. *
  667. * @param id2File
  668. * @param w
  669. * @throws MissingObjectException
  670. * @throws IncorrectObjectTypeException
  671. * @throws IOException
  672. */
  673. private void removeReferenced(Map<ObjectId, File> id2File,
  674. ObjectWalk w) throws MissingObjectException,
  675. IncorrectObjectTypeException, IOException {
  676. RevObject ro = w.next();
  677. while (ro != null) {
  678. checkCancelled();
  679. if (id2File.remove(ro.getId()) != null)
  680. if (id2File.isEmpty())
  681. return;
  682. ro = w.next();
  683. }
  684. ro = w.nextObject();
  685. while (ro != null) {
  686. checkCancelled();
  687. if (id2File.remove(ro.getId()) != null)
  688. if (id2File.isEmpty())
  689. return;
  690. ro = w.nextObject();
  691. }
  692. }
  693. private static boolean equals(Ref r1, Ref r2) {
  694. if (r1 == null || r2 == null)
  695. return false;
  696. if (r1.isSymbolic()) {
  697. if (!r2.isSymbolic())
  698. return false;
  699. return r1.getTarget().getName().equals(r2.getTarget().getName());
  700. } else {
  701. if (r2.isSymbolic()) {
  702. return false;
  703. }
  704. return Objects.equals(r1.getObjectId(), r2.getObjectId());
  705. }
  706. }
  707. /**
  708. * Packs all non-symbolic, loose refs into packed-refs.
  709. *
  710. * @throws IOException
  711. */
  712. public void packRefs() throws IOException {
  713. Collection<Ref> refs = repo.getRefDatabase().getRefs(Constants.R_REFS).values();
  714. List<String> refsToBePacked = new ArrayList<>(refs.size());
  715. pm.beginTask(JGitText.get().packRefs, refs.size());
  716. try {
  717. for (Ref ref : refs) {
  718. checkCancelled();
  719. if (!ref.isSymbolic() && ref.getStorage().isLoose())
  720. refsToBePacked.add(ref.getName());
  721. pm.update(1);
  722. }
  723. ((RefDirectory) repo.getRefDatabase()).pack(refsToBePacked);
  724. } finally {
  725. pm.endTask();
  726. }
  727. }
  728. /**
  729. * Packs all objects which reachable from any of the heads into one pack
  730. * file. Additionally all objects which are not reachable from any head but
  731. * which are reachable from any of the other refs (e.g. tags), special refs
  732. * (e.g. FETCH_HEAD) or index are packed into a separate pack file. Objects
  733. * included in pack files which have a .keep file associated are never
  734. * repacked. All old pack files which existed before are deleted.
  735. *
  736. * @return a collection of the newly created pack files
  737. * @throws IOException
  738. * when during reading of refs, index, packfiles, objects,
  739. * reflog-entries or during writing to the packfiles
  740. * {@link IOException} occurs
  741. */
  742. public Collection<PackFile> repack() throws IOException {
  743. Collection<PackFile> toBeDeleted = repo.getObjectDatabase().getPacks();
  744. long time = System.currentTimeMillis();
  745. Collection<Ref> refsBefore = getAllRefs();
  746. Set<ObjectId> allHeads = new HashSet<>();
  747. Set<ObjectId> nonHeads = new HashSet<>();
  748. Set<ObjectId> txnHeads = new HashSet<>();
  749. Set<ObjectId> tagTargets = new HashSet<>();
  750. Set<ObjectId> indexObjects = listNonHEADIndexObjects();
  751. RefDatabase refdb = repo.getRefDatabase();
  752. for (Ref ref : refsBefore) {
  753. checkCancelled();
  754. nonHeads.addAll(listRefLogObjects(ref, 0));
  755. if (ref.isSymbolic() || ref.getObjectId() == null)
  756. continue;
  757. if (isHead(ref) || isTag(ref))
  758. allHeads.add(ref.getObjectId());
  759. else if (RefTreeNames.isRefTree(refdb, ref.getName()))
  760. txnHeads.add(ref.getObjectId());
  761. else
  762. nonHeads.add(ref.getObjectId());
  763. if (ref.getPeeledObjectId() != null)
  764. tagTargets.add(ref.getPeeledObjectId());
  765. }
  766. List<ObjectIdSet> excluded = new LinkedList<>();
  767. for (final PackFile f : repo.getObjectDatabase().getPacks()) {
  768. checkCancelled();
  769. if (f.shouldBeKept())
  770. excluded.add(f.getIndex());
  771. }
  772. tagTargets.addAll(allHeads);
  773. nonHeads.addAll(indexObjects);
  774. List<PackFile> ret = new ArrayList<>(2);
  775. PackFile heads = null;
  776. if (!allHeads.isEmpty()) {
  777. heads = writePack(allHeads, Collections.<ObjectId> emptySet(),
  778. tagTargets, excluded);
  779. if (heads != null) {
  780. ret.add(heads);
  781. excluded.add(0, heads.getIndex());
  782. }
  783. }
  784. if (!nonHeads.isEmpty()) {
  785. PackFile rest = writePack(nonHeads, allHeads, tagTargets, excluded);
  786. if (rest != null)
  787. ret.add(rest);
  788. }
  789. if (!txnHeads.isEmpty()) {
  790. PackFile txn = writePack(txnHeads, PackWriter.NONE, null, excluded);
  791. if (txn != null)
  792. ret.add(txn);
  793. }
  794. try {
  795. deleteOldPacks(toBeDeleted, ret);
  796. } catch (ParseException e) {
  797. // TODO: the exception has to be wrapped into an IOException because
  798. // throwing the ParseException directly would break the API, instead
  799. // we should throw a ConfigInvalidException
  800. throw new IOException(e);
  801. }
  802. prunePacked();
  803. deleteEmptyRefsFolders();
  804. deleteOrphans();
  805. lastPackedRefs = refsBefore;
  806. lastRepackTime = time;
  807. return ret;
  808. }
  809. private static boolean isHead(Ref ref) {
  810. return ref.getName().startsWith(Constants.R_HEADS);
  811. }
  812. private static boolean isTag(Ref ref) {
  813. return ref.getName().startsWith(Constants.R_TAGS);
  814. }
  815. private void deleteEmptyRefsFolders() throws IOException {
  816. Path refs = repo.getDirectory().toPath().resolve("refs"); //$NON-NLS-1$
  817. try (Stream<Path> entries = Files.list(refs)) {
  818. Iterator<Path> iterator = entries.iterator();
  819. while (iterator.hasNext()) {
  820. try (Stream<Path> s = Files.list(iterator.next())) {
  821. s.forEach(this::deleteDir);
  822. }
  823. }
  824. }
  825. }
  826. private void deleteDir(Path dir) {
  827. try (Stream<Path> dirs = Files.walk(dir)) {
  828. dirs.filter(this::isDirectory).sorted(Comparator.reverseOrder())
  829. .forEach(this::delete);
  830. } catch (IOException e) {
  831. LOG.error(e.getMessage(), e);
  832. }
  833. }
  834. private boolean isDirectory(Path p) {
  835. return p.toFile().isDirectory();
  836. }
  837. private boolean delete(Path d) {
  838. try {
  839. // Avoid deleting a folder that was just created so that concurrent
  840. // operations trying to create a reference are not impacted
  841. Instant threshold = Instant.now().minus(30, ChronoUnit.SECONDS);
  842. Instant lastModified = Files.getLastModifiedTime(d).toInstant();
  843. if (lastModified.isBefore(threshold)) {
  844. // If the folder is not empty, the delete operation will fail
  845. // silently. This is a cheaper alternative to filtering the
  846. // stream in the calling method.
  847. return d.toFile().delete();
  848. }
  849. } catch (IOException e) {
  850. LOG.error(e.getMessage(), e);
  851. }
  852. return false;
  853. }
  854. /**
  855. * Deletes orphans
  856. * <p>
  857. * A file is considered an orphan if it is either a "bitmap" or an index
  858. * file, and its corresponding pack file is missing in the list.
  859. * </p>
  860. */
  861. private void deleteOrphans() {
  862. Path packDir = Paths.get(repo.getObjectsDirectory().getAbsolutePath(),
  863. "pack"); //$NON-NLS-1$
  864. List<String> fileNames = null;
  865. try (Stream<Path> files = Files.list(packDir)) {
  866. fileNames = files.map(path -> path.getFileName().toString())
  867. .filter(name -> {
  868. return (name.endsWith(PACK_EXT)
  869. || name.endsWith(BITMAP_EXT)
  870. || name.endsWith(INDEX_EXT));
  871. }).sorted(Collections.reverseOrder())
  872. .collect(Collectors.toList());
  873. } catch (IOException e1) {
  874. // ignore
  875. }
  876. if (fileNames == null) {
  877. return;
  878. }
  879. String base = null;
  880. for (String n : fileNames) {
  881. if (n.endsWith(PACK_EXT)) {
  882. base = n.substring(0, n.lastIndexOf('.'));
  883. } else {
  884. if (base == null || !n.startsWith(base)) {
  885. try {
  886. Files.delete(new File(packDir.toFile(), n).toPath());
  887. } catch (IOException e) {
  888. LOG.error(e.getMessage(), e);
  889. }
  890. }
  891. }
  892. }
  893. }
  894. /**
  895. * @param ref
  896. * the ref which log should be inspected
  897. * @param minTime only reflog entries not older then this time are processed
  898. * @return the {@link ObjectId}s contained in the reflog
  899. * @throws IOException
  900. */
  901. private Set<ObjectId> listRefLogObjects(Ref ref, long minTime) throws IOException {
  902. ReflogReader reflogReader = repo.getReflogReader(ref.getName());
  903. if (reflogReader == null) {
  904. return Collections.emptySet();
  905. }
  906. List<ReflogEntry> rlEntries = reflogReader
  907. .getReverseEntries();
  908. if (rlEntries == null || rlEntries.isEmpty())
  909. return Collections.<ObjectId> emptySet();
  910. Set<ObjectId> ret = new HashSet<>();
  911. for (ReflogEntry e : rlEntries) {
  912. if (e.getWho().getWhen().getTime() < minTime)
  913. break;
  914. ObjectId newId = e.getNewId();
  915. if (newId != null && !ObjectId.zeroId().equals(newId))
  916. ret.add(newId);
  917. ObjectId oldId = e.getOldId();
  918. if (oldId != null && !ObjectId.zeroId().equals(oldId))
  919. ret.add(oldId);
  920. }
  921. return ret;
  922. }
  923. /**
  924. * Returns a collection of all refs and additional refs.
  925. *
  926. * Additional refs which don't start with "refs/" are not returned because
  927. * they should not save objects from being garbage collected. Examples for
  928. * such references are ORIG_HEAD, MERGE_HEAD, FETCH_HEAD and
  929. * CHERRY_PICK_HEAD.
  930. *
  931. * @return a collection of refs pointing to live objects.
  932. * @throws IOException
  933. */
  934. private Collection<Ref> getAllRefs() throws IOException {
  935. RefDatabase refdb = repo.getRefDatabase();
  936. Collection<Ref> refs = refdb.getRefs(RefDatabase.ALL).values();
  937. List<Ref> addl = refdb.getAdditionalRefs();
  938. if (!addl.isEmpty()) {
  939. List<Ref> all = new ArrayList<>(refs.size() + addl.size());
  940. all.addAll(refs);
  941. // add additional refs which start with refs/
  942. for (Ref r : addl) {
  943. checkCancelled();
  944. if (r.getName().startsWith(Constants.R_REFS)) {
  945. all.add(r);
  946. }
  947. }
  948. return all;
  949. }
  950. return refs;
  951. }
  952. /**
  953. * Return a list of those objects in the index which differ from whats in
  954. * HEAD
  955. *
  956. * @return a set of ObjectIds of changed objects in the index
  957. * @throws IOException
  958. * @throws CorruptObjectException
  959. * @throws NoWorkTreeException
  960. */
  961. private Set<ObjectId> listNonHEADIndexObjects()
  962. throws CorruptObjectException, IOException {
  963. if (repo.isBare()) {
  964. return Collections.emptySet();
  965. }
  966. try (TreeWalk treeWalk = new TreeWalk(repo)) {
  967. treeWalk.addTree(new DirCacheIterator(repo.readDirCache()));
  968. ObjectId headID = repo.resolve(Constants.HEAD);
  969. if (headID != null) {
  970. try (RevWalk revWalk = new RevWalk(repo)) {
  971. treeWalk.addTree(revWalk.parseTree(headID));
  972. }
  973. }
  974. treeWalk.setFilter(TreeFilter.ANY_DIFF);
  975. treeWalk.setRecursive(true);
  976. Set<ObjectId> ret = new HashSet<>();
  977. while (treeWalk.next()) {
  978. checkCancelled();
  979. ObjectId objectId = treeWalk.getObjectId(0);
  980. switch (treeWalk.getRawMode(0) & FileMode.TYPE_MASK) {
  981. case FileMode.TYPE_MISSING:
  982. case FileMode.TYPE_GITLINK:
  983. continue;
  984. case FileMode.TYPE_TREE:
  985. case FileMode.TYPE_FILE:
  986. case FileMode.TYPE_SYMLINK:
  987. ret.add(objectId);
  988. continue;
  989. default:
  990. throw new IOException(MessageFormat.format(
  991. JGitText.get().corruptObjectInvalidMode3,
  992. String.format("%o", //$NON-NLS-1$
  993. Integer.valueOf(treeWalk.getRawMode(0))),
  994. (objectId == null) ? "null" : objectId.name(), //$NON-NLS-1$
  995. treeWalk.getPathString(), //
  996. repo.getIndexFile()));
  997. }
  998. }
  999. return ret;
  1000. }
  1001. }
  1002. private PackFile writePack(@NonNull Set<? extends ObjectId> want,
  1003. @NonNull Set<? extends ObjectId> have, Set<ObjectId> tagTargets,
  1004. List<ObjectIdSet> excludeObjects) throws IOException {
  1005. checkCancelled();
  1006. File tmpPack = null;
  1007. Map<PackExt, File> tmpExts = new TreeMap<>(
  1008. new Comparator<PackExt>() {
  1009. @Override
  1010. public int compare(PackExt o1, PackExt o2) {
  1011. // INDEX entries must be returned last, so the pack
  1012. // scanner does pick up the new pack until all the
  1013. // PackExt entries have been written.
  1014. if (o1 == o2)
  1015. return 0;
  1016. if (o1 == PackExt.INDEX)
  1017. return 1;
  1018. if (o2 == PackExt.INDEX)
  1019. return -1;
  1020. return Integer.signum(o1.hashCode() - o2.hashCode());
  1021. }
  1022. });
  1023. try (PackWriter pw = new PackWriter(
  1024. (pconfig == null) ? new PackConfig(repo) : pconfig,
  1025. repo.newObjectReader())) {
  1026. // prepare the PackWriter
  1027. pw.setDeltaBaseAsOffset(true);
  1028. pw.setReuseDeltaCommits(false);
  1029. if (tagTargets != null)
  1030. pw.setTagTargets(tagTargets);
  1031. if (excludeObjects != null)
  1032. for (ObjectIdSet idx : excludeObjects)
  1033. pw.excludeObjects(idx);
  1034. pw.preparePack(pm, want, have);
  1035. if (pw.getObjectCount() == 0)
  1036. return null;
  1037. checkCancelled();
  1038. // create temporary files
  1039. String id = pw.computeName().getName();
  1040. File packdir = new File(repo.getObjectsDirectory(), "pack"); //$NON-NLS-1$
  1041. tmpPack = File.createTempFile("gc_", ".pack_tmp", packdir); //$NON-NLS-1$ //$NON-NLS-2$
  1042. final String tmpBase = tmpPack.getName()
  1043. .substring(0, tmpPack.getName().lastIndexOf('.'));
  1044. File tmpIdx = new File(packdir, tmpBase + ".idx_tmp"); //$NON-NLS-1$
  1045. tmpExts.put(INDEX, tmpIdx);
  1046. if (!tmpIdx.createNewFile())
  1047. throw new IOException(MessageFormat.format(
  1048. JGitText.get().cannotCreateIndexfile, tmpIdx.getPath()));
  1049. // write the packfile
  1050. FileOutputStream fos = new FileOutputStream(tmpPack);
  1051. FileChannel channel = fos.getChannel();
  1052. OutputStream channelStream = Channels.newOutputStream(channel);
  1053. try {
  1054. pw.writePack(pm, pm, channelStream);
  1055. } finally {
  1056. channel.force(true);
  1057. channelStream.close();
  1058. fos.close();
  1059. }
  1060. // write the packindex
  1061. fos = new FileOutputStream(tmpIdx);
  1062. FileChannel idxChannel = fos.getChannel();
  1063. OutputStream idxStream = Channels.newOutputStream(idxChannel);
  1064. try {
  1065. pw.writeIndex(idxStream);
  1066. } finally {
  1067. idxChannel.force(true);
  1068. idxStream.close();
  1069. fos.close();
  1070. }
  1071. if (pw.prepareBitmapIndex(pm)) {
  1072. File tmpBitmapIdx = new File(packdir, tmpBase + ".bitmap_tmp"); //$NON-NLS-1$
  1073. tmpExts.put(BITMAP_INDEX, tmpBitmapIdx);
  1074. if (!tmpBitmapIdx.createNewFile())
  1075. throw new IOException(MessageFormat.format(
  1076. JGitText.get().cannotCreateIndexfile,
  1077. tmpBitmapIdx.getPath()));
  1078. fos = new FileOutputStream(tmpBitmapIdx);
  1079. idxChannel = fos.getChannel();
  1080. idxStream = Channels.newOutputStream(idxChannel);
  1081. try {
  1082. pw.writeBitmapIndex(idxStream);
  1083. } finally {
  1084. idxChannel.force(true);
  1085. idxStream.close();
  1086. fos.close();
  1087. }
  1088. }
  1089. // rename the temporary files to real files
  1090. File realPack = nameFor(id, ".pack"); //$NON-NLS-1$
  1091. // if the packfile already exists (because we are rewriting a
  1092. // packfile for the same set of objects maybe with different
  1093. // PackConfig) then make sure we get rid of all handles on the file.
  1094. // Windows will not allow for rename otherwise.
  1095. if (realPack.exists())
  1096. for (PackFile p : repo.getObjectDatabase().getPacks())
  1097. if (realPack.getPath().equals(p.getPackFile().getPath())) {
  1098. p.close();
  1099. break;
  1100. }
  1101. tmpPack.setReadOnly();
  1102. FileUtils.rename(tmpPack, realPack, StandardCopyOption.ATOMIC_MOVE);
  1103. for (Map.Entry<PackExt, File> tmpEntry : tmpExts.entrySet()) {
  1104. File tmpExt = tmpEntry.getValue();
  1105. tmpExt.setReadOnly();
  1106. File realExt = nameFor(id,
  1107. "." + tmpEntry.getKey().getExtension()); //$NON-NLS-1$
  1108. try {
  1109. FileUtils.rename(tmpExt, realExt,
  1110. StandardCopyOption.ATOMIC_MOVE);
  1111. } catch (IOException e) {
  1112. File newExt = new File(realExt.getParentFile(),
  1113. realExt.getName() + ".new"); //$NON-NLS-1$
  1114. try {
  1115. FileUtils.rename(tmpExt, newExt,
  1116. StandardCopyOption.ATOMIC_MOVE);
  1117. } catch (IOException e2) {
  1118. newExt = tmpExt;
  1119. e = e2;
  1120. }
  1121. throw new IOException(MessageFormat.format(
  1122. JGitText.get().panicCantRenameIndexFile, newExt,
  1123. realExt), e);
  1124. }
  1125. }
  1126. return repo.getObjectDatabase().openPack(realPack);
  1127. } finally {
  1128. if (tmpPack != null && tmpPack.exists())
  1129. tmpPack.delete();
  1130. for (File tmpExt : tmpExts.values()) {
  1131. if (tmpExt.exists())
  1132. tmpExt.delete();
  1133. }
  1134. }
  1135. }
  1136. private File nameFor(String name, String ext) {
  1137. File packdir = new File(repo.getObjectsDirectory(), "pack"); //$NON-NLS-1$
  1138. return new File(packdir, "pack-" + name + ext); //$NON-NLS-1$
  1139. }
  1140. private void checkCancelled() throws CancelledException {
  1141. if (pm.isCancelled()) {
  1142. throw new CancelledException(JGitText.get().operationCanceled);
  1143. }
  1144. }
  1145. /**
  1146. * A class holding statistical data for a FileRepository regarding how many
  1147. * objects are stored as loose or packed objects
  1148. */
  1149. public static class RepoStatistics {
  1150. /**
  1151. * The number of objects stored in pack files. If the same object is
  1152. * stored in multiple pack files then it is counted as often as it
  1153. * occurs in pack files.
  1154. */
  1155. public long numberOfPackedObjects;
  1156. /**
  1157. * The number of pack files
  1158. */
  1159. public long numberOfPackFiles;
  1160. /**
  1161. * The number of objects stored as loose objects.
  1162. */
  1163. public long numberOfLooseObjects;
  1164. /**
  1165. * The sum of the sizes of all files used to persist loose objects.
  1166. */
  1167. public long sizeOfLooseObjects;
  1168. /**
  1169. * The sum of the sizes of all pack files.
  1170. */
  1171. public long sizeOfPackedObjects;
  1172. /**
  1173. * The number of loose refs.
  1174. */
  1175. public long numberOfLooseRefs;
  1176. /**
  1177. * The number of refs stored in pack files.
  1178. */
  1179. public long numberOfPackedRefs;
  1180. /**
  1181. * The number of bitmaps in the bitmap indices.
  1182. */
  1183. public long numberOfBitmaps;
  1184. @Override
  1185. public String toString() {
  1186. final StringBuilder b = new StringBuilder();
  1187. b.append("numberOfPackedObjects=").append(numberOfPackedObjects); //$NON-NLS-1$
  1188. b.append(", numberOfPackFiles=").append(numberOfPackFiles); //$NON-NLS-1$
  1189. b.append(", numberOfLooseObjects=").append(numberOfLooseObjects); //$NON-NLS-1$
  1190. b.append(", numberOfLooseRefs=").append(numberOfLooseRefs); //$NON-NLS-1$
  1191. b.append(", numberOfPackedRefs=").append(numberOfPackedRefs); //$NON-NLS-1$
  1192. b.append(", sizeOfLooseObjects=").append(sizeOfLooseObjects); //$NON-NLS-1$
  1193. b.append(", sizeOfPackedObjects=").append(sizeOfPackedObjects); //$NON-NLS-1$
  1194. b.append(", numberOfBitmaps=").append(numberOfBitmaps); //$NON-NLS-1$
  1195. return b.toString();
  1196. }
  1197. }
  1198. /**
  1199. * Returns information about objects and pack files for a FileRepository.
  1200. *
  1201. * @return information about objects and pack files for a FileRepository
  1202. * @throws IOException
  1203. */
  1204. public RepoStatistics getStatistics() throws IOException {
  1205. RepoStatistics ret = new RepoStatistics();
  1206. Collection<PackFile> packs = repo.getObjectDatabase().getPacks();
  1207. for (PackFile f : packs) {
  1208. ret.numberOfPackedObjects += f.getIndex().getObjectCount();
  1209. ret.numberOfPackFiles++;
  1210. ret.sizeOfPackedObjects += f.getPackFile().length();
  1211. if (f.getBitmapIndex() != null)
  1212. ret.numberOfBitmaps += f.getBitmapIndex().getBitmapCount();
  1213. }
  1214. File objDir = repo.getObjectsDirectory();
  1215. String[] fanout = objDir.list();
  1216. if (fanout != null && fanout.length > 0) {
  1217. for (String d : fanout) {
  1218. if (d.length() != 2)
  1219. continue;
  1220. File[] entries = new File(objDir, d).listFiles();
  1221. if (entries == null)
  1222. continue;
  1223. for (File f : entries) {
  1224. if (f.getName().length() != Constants.OBJECT_ID_STRING_LENGTH - 2)
  1225. continue;
  1226. ret.numberOfLooseObjects++;
  1227. ret.sizeOfLooseObjects += f.length();
  1228. }
  1229. }
  1230. }
  1231. RefDatabase refDb = repo.getRefDatabase();
  1232. for (Ref r : refDb.getRefs(RefDatabase.ALL).values()) {
  1233. Storage storage = r.getStorage();
  1234. if (storage == Storage.LOOSE || storage == Storage.LOOSE_PACKED)
  1235. ret.numberOfLooseRefs++;
  1236. if (storage == Storage.PACKED || storage == Storage.LOOSE_PACKED)
  1237. ret.numberOfPackedRefs++;
  1238. }
  1239. return ret;
  1240. }
  1241. /**
  1242. * Set the progress monitor used for garbage collection methods.
  1243. *
  1244. * @param pm
  1245. * @return this
  1246. */
  1247. public GC setProgressMonitor(ProgressMonitor pm) {
  1248. this.pm = (pm == null) ? NullProgressMonitor.INSTANCE : pm;
  1249. return this;
  1250. }
  1251. /**
  1252. * During gc() or prune() each unreferenced, loose object which has been
  1253. * created or modified in the last <code>expireAgeMillis</code> milliseconds
  1254. * will not be pruned. Only older objects may be pruned. If set to 0 then
  1255. * every object is a candidate for pruning.
  1256. *
  1257. * @param expireAgeMillis
  1258. * minimal age of objects to be pruned in milliseconds.
  1259. */
  1260. public void setExpireAgeMillis(long expireAgeMillis) {
  1261. this.expireAgeMillis = expireAgeMillis;
  1262. expire = null;
  1263. }
  1264. /**
  1265. * During gc() or prune() packfiles which are created or modified in the
  1266. * last <code>packExpireAgeMillis</code> milliseconds will not be deleted.
  1267. * Only older packfiles may be deleted. If set to 0 then every packfile is a
  1268. * candidate for deletion.
  1269. *
  1270. * @param packExpireAgeMillis
  1271. * minimal age of packfiles to be deleted in milliseconds.
  1272. */
  1273. public void setPackExpireAgeMillis(long packExpireAgeMillis) {
  1274. this.packExpireAgeMillis = packExpireAgeMillis;
  1275. expire = null;
  1276. }
  1277. /**
  1278. * Set the PackConfig used when (re-)writing packfiles. This allows to
  1279. * influence how packs are written and to implement something similar to
  1280. * "git gc --aggressive"
  1281. *
  1282. * @param pconfig
  1283. * the {@link PackConfig} used when writing packs
  1284. */
  1285. public void setPackConfig(PackConfig pconfig) {
  1286. this.pconfig = pconfig;
  1287. }
  1288. /**
  1289. * During gc() or prune() each unreferenced, loose object which has been
  1290. * created or modified after or at <code>expire</code> will not be pruned.
  1291. * Only older objects may be pruned. If set to null then every object is a
  1292. * candidate for pruning.
  1293. *
  1294. * @param expire
  1295. * instant in time which defines object expiration
  1296. * objects with modification time before this instant are expired
  1297. * objects with modification time newer or equal to this instant
  1298. * are not expired
  1299. */
  1300. public void setExpire(Date expire) {
  1301. this.expire = expire;
  1302. expireAgeMillis = -1;
  1303. }
  1304. /**
  1305. * During gc() or prune() packfiles which are created or modified after or
  1306. * at <code>packExpire</code> will not be deleted. Only older packfiles may
  1307. * be deleted. If set to null then every packfile is a candidate for
  1308. * deletion.
  1309. *
  1310. * @param packExpire
  1311. * instant in time which defines packfile expiration
  1312. */
  1313. public void setPackExpire(Date packExpire) {
  1314. this.packExpire = packExpire;
  1315. packExpireAgeMillis = -1;
  1316. }
  1317. /**
  1318. * Set the {@code gc --auto} option.
  1319. *
  1320. * With this option, gc checks whether any housekeeping is required; if not,
  1321. * it exits without performing any work. Some JGit commands run
  1322. * {@code gc --auto} after performing operations that could create many
  1323. * loose objects.
  1324. * <p/>
  1325. * Housekeeping is required if there are too many loose objects or too many
  1326. * packs in the repository. If the number of loose objects exceeds the value
  1327. * of the gc.auto option JGit GC consolidates all existing packs into a
  1328. * single pack (equivalent to {@code -A} option), whereas git-core would
  1329. * combine all loose objects into a single pack using {@code repack -d -l}.
  1330. * Setting the value of {@code gc.auto} to 0 disables automatic packing of
  1331. * loose objects.
  1332. * <p/>
  1333. * If the number of packs exceeds the value of {@code gc.autoPackLimit},
  1334. * then existing packs (except those marked with a .keep file) are
  1335. * consolidated into a single pack by using the {@code -A} option of repack.
  1336. * Setting {@code gc.autoPackLimit} to 0 disables automatic consolidation of
  1337. * packs.
  1338. * <p/>
  1339. * Like git the following jgit commands run auto gc:
  1340. * <ul>
  1341. * <li>fetch</li>
  1342. * <li>merge</li>
  1343. * <li>rebase</li>
  1344. * <li>receive-pack</li>
  1345. * </ul>
  1346. * The auto gc for receive-pack can be suppressed by setting the config
  1347. * option {@code receive.autogc = false}
  1348. *
  1349. * @param auto
  1350. * defines whether gc should do automatic housekeeping
  1351. */
  1352. public void setAuto(boolean auto) {
  1353. this.automatic = auto;
  1354. }
  1355. /**
  1356. * @param background
  1357. * whether to run the gc in a background thread.
  1358. */
  1359. void setBackground(boolean background) {
  1360. this.background = background;
  1361. }
  1362. private boolean needGc() {
  1363. if (tooManyPacks()) {
  1364. addRepackAllOption();
  1365. } else if (!tooManyLooseObjects()) {
  1366. return false;
  1367. }
  1368. // TODO run pre-auto-gc hook, if it fails return false
  1369. return true;
  1370. }
  1371. private void addRepackAllOption() {
  1372. // TODO: if JGit GC is enhanced to support repack's option -l this
  1373. // method needs to be implemented
  1374. }
  1375. /**
  1376. * @return {@code true} if number of packs > gc.autopacklimit (default 50)
  1377. */
  1378. boolean tooManyPacks() {
  1379. int autopacklimit = repo.getConfig().getInt(
  1380. ConfigConstants.CONFIG_GC_SECTION,
  1381. ConfigConstants.CONFIG_KEY_AUTOPACKLIMIT,
  1382. DEFAULT_AUTOPACKLIMIT);
  1383. if (autopacklimit <= 0) {
  1384. return false;
  1385. }
  1386. // JGit always creates two packfiles, one for the objects reachable from
  1387. // branches, and another one for the rest
  1388. return repo.getObjectDatabase().getPacks().size() > (autopacklimit + 1);
  1389. }
  1390. /**
  1391. * Quickly estimate number of loose objects, SHA1 is distributed evenly so
  1392. * counting objects in one directory (bucket 17) is sufficient
  1393. *
  1394. * @return {@code true} if number of loose objects > gc.auto (default 6700)
  1395. */
  1396. boolean tooManyLooseObjects() {
  1397. int auto = getLooseObjectLimit();
  1398. if (auto <= 0) {
  1399. return false;
  1400. }
  1401. int n = 0;
  1402. int threshold = (auto + 255) / 256;
  1403. Path dir = repo.getObjectsDirectory().toPath().resolve("17"); //$NON-NLS-1$
  1404. if (!Files.exists(dir)) {
  1405. return false;
  1406. }
  1407. try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir,
  1408. new DirectoryStream.Filter<Path>() {
  1409. @Override
  1410. public boolean accept(Path file) throws IOException {
  1411. Path fileName = file.getFileName();
  1412. return Files.isRegularFile(file) && fileName != null
  1413. && PATTERN_LOOSE_OBJECT
  1414. .matcher(fileName.toString()).matches();
  1415. }
  1416. })) {
  1417. for (Iterator<Path> iter = stream.iterator(); iter.hasNext();
  1418. iter.next()) {
  1419. if (++n > threshold) {
  1420. return true;
  1421. }
  1422. }
  1423. } catch (IOException e) {
  1424. LOG.error(e.getMessage(), e);
  1425. }
  1426. return false;
  1427. }
  1428. private int getLooseObjectLimit() {
  1429. return repo.getConfig().getInt(ConfigConstants.CONFIG_GC_SECTION,
  1430. ConfigConstants.CONFIG_KEY_AUTO, DEFAULT_AUTOLIMIT);
  1431. }
  1432. }