1
2
3
4
5
6
7
8
9
10
11 package org.eclipse.jgit.internal.storage.file;
12
13 import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
14 import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
15 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
16 import static org.eclipse.jgit.internal.storage.pack.PackExt.KEEP;
17
18 import java.io.File;
19 import java.io.FileOutputStream;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.io.PrintWriter;
23 import java.io.StringWriter;
24 import java.nio.channels.Channels;
25 import java.nio.channels.FileChannel;
26 import java.nio.file.DirectoryNotEmptyException;
27 import java.nio.file.DirectoryStream;
28 import java.nio.file.Files;
29 import java.nio.file.Path;
30 import java.nio.file.StandardCopyOption;
31 import java.text.MessageFormat;
32 import java.text.ParseException;
33 import java.time.Instant;
34 import java.time.temporal.ChronoUnit;
35 import java.util.ArrayList;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.Comparator;
39 import java.util.Date;
40 import java.util.HashMap;
41 import java.util.HashSet;
42 import java.util.Iterator;
43 import java.util.LinkedList;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Objects;
47 import java.util.Set;
48 import java.util.TreeMap;
49 import java.util.concurrent.Callable;
50 import java.util.concurrent.ExecutorService;
51 import java.util.regex.Pattern;
52 import java.util.stream.Collectors;
53 import java.util.stream.Stream;
54
55 import org.eclipse.jgit.annotations.NonNull;
56 import org.eclipse.jgit.dircache.DirCacheIterator;
57 import org.eclipse.jgit.errors.CancelledException;
58 import org.eclipse.jgit.errors.CorruptObjectException;
59 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
60 import org.eclipse.jgit.errors.MissingObjectException;
61 import org.eclipse.jgit.errors.NoWorkTreeException;
62 import org.eclipse.jgit.internal.JGitText;
63 import org.eclipse.jgit.internal.storage.pack.PackExt;
64 import org.eclipse.jgit.internal.storage.pack.PackWriter;
65 import org.eclipse.jgit.lib.ConfigConstants;
66 import org.eclipse.jgit.lib.Constants;
67 import org.eclipse.jgit.lib.FileMode;
68 import org.eclipse.jgit.lib.NullProgressMonitor;
69 import org.eclipse.jgit.lib.ObjectId;
70 import org.eclipse.jgit.lib.ObjectIdSet;
71 import org.eclipse.jgit.lib.ObjectLoader;
72 import org.eclipse.jgit.lib.ObjectReader;
73 import org.eclipse.jgit.lib.ProgressMonitor;
74 import org.eclipse.jgit.lib.Ref;
75 import org.eclipse.jgit.lib.Ref.Storage;
76 import org.eclipse.jgit.lib.RefDatabase;
77 import org.eclipse.jgit.lib.ReflogEntry;
78 import org.eclipse.jgit.lib.ReflogReader;
79 import org.eclipse.jgit.lib.internal.WorkQueue;
80 import org.eclipse.jgit.revwalk.ObjectWalk;
81 import org.eclipse.jgit.revwalk.RevObject;
82 import org.eclipse.jgit.revwalk.RevWalk;
83 import org.eclipse.jgit.storage.pack.PackConfig;
84 import org.eclipse.jgit.treewalk.TreeWalk;
85 import org.eclipse.jgit.treewalk.filter.TreeFilter;
86 import org.eclipse.jgit.util.FileUtils;
87 import org.eclipse.jgit.util.GitDateParser;
88 import org.eclipse.jgit.util.SystemReader;
89 import org.slf4j.Logger;
90 import org.slf4j.LoggerFactory;
91
92
93
94
95
96
97
98
99
100
101 public class GC {
102 private static final Logger LOG = LoggerFactory
103 .getLogger(GC.class);
104
105 private static final String PRUNE_EXPIRE_DEFAULT = "2.weeks.ago";
106
107 private static final String PRUNE_PACK_EXPIRE_DEFAULT = "1.hour.ago";
108
109 private static final Pattern PATTERN_LOOSE_OBJECT = Pattern
110 .compile("[0-9a-fA-F]{38}");
111
112 private static final String PACK_EXT = "." + PackExt.PACK.getExtension();
113
114 private static final String BITMAP_EXT = "."
115 + PackExt.BITMAP_INDEX.getExtension();
116
117 private static final String INDEX_EXT = "." + PackExt.INDEX.getExtension();
118
119 private static final String KEEP_EXT = "." + PackExt.KEEP.getExtension();
120
121 private static final int DEFAULT_AUTOPACKLIMIT = 50;
122
123 private static final int DEFAULT_AUTOLIMIT = 6700;
124
125 private static volatile ExecutorService executor;
126
127
128
129
130
131
132
133
134 public static void setExecutor(ExecutorService e) {
135 executor = e;
136 }
137
138 private final FileRepository repo;
139
140 private ProgressMonitor pm;
141
142 private long expireAgeMillis = -1;
143
144 private Date expire;
145
146 private long packExpireAgeMillis = -1;
147
148 private Date packExpire;
149
150 private PackConfig pconfig;
151
152
153
154
155
156
157
158 private Collection<Ref> lastPackedRefs;
159
160
161
162
163
164
165 private long lastRepackTime;
166
167
168
169
170 private boolean automatic;
171
172
173
174
175 private boolean background;
176
177
178
179
180
181
182
183
184 public GC(FileRepository repo) {
185 this.repo = repo;
186 this.pconfig = new PackConfig(repo);
187 this.pm = NullProgressMonitor.INSTANCE;
188 }
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218 @SuppressWarnings("FutureReturnValueIgnored")
219 public Collection<Pack> gc() throws IOException, ParseException {
220 if (!background) {
221 return doGc();
222 }
223 final GcLog gcLog = new GcLog(repo);
224 if (!gcLog.lock()) {
225
226 return Collections.emptyList();
227 }
228
229 Callable<Collection<Pack>> gcTask = () -> {
230 try {
231 Collection<Pack> newPacks = doGc();
232 if (automatic && tooManyLooseObjects()) {
233 String message = JGitText.get().gcTooManyUnpruned;
234 gcLog.write(message);
235 gcLog.commit();
236 }
237 return newPacks;
238 } catch (IOException | ParseException e) {
239 try {
240 gcLog.write(e.getMessage());
241 StringWriter sw = new StringWriter();
242 e.printStackTrace(new PrintWriter(sw));
243 gcLog.write(sw.toString());
244 gcLog.commit();
245 } catch (IOException e2) {
246 e2.addSuppressed(e);
247 LOG.error(e2.getMessage(), e2);
248 }
249 } finally {
250 gcLog.unlock();
251 }
252 return Collections.emptyList();
253 };
254
255 executor().submit(gcTask);
256 return Collections.emptyList();
257 }
258
259 private ExecutorService executor() {
260 return (executor != null) ? executor : WorkQueue.getExecutor();
261 }
262
263 private Collection<Pack> doGc() throws IOException, ParseException {
264 if (automatic && !needGc()) {
265 return Collections.emptyList();
266 }
267 pm.start(6 );
268 packRefs();
269
270 Collection<Pack> newPacks = repack();
271 prune(Collections.emptySet());
272
273 return newPacks;
274 }
275
276
277
278
279
280
281
282
283
284
285
286 private void loosen(ObjectDirectoryInserter inserter, ObjectReader reader, Pack pack, HashSet<ObjectId> existing)
287 throws IOException {
288 for (PackIndex.MutableEntry entry : pack) {
289 ObjectId oid = entry.toObjectId();
290 if (existing.contains(oid)) {
291 continue;
292 }
293 existing.add(oid);
294 ObjectLoader loader = reader.open(oid);
295 inserter.insert(loader.getType(),
296 loader.getSize(),
297 loader.openStream(),
298 true );
299 }
300 }
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318 private void deleteOldPacks(Collection<Pack> oldPacks,
319 Collection<Pack> newPacks) throws ParseException, IOException {
320 HashSet<ObjectId> ids = new HashSet<>();
321 for (Pack pack : newPacks) {
322 for (PackIndex.MutableEntry entry : pack) {
323 ids.add(entry.toObjectId());
324 }
325 }
326 ObjectReader reader = repo.newObjectReader();
327 ObjectDirectory dir = repo.getObjectDatabase();
328 ObjectDirectoryInserter inserter = dir.newInserter();
329 boolean shouldLoosen = !"now".equals(getPruneExpireStr()) &&
330 getExpireDate() < Long.MAX_VALUE;
331
332 prunePreserved();
333 long packExpireDate = getPackExpireDate();
334 oldPackLoop: for (Pack oldPack : oldPacks) {
335 checkCancelled();
336 String oldName = oldPack.getPackName();
337
338
339 for (Pack newPack : newPacks)
340 if (oldName.equals(newPack.getPackName()))
341 continue oldPackLoop;
342
343 if (!oldPack.shouldBeKept()
344 && repo.getFS()
345 .lastModifiedInstant(oldPack.getPackFile())
346 .toEpochMilli() < packExpireDate) {
347 if (shouldLoosen) {
348 loosen(inserter, reader, oldPack, ids);
349 }
350 oldPack.close();
351 prunePack(oldPack.getPackFile());
352 }
353 }
354
355
356
357 repo.getObjectDatabase().close();
358 }
359
360
361
362
363
364
365
366
367
368 private void removeOldPack(PackFile packFile, int deleteOptions)
369 throws IOException {
370 if (pconfig.isPreserveOldPacks()) {
371 File oldPackDir = repo.getObjectDatabase().getPreservedDirectory();
372 FileUtils.mkdir(oldPackDir, true);
373
374 PackFile oldPackFile = packFile
375 .createPreservedForDirectory(oldPackDir);
376 FileUtils.rename(packFile, oldPackFile);
377 } else {
378 FileUtils.delete(packFile, deleteOptions);
379 }
380 }
381
382
383
384
385 private void prunePreserved() {
386 if (pconfig.isPrunePreserved()) {
387 try {
388 FileUtils.delete(repo.getObjectDatabase().getPreservedDirectory(),
389 FileUtils.RECURSIVE | FileUtils.RETRY | FileUtils.SKIP_MISSING);
390 } catch (IOException e) {
391
392 }
393 }
394 }
395
396
397
398
399
400
401
402
403
404
405
406 private void prunePack(PackFile packFile) {
407 try {
408
409
410 int deleteOptions = FileUtils.RETRY | FileUtils.SKIP_MISSING;
411 removeOldPack(packFile.create(PackExt.PACK), deleteOptions);
412
413
414
415 deleteOptions |= FileUtils.IGNORE_ERRORS;
416 for (PackExt ext : PackExt.values()) {
417 if (!PackExt.PACK.equals(ext)) {
418 removeOldPack(packFile.create(ext), deleteOptions);
419 }
420 }
421 } catch (IOException e) {
422
423 }
424 }
425
426
427
428
429
430
431
432
433 public void prunePacked() throws IOException {
434 ObjectDirectory objdb = repo.getObjectDatabase();
435 Collection<Pack> packs = objdb.getPacks();
436 File objects = repo.getObjectsDirectory();
437 String[] fanout = objects.list();
438
439 if (fanout != null && fanout.length > 0) {
440 pm.beginTask(JGitText.get().pruneLoosePackedObjects, fanout.length);
441 try {
442 for (String d : fanout) {
443 checkCancelled();
444 pm.update(1);
445 if (d.length() != 2)
446 continue;
447 String[] entries = new File(objects, d).list();
448 if (entries == null)
449 continue;
450 for (String e : entries) {
451 checkCancelled();
452 if (e.length() != Constants.OBJECT_ID_STRING_LENGTH - 2)
453 continue;
454 ObjectId id;
455 try {
456 id = ObjectId.fromString(d + e);
457 } catch (IllegalArgumentException notAnObject) {
458
459
460 continue;
461 }
462 boolean found = false;
463 for (Pack p : packs) {
464 checkCancelled();
465 if (p.hasObject(id)) {
466 found = true;
467 break;
468 }
469 }
470 if (found)
471 FileUtils.delete(objdb.fileFor(id), FileUtils.RETRY
472 | FileUtils.SKIP_MISSING
473 | FileUtils.IGNORE_ERRORS);
474 }
475 }
476 } finally {
477 pm.endTask();
478 }
479 }
480 }
481
482
483
484
485
486
487
488
489
490
491
492
493
494 public void prune(Set<ObjectId> objectsToKeep) throws IOException,
495 ParseException {
496 long expireDate = getExpireDate();
497
498
499
500 Map<ObjectId, File> deletionCandidates = new HashMap<>();
501 Set<ObjectId> indexObjects = null;
502 File objects = repo.getObjectsDirectory();
503 String[] fanout = objects.list();
504 if (fanout == null || fanout.length == 0) {
505 return;
506 }
507 pm.beginTask(JGitText.get().pruneLooseUnreferencedObjects,
508 fanout.length);
509 try {
510 for (String d : fanout) {
511 checkCancelled();
512 pm.update(1);
513 if (d.length() != 2)
514 continue;
515 File dir = new File(objects, d);
516 File[] entries = dir.listFiles();
517 if (entries == null || entries.length == 0) {
518 FileUtils.delete(dir, FileUtils.IGNORE_ERRORS);
519 continue;
520 }
521 for (File f : entries) {
522 checkCancelled();
523 String fName = f.getName();
524 if (fName.length() != Constants.OBJECT_ID_STRING_LENGTH - 2)
525 continue;
526 if (repo.getFS().lastModifiedInstant(f)
527 .toEpochMilli() >= expireDate) {
528 continue;
529 }
530 try {
531 ObjectId id = ObjectId.fromString(d + fName);
532 if (objectsToKeep.contains(id))
533 continue;
534 if (indexObjects == null)
535 indexObjects = listNonHEADIndexObjects();
536 if (indexObjects.contains(id))
537 continue;
538 deletionCandidates.put(id, f);
539 } catch (IllegalArgumentException notAnObject) {
540
541
542 }
543 }
544 }
545 } finally {
546 pm.endTask();
547 }
548
549 if (deletionCandidates.isEmpty()) {
550 return;
551 }
552
553 checkCancelled();
554
555
556
557
558
559 Collection<Ref> newRefs;
560 if (lastPackedRefs == null || lastPackedRefs.isEmpty())
561 newRefs = getAllRefs();
562 else {
563 Map<String, Ref> last = new HashMap<>();
564 for (Ref r : lastPackedRefs) {
565 last.put(r.getName(), r);
566 }
567 newRefs = new ArrayList<>();
568 for (Ref r : getAllRefs()) {
569 Ref old = last.get(r.getName());
570 if (!equals(r, old)) {
571 newRefs.add(r);
572 }
573 }
574 }
575
576 if (!newRefs.isEmpty()) {
577
578
579
580
581
582 ObjectWalk w = new ObjectWalk(repo);
583 try {
584 for (Ref cr : newRefs) {
585 checkCancelled();
586 w.markStart(w.parseAny(cr.getObjectId()));
587 }
588 if (lastPackedRefs != null)
589 for (Ref lpr : lastPackedRefs) {
590 w.markUninteresting(w.parseAny(lpr.getObjectId()));
591 }
592 removeReferenced(deletionCandidates, w);
593 } finally {
594 w.dispose();
595 }
596 }
597
598 if (deletionCandidates.isEmpty())
599 return;
600
601
602
603
604
605
606 ObjectWalk w = new ObjectWalk(repo);
607 try {
608 for (Ref ar : getAllRefs())
609 for (ObjectId id : listRefLogObjects(ar, lastRepackTime)) {
610 checkCancelled();
611 w.markStart(w.parseAny(id));
612 }
613 if (lastPackedRefs != null)
614 for (Ref lpr : lastPackedRefs) {
615 checkCancelled();
616 w.markUninteresting(w.parseAny(lpr.getObjectId()));
617 }
618 removeReferenced(deletionCandidates, w);
619 } finally {
620 w.dispose();
621 }
622
623 if (deletionCandidates.isEmpty())
624 return;
625
626 checkCancelled();
627
628
629
630
631
632 Set<File> touchedFanout = new HashSet<>();
633 for (File f : deletionCandidates.values()) {
634 if (f.lastModified() < expireDate) {
635 f.delete();
636 touchedFanout.add(f.getParentFile());
637 }
638 }
639
640 for (File f : touchedFanout) {
641 FileUtils.delete(f,
642 FileUtils.EMPTY_DIRECTORIES_ONLY | FileUtils.IGNORE_ERRORS);
643 }
644
645 repo.getObjectDatabase().close();
646 }
647
648 private long getExpireDate() throws ParseException {
649 long expireDate = Long.MAX_VALUE;
650
651 if (expire == null && expireAgeMillis == -1) {
652 String pruneExpireStr = getPruneExpireStr();
653 if (pruneExpireStr == null)
654 pruneExpireStr = PRUNE_EXPIRE_DEFAULT;
655 expire = GitDateParser.parse(pruneExpireStr, null, SystemReader
656 .getInstance().getLocale());
657 expireAgeMillis = -1;
658 }
659 if (expire != null)
660 expireDate = expire.getTime();
661 if (expireAgeMillis != -1)
662 expireDate = System.currentTimeMillis() - expireAgeMillis;
663 return expireDate;
664 }
665
666 private String getPruneExpireStr() {
667 return repo.getConfig().getString(
668 ConfigConstants.CONFIG_GC_SECTION, null,
669 ConfigConstants.CONFIG_KEY_PRUNEEXPIRE);
670 }
671
672 private long getPackExpireDate() throws ParseException {
673 long packExpireDate = Long.MAX_VALUE;
674
675 if (packExpire == null && packExpireAgeMillis == -1) {
676 String prunePackExpireStr = repo.getConfig().getString(
677 ConfigConstants.CONFIG_GC_SECTION, null,
678 ConfigConstants.CONFIG_KEY_PRUNEPACKEXPIRE);
679 if (prunePackExpireStr == null)
680 prunePackExpireStr = PRUNE_PACK_EXPIRE_DEFAULT;
681 packExpire = GitDateParser.parse(prunePackExpireStr, null,
682 SystemReader.getInstance().getLocale());
683 packExpireAgeMillis = -1;
684 }
685 if (packExpire != null)
686 packExpireDate = packExpire.getTime();
687 if (packExpireAgeMillis != -1)
688 packExpireDate = System.currentTimeMillis() - packExpireAgeMillis;
689 return packExpireDate;
690 }
691
692
693
694
695
696
697
698
699
700
701
702 private void removeReferenced(Map<ObjectId, File> id2File,
703 ObjectWalk w) throws MissingObjectException,
704 IncorrectObjectTypeException, IOException {
705 RevObject ro = w.next();
706 while (ro != null) {
707 checkCancelled();
708 if (id2File.remove(ro.getId()) != null && id2File.isEmpty()) {
709 return;
710 }
711 ro = w.next();
712 }
713 ro = w.nextObject();
714 while (ro != null) {
715 checkCancelled();
716 if (id2File.remove(ro.getId()) != null && id2File.isEmpty()) {
717 return;
718 }
719 ro = w.nextObject();
720 }
721 }
722
723 private static boolean equals(Ref r1, Ref r2) {
724 if (r1 == null || r2 == null) {
725 return false;
726 }
727 if (r1.isSymbolic()) {
728 return r2.isSymbolic() && r1.getTarget().getName()
729 .equals(r2.getTarget().getName());
730 }
731 return !r2.isSymbolic()
732 && Objects.equals(r1.getObjectId(), r2.getObjectId());
733 }
734
735
736
737
738
739
740
741
742 public void packRefs() throws IOException {
743 RefDatabase refDb = repo.getRefDatabase();
744 if (refDb instanceof FileReftableDatabase) {
745
746 pm.beginTask(JGitText.get().packRefs, 1);
747 try {
748 ((FileReftableDatabase) refDb).compactFully();
749 } finally {
750 pm.endTask();
751 }
752 return;
753 }
754
755 Collection<Ref> refs = refDb.getRefsByPrefix(Constants.R_REFS);
756 List<String> refsToBePacked = new ArrayList<>(refs.size());
757 pm.beginTask(JGitText.get().packRefs, refs.size());
758 try {
759 for (Ref ref : refs) {
760 checkCancelled();
761 if (!ref.isSymbolic() && ref.getStorage().isLoose())
762 refsToBePacked.add(ref.getName());
763 pm.update(1);
764 }
765 ((RefDirectory) repo.getRefDatabase()).pack(refsToBePacked);
766 } finally {
767 pm.endTask();
768 }
769 }
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785 public Collection<Pack> repack() throws IOException {
786 Collection<Pack> toBeDeleted = repo.getObjectDatabase().getPacks();
787
788 long time = System.currentTimeMillis();
789 Collection<Ref> refsBefore = getAllRefs();
790
791 Set<ObjectId> allHeadsAndTags = new HashSet<>();
792 Set<ObjectId> allHeads = new HashSet<>();
793 Set<ObjectId> allTags = new HashSet<>();
794 Set<ObjectId> nonHeads = new HashSet<>();
795 Set<ObjectId> txnHeads = new HashSet<>();
796 Set<ObjectId> tagTargets = new HashSet<>();
797 Set<ObjectId> indexObjects = listNonHEADIndexObjects();
798
799 for (Ref ref : refsBefore) {
800 checkCancelled();
801 nonHeads.addAll(listRefLogObjects(ref, 0));
802 if (ref.isSymbolic() || ref.getObjectId() == null) {
803 continue;
804 }
805 if (isHead(ref)) {
806 allHeads.add(ref.getObjectId());
807 } else if (isTag(ref)) {
808 allTags.add(ref.getObjectId());
809 } else {
810 nonHeads.add(ref.getObjectId());
811 }
812 if (ref.getPeeledObjectId() != null) {
813 tagTargets.add(ref.getPeeledObjectId());
814 }
815 }
816
817 List<ObjectIdSet> excluded = new LinkedList<>();
818 for (Pack p : repo.getObjectDatabase().getPacks()) {
819 checkCancelled();
820 if (p.shouldBeKept())
821 excluded.add(p.getIndex());
822 }
823
824
825 allTags.removeAll(allHeads);
826 allHeadsAndTags.addAll(allHeads);
827 allHeadsAndTags.addAll(allTags);
828
829
830 tagTargets.addAll(allHeadsAndTags);
831 nonHeads.addAll(indexObjects);
832
833
834 if (pconfig.getSinglePack()) {
835 allHeadsAndTags.addAll(nonHeads);
836 nonHeads.clear();
837 }
838
839 List<Pack> ret = new ArrayList<>(2);
840 Pack heads = null;
841 if (!allHeadsAndTags.isEmpty()) {
842 heads = writePack(allHeadsAndTags, PackWriter.NONE, allTags,
843 tagTargets, excluded);
844 if (heads != null) {
845 ret.add(heads);
846 excluded.add(0, heads.getIndex());
847 }
848 }
849 if (!nonHeads.isEmpty()) {
850 Pack rest = writePack(nonHeads, allHeadsAndTags, PackWriter.NONE,
851 tagTargets, excluded);
852 if (rest != null)
853 ret.add(rest);
854 }
855 if (!txnHeads.isEmpty()) {
856 Pack txn = writePack(txnHeads, PackWriter.NONE, PackWriter.NONE,
857 null, excluded);
858 if (txn != null)
859 ret.add(txn);
860 }
861 try {
862 deleteOldPacks(toBeDeleted, ret);
863 } catch (ParseException e) {
864
865
866
867 throw new IOException(e);
868 }
869 prunePacked();
870 if (repo.getRefDatabase() instanceof RefDirectory) {
871
872 deleteEmptyRefsFolders();
873 }
874 deleteOrphans();
875 deleteTempPacksIdx();
876
877 lastPackedRefs = refsBefore;
878 lastRepackTime = time;
879 return ret;
880 }
881
882 private static boolean isHead(Ref ref) {
883 return ref.getName().startsWith(Constants.R_HEADS);
884 }
885
886 private static boolean isTag(Ref ref) {
887 return ref.getName().startsWith(Constants.R_TAGS);
888 }
889
890 private void deleteEmptyRefsFolders() throws IOException {
891 Path refs = repo.getDirectory().toPath().resolve(Constants.R_REFS);
892
893
894 Instant threshold = Instant.now().minus(30, ChronoUnit.SECONDS);
895 try (Stream<Path> entries = Files.list(refs)
896 .filter(Files::isDirectory)) {
897 Iterator<Path> iterator = entries.iterator();
898 while (iterator.hasNext()) {
899 try (Stream<Path> s = Files.list(iterator.next())) {
900 s.filter(path -> canBeSafelyDeleted(path, threshold)).forEach(this::deleteDir);
901 }
902 }
903 }
904 }
905
906 private boolean canBeSafelyDeleted(Path path, Instant threshold) {
907 try {
908 return Files.getLastModifiedTime(path).toInstant().isBefore(threshold);
909 }
910 catch (IOException e) {
911 LOG.warn(MessageFormat.format(
912 JGitText.get().cannotAccessLastModifiedForSafeDeletion,
913 path), e);
914 return false;
915 }
916 }
917
918 private void deleteDir(Path dir) {
919 try (Stream<Path> dirs = Files.walk(dir)) {
920 dirs.filter(this::isDirectory).sorted(Comparator.reverseOrder())
921 .forEach(this::delete);
922 } catch (IOException e) {
923 LOG.error(e.getMessage(), e);
924 }
925 }
926
927 private boolean isDirectory(Path p) {
928 return p.toFile().isDirectory();
929 }
930
931 private void delete(Path d) {
932 try {
933 Files.delete(d);
934 } catch (DirectoryNotEmptyException e) {
935
936 } catch (IOException e) {
937 LOG.error(MessageFormat.format(JGitText.get().cannotDeleteFile, d),
938 e);
939 }
940 }
941
942
943
944
945
946
947
948
949 private void deleteOrphans() {
950 Path packDir = repo.getObjectDatabase().getPackDirectory().toPath();
951 List<String> fileNames = null;
952 try (Stream<Path> files = Files.list(packDir)) {
953 fileNames = files.map(path -> path.getFileName().toString())
954 .filter(name -> (name.endsWith(PACK_EXT)
955 || name.endsWith(BITMAP_EXT)
956 || name.endsWith(INDEX_EXT)
957 || name.endsWith(KEEP_EXT)))
958
959
960 .sorted(Collections.reverseOrder())
961 .collect(Collectors.toList());
962 } catch (IOException e) {
963 LOG.error(e.getMessage(), e);
964 return;
965 }
966 if (fileNames == null) {
967 return;
968 }
969
970 String latestId = null;
971 for (String n : fileNames) {
972 PackFile pf = new PackFile(packDir.toFile(), n);
973 PackExt ext = pf.getPackExt();
974 if (ext.equals(PACK) || ext.equals(KEEP)) {
975 latestId = pf.getId();
976 }
977 if (latestId == null || !pf.getId().equals(latestId)) {
978
979 try {
980 FileUtils.delete(pf,
981 FileUtils.RETRY | FileUtils.SKIP_MISSING);
982 LOG.warn(JGitText.get().deletedOrphanInPackDir, pf);
983 } catch (IOException e) {
984 LOG.error(e.getMessage(), e);
985 }
986 }
987 }
988 }
989
990 private void deleteTempPacksIdx() {
991 Path packDir = repo.getObjectDatabase().getPackDirectory().toPath();
992 Instant threshold = Instant.now().minus(1, ChronoUnit.DAYS);
993 if (!Files.exists(packDir)) {
994 return;
995 }
996 try (DirectoryStream<Path> stream =
997 Files.newDirectoryStream(packDir, "gc_*_tmp")) {
998 stream.forEach(t -> {
999 try {
1000 Instant lastModified = Files.getLastModifiedTime(t)
1001 .toInstant();
1002 if (lastModified.isBefore(threshold)) {
1003 Files.deleteIfExists(t);
1004 }
1005 } catch (IOException e) {
1006 LOG.error(e.getMessage(), e);
1007 }
1008 });
1009 } catch (IOException e) {
1010 LOG.error(e.getMessage(), e);
1011 }
1012 }
1013
1014
1015
1016
1017
1018
1019
1020
1021 private Set<ObjectId> listRefLogObjects(Ref ref, long minTime) throws IOException {
1022 ReflogReader reflogReader = repo.getReflogReader(ref.getName());
1023 if (reflogReader == null) {
1024 return Collections.emptySet();
1025 }
1026 List<ReflogEntry> rlEntries = reflogReader
1027 .getReverseEntries();
1028 if (rlEntries == null || rlEntries.isEmpty())
1029 return Collections.emptySet();
1030 Set<ObjectId> ret = new HashSet<>();
1031 for (ReflogEntry e : rlEntries) {
1032 if (e.getWho().getWhen().getTime() < minTime)
1033 break;
1034 ObjectId newId = e.getNewId();
1035 if (newId != null && !ObjectId.zeroId().equals(newId))
1036 ret.add(newId);
1037 ObjectId oldId = e.getOldId();
1038 if (oldId != null && !ObjectId.zeroId().equals(oldId))
1039 ret.add(oldId);
1040 }
1041 return ret;
1042 }
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055 private Collection<Ref> getAllRefs() throws IOException {
1056 RefDatabase refdb = repo.getRefDatabase();
1057 Collection<Ref> refs = refdb.getRefs();
1058 List<Ref> addl = refdb.getAdditionalRefs();
1059 if (!addl.isEmpty()) {
1060 List<Ref> all = new ArrayList<>(refs.size() + addl.size());
1061 all.addAll(refs);
1062
1063 for (Ref r : addl) {
1064 checkCancelled();
1065 if (r.getName().startsWith(Constants.R_REFS)) {
1066 all.add(r);
1067 }
1068 }
1069 return all;
1070 }
1071 return refs;
1072 }
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083 private Set<ObjectId> listNonHEADIndexObjects()
1084 throws CorruptObjectException, IOException {
1085 if (repo.isBare()) {
1086 return Collections.emptySet();
1087 }
1088 try (TreeWalk treeWalk = new TreeWalk(repo)) {
1089 treeWalk.addTree(new DirCacheIterator(repo.readDirCache()));
1090 ObjectId headID = repo.resolve(Constants.HEAD);
1091 if (headID != null) {
1092 try (RevWalk revWalk = new RevWalk(repo)) {
1093 treeWalk.addTree(revWalk.parseTree(headID));
1094 }
1095 }
1096
1097 treeWalk.setFilter(TreeFilter.ANY_DIFF);
1098 treeWalk.setRecursive(true);
1099 Set<ObjectId> ret = new HashSet<>();
1100
1101 while (treeWalk.next()) {
1102 checkCancelled();
1103 ObjectId objectId = treeWalk.getObjectId(0);
1104 switch (treeWalk.getRawMode(0) & FileMode.TYPE_MASK) {
1105 case FileMode.TYPE_MISSING:
1106 case FileMode.TYPE_GITLINK:
1107 continue;
1108 case FileMode.TYPE_TREE:
1109 case FileMode.TYPE_FILE:
1110 case FileMode.TYPE_SYMLINK:
1111 ret.add(objectId);
1112 continue;
1113 default:
1114 throw new IOException(MessageFormat.format(
1115 JGitText.get().corruptObjectInvalidMode3,
1116 String.format("%o",
1117 Integer.valueOf(treeWalk.getRawMode(0))),
1118 (objectId == null) ? "null" : objectId.name(),
1119 treeWalk.getPathString(),
1120 repo.getIndexFile()));
1121 }
1122 }
1123 return ret;
1124 }
1125 }
1126
1127 private Pack writePack(@NonNull Set<? extends ObjectId> want,
1128 @NonNull Set<? extends ObjectId> have, @NonNull Set<ObjectId> tags,
1129 Set<ObjectId> tagTargets, List<ObjectIdSet> excludeObjects)
1130 throws IOException {
1131 checkCancelled();
1132 File tmpPack = null;
1133 Map<PackExt, File> tmpExts = new TreeMap<>((o1, o2) -> {
1134
1135
1136
1137 if (o1 == o2) {
1138 return 0;
1139 }
1140 if (o1 == PackExt.INDEX) {
1141 return 1;
1142 }
1143 if (o2 == PackExt.INDEX) {
1144 return -1;
1145 }
1146 return Integer.signum(o1.hashCode() - o2.hashCode());
1147 });
1148 try (PackWriter pw = new PackWriter(
1149 pconfig,
1150 repo.newObjectReader())) {
1151
1152 pw.setDeltaBaseAsOffset(true);
1153 pw.setReuseDeltaCommits(false);
1154 if (tagTargets != null) {
1155 pw.setTagTargets(tagTargets);
1156 }
1157 if (excludeObjects != null)
1158 for (ObjectIdSet idx : excludeObjects)
1159 pw.excludeObjects(idx);
1160 pw.preparePack(pm, want, have, PackWriter.NONE, tags);
1161 if (pw.getObjectCount() == 0)
1162 return null;
1163 checkCancelled();
1164
1165
1166 ObjectId id = pw.computeName();
1167 File packdir = repo.getObjectDatabase().getPackDirectory();
1168 packdir.mkdirs();
1169 tmpPack = File.createTempFile("gc_", ".pack_tmp", packdir);
1170 final String tmpBase = tmpPack.getName()
1171 .substring(0, tmpPack.getName().lastIndexOf('.'));
1172 File tmpIdx = new File(packdir, tmpBase + ".idx_tmp");
1173 tmpExts.put(INDEX, tmpIdx);
1174
1175 if (!tmpIdx.createNewFile())
1176 throw new IOException(MessageFormat.format(
1177 JGitText.get().cannotCreateIndexfile, tmpIdx.getPath()));
1178
1179
1180 try (FileOutputStream fos = new FileOutputStream(tmpPack);
1181 FileChannel channel = fos.getChannel();
1182 OutputStream channelStream = Channels
1183 .newOutputStream(channel)) {
1184 pw.writePack(pm, pm, channelStream);
1185 channel.force(true);
1186 }
1187
1188
1189 try (FileOutputStream fos = new FileOutputStream(tmpIdx);
1190 FileChannel idxChannel = fos.getChannel();
1191 OutputStream idxStream = Channels
1192 .newOutputStream(idxChannel)) {
1193 pw.writeIndex(idxStream);
1194 idxChannel.force(true);
1195 }
1196
1197 if (pw.prepareBitmapIndex(pm)) {
1198 File tmpBitmapIdx = new File(packdir, tmpBase + ".bitmap_tmp");
1199 tmpExts.put(BITMAP_INDEX, tmpBitmapIdx);
1200
1201 if (!tmpBitmapIdx.createNewFile())
1202 throw new IOException(MessageFormat.format(
1203 JGitText.get().cannotCreateIndexfile,
1204 tmpBitmapIdx.getPath()));
1205
1206 try (FileOutputStream fos = new FileOutputStream(tmpBitmapIdx);
1207 FileChannel idxChannel = fos.getChannel();
1208 OutputStream idxStream = Channels
1209 .newOutputStream(idxChannel)) {
1210 pw.writeBitmapIndex(idxStream);
1211 idxChannel.force(true);
1212 }
1213 }
1214
1215
1216 File packDir = repo.getObjectDatabase().getPackDirectory();
1217 PackFile realPack = new PackFile(packDir, id, PackExt.PACK);
1218
1219 repo.getObjectDatabase().closeAllPackHandles(realPack);
1220 tmpPack.setReadOnly();
1221
1222 FileUtils.rename(tmpPack, realPack, StandardCopyOption.ATOMIC_MOVE);
1223 for (Map.Entry<PackExt, File> tmpEntry : tmpExts.entrySet()) {
1224 File tmpExt = tmpEntry.getValue();
1225 tmpExt.setReadOnly();
1226
1227 PackFile realExt = new PackFile(packDir, id, tmpEntry.getKey());
1228 try {
1229 FileUtils.rename(tmpExt, realExt,
1230 StandardCopyOption.ATOMIC_MOVE);
1231 } catch (IOException e) {
1232 File newExt = new File(realExt.getParentFile(),
1233 realExt.getName() + ".new");
1234 try {
1235 FileUtils.rename(tmpExt, newExt,
1236 StandardCopyOption.ATOMIC_MOVE);
1237 } catch (IOException e2) {
1238 newExt = tmpExt;
1239 e = e2;
1240 }
1241 throw new IOException(MessageFormat.format(
1242 JGitText.get().panicCantRenameIndexFile, newExt,
1243 realExt), e);
1244 }
1245 }
1246 boolean interrupted = false;
1247 try {
1248 FileSnapshot snapshot = FileSnapshot.save(realPack);
1249 if (pconfig.doWaitPreventRacyPack(snapshot.size())) {
1250 snapshot.waitUntilNotRacy();
1251 }
1252 } catch (InterruptedException e) {
1253 interrupted = true;
1254 }
1255 try {
1256 return repo.getObjectDatabase().openPack(realPack);
1257 } finally {
1258 if (interrupted) {
1259
1260 Thread.currentThread().interrupt();
1261 }
1262 }
1263 } finally {
1264 if (tmpPack != null && tmpPack.exists())
1265 tmpPack.delete();
1266 for (File tmpExt : tmpExts.values()) {
1267 if (tmpExt.exists())
1268 tmpExt.delete();
1269 }
1270 }
1271 }
1272
1273 private void checkCancelled() throws CancelledException {
1274 if (pm.isCancelled() || Thread.currentThread().isInterrupted()) {
1275 throw new CancelledException(JGitText.get().operationCanceled);
1276 }
1277 }
1278
1279
1280
1281
1282
1283 public static class RepoStatistics {
1284
1285
1286
1287
1288
1289 public long numberOfPackedObjects;
1290
1291
1292
1293
1294 public long numberOfPackFiles;
1295
1296
1297
1298
1299 public long numberOfLooseObjects;
1300
1301
1302
1303
1304 public long sizeOfLooseObjects;
1305
1306
1307
1308
1309 public long sizeOfPackedObjects;
1310
1311
1312
1313
1314 public long numberOfLooseRefs;
1315
1316
1317
1318
1319 public long numberOfPackedRefs;
1320
1321
1322
1323
1324 public long numberOfBitmaps;
1325
1326 @Override
1327 public String toString() {
1328 final StringBuilder b = new StringBuilder();
1329 b.append("numberOfPackedObjects=").append(numberOfPackedObjects);
1330 b.append(", numberOfPackFiles=").append(numberOfPackFiles);
1331 b.append(", numberOfLooseObjects=").append(numberOfLooseObjects);
1332 b.append(", numberOfLooseRefs=").append(numberOfLooseRefs);
1333 b.append(", numberOfPackedRefs=").append(numberOfPackedRefs);
1334 b.append(", sizeOfLooseObjects=").append(sizeOfLooseObjects);
1335 b.append(", sizeOfPackedObjects=").append(sizeOfPackedObjects);
1336 b.append(", numberOfBitmaps=").append(numberOfBitmaps);
1337 return b.toString();
1338 }
1339 }
1340
1341
1342
1343
1344
1345
1346
1347 public RepoStatistics getStatistics() throws IOException {
1348 RepoStatistics ret = new RepoStatistics();
1349 Collection<Pack> packs = repo.getObjectDatabase().getPacks();
1350 for (Pack p : packs) {
1351 ret.numberOfPackedObjects += p.getIndex().getObjectCount();
1352 ret.numberOfPackFiles++;
1353 ret.sizeOfPackedObjects += p.getPackFile().length();
1354 if (p.getBitmapIndex() != null)
1355 ret.numberOfBitmaps += p.getBitmapIndex().getBitmapCount();
1356 }
1357 File objDir = repo.getObjectsDirectory();
1358 String[] fanout = objDir.list();
1359 if (fanout != null && fanout.length > 0) {
1360 for (String d : fanout) {
1361 if (d.length() != 2)
1362 continue;
1363 File[] entries = new File(objDir, d).listFiles();
1364 if (entries == null)
1365 continue;
1366 for (File f : entries) {
1367 if (f.getName().length() != Constants.OBJECT_ID_STRING_LENGTH - 2)
1368 continue;
1369 ret.numberOfLooseObjects++;
1370 ret.sizeOfLooseObjects += f.length();
1371 }
1372 }
1373 }
1374
1375 RefDatabase refDb = repo.getRefDatabase();
1376 for (Ref r : refDb.getRefs()) {
1377 Storage storage = r.getStorage();
1378 if (storage == Storage.LOOSE || storage == Storage.LOOSE_PACKED)
1379 ret.numberOfLooseRefs++;
1380 if (storage == Storage.PACKED || storage == Storage.LOOSE_PACKED)
1381 ret.numberOfPackedRefs++;
1382 }
1383
1384 return ret;
1385 }
1386
1387
1388
1389
1390
1391
1392
1393 public GC setProgressMonitor(ProgressMonitor pm) {
1394 this.pm = (pm == null) ? NullProgressMonitor.INSTANCE : pm;
1395 return this;
1396 }
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407 public void setExpireAgeMillis(long expireAgeMillis) {
1408 this.expireAgeMillis = expireAgeMillis;
1409 expire = null;
1410 }
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421 public void setPackExpireAgeMillis(long packExpireAgeMillis) {
1422 this.packExpireAgeMillis = packExpireAgeMillis;
1423 expire = null;
1424 }
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435 public void setPackConfig(@NonNull PackConfig pconfig) {
1436 this.pconfig = pconfig;
1437 }
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451 public void setExpire(Date expire) {
1452 this.expire = expire;
1453 expireAgeMillis = -1;
1454 }
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465 public void setPackExpire(Date packExpire) {
1466 this.packExpire = packExpire;
1467 packExpireAgeMillis = -1;
1468 }
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505 public void setAuto(boolean auto) {
1506 this.automatic = auto;
1507 }
1508
1509
1510
1511
1512
1513 void setBackground(boolean background) {
1514 this.background = background;
1515 }
1516
1517 private boolean needGc() {
1518 if (tooManyPacks()) {
1519 addRepackAllOption();
1520 } else {
1521 return tooManyLooseObjects();
1522 }
1523
1524 return true;
1525 }
1526
1527 private void addRepackAllOption() {
1528
1529
1530 }
1531
1532
1533
1534
1535 boolean tooManyPacks() {
1536 int autopacklimit = repo.getConfig().getInt(
1537 ConfigConstants.CONFIG_GC_SECTION,
1538 ConfigConstants.CONFIG_KEY_AUTOPACKLIMIT,
1539 DEFAULT_AUTOPACKLIMIT);
1540 if (autopacklimit <= 0) {
1541 return false;
1542 }
1543
1544
1545 return repo.getObjectDatabase().getPacks().size() > (autopacklimit + 1);
1546 }
1547
1548
1549
1550
1551
1552
1553
1554 boolean tooManyLooseObjects() {
1555 int auto = getLooseObjectLimit();
1556 if (auto <= 0) {
1557 return false;
1558 }
1559 int n = 0;
1560 int threshold = (auto + 255) / 256;
1561 Path dir = repo.getObjectsDirectory().toPath().resolve("17");
1562 if (!dir.toFile().exists()) {
1563 return false;
1564 }
1565 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, file -> {
1566 Path fileName = file.getFileName();
1567 return file.toFile().isFile() && fileName != null
1568 && PATTERN_LOOSE_OBJECT.matcher(fileName.toString())
1569 .matches();
1570 })) {
1571 for (Iterator<Path> iter = stream.iterator(); iter.hasNext(); iter
1572 .next()) {
1573 if (++n > threshold) {
1574 return true;
1575 }
1576 }
1577 } catch (IOException e) {
1578 LOG.error(e.getMessage(), e);
1579 }
1580 return false;
1581 }
1582
1583 private int getLooseObjectLimit() {
1584 return repo.getConfig().getInt(ConfigConstants.CONFIG_GC_SECTION,
1585 ConfigConstants.CONFIG_KEY_AUTO, DEFAULT_AUTOLIMIT);
1586 }
1587 }