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