1
2
3
4
5
6
7
8
9
10
11
12
13
14 package org.eclipse.jgit.internal.storage.file;
15
16 import static java.util.stream.Collectors.toList;
17
18 import java.io.File;
19 import java.io.FileInputStream;
20 import java.io.FileNotFoundException;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.io.OutputStream;
24 import java.text.MessageFormat;
25 import java.text.ParseException;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Objects;
32 import java.util.Set;
33
34 import org.eclipse.jgit.annotations.Nullable;
35 import org.eclipse.jgit.api.errors.JGitInternalException;
36 import org.eclipse.jgit.attributes.AttributesNode;
37 import org.eclipse.jgit.attributes.AttributesNodeProvider;
38 import org.eclipse.jgit.errors.ConfigInvalidException;
39 import org.eclipse.jgit.events.IndexChangedEvent;
40 import org.eclipse.jgit.internal.JGitText;
41 import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateHandle;
42 import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateRepository;
43 import org.eclipse.jgit.lib.BaseRepositoryBuilder;
44 import org.eclipse.jgit.lib.BatchRefUpdate;
45 import org.eclipse.jgit.lib.ConfigConstants;
46 import org.eclipse.jgit.lib.Constants;
47 import org.eclipse.jgit.lib.CoreConfig.HideDotFiles;
48 import org.eclipse.jgit.lib.CoreConfig.SymLinks;
49 import org.eclipse.jgit.lib.NullProgressMonitor;
50 import org.eclipse.jgit.lib.ObjectId;
51 import org.eclipse.jgit.lib.ProgressMonitor;
52 import org.eclipse.jgit.lib.Ref;
53 import org.eclipse.jgit.lib.RefDatabase;
54 import org.eclipse.jgit.lib.RefUpdate;
55 import org.eclipse.jgit.lib.ReflogEntry;
56 import org.eclipse.jgit.lib.ReflogReader;
57 import org.eclipse.jgit.lib.Repository;
58 import org.eclipse.jgit.lib.StoredConfig;
59 import org.eclipse.jgit.revwalk.RevWalk;
60 import org.eclipse.jgit.storage.file.FileBasedConfig;
61 import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
62 import org.eclipse.jgit.storage.pack.PackConfig;
63 import org.eclipse.jgit.transport.ReceiveCommand;
64 import org.eclipse.jgit.util.FileUtils;
65 import org.eclipse.jgit.util.IO;
66 import org.eclipse.jgit.util.RawParseUtils;
67 import org.eclipse.jgit.util.StringUtils;
68 import org.eclipse.jgit.util.SystemReader;
69 import org.slf4j.Logger;
70 import org.slf4j.LoggerFactory;
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96 public class FileRepository extends Repository {
97 private static final Logger LOG = LoggerFactory
98 .getLogger(FileRepository.class);
99 private static final String UNNAMED = "Unnamed repository; edit this file to name it for gitweb.";
100
101 private final FileBasedConfig repoConfig;
102 private RefDatabase refs;
103 private final ObjectDirectory objectDatabase;
104
105 private final Object snapshotLock = new Object();
106
107
108 private FileSnapshot snapshot;
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130 public FileRepository(File gitDir) throws IOException {
131 this(new FileRepositoryBuilder().setGitDir(gitDir).setup());
132 }
133
134
135
136
137
138
139
140
141
142
143
144 public FileRepository(String gitDir) throws IOException {
145 this(new File(gitDir));
146 }
147
148
149
150
151
152
153
154
155
156
157 public FileRepository(BaseRepositoryBuilder options) throws IOException {
158 super(options);
159 StoredConfig userConfig = null;
160 try {
161 userConfig = SystemReader.getInstance().getUserConfig();
162 } catch (ConfigInvalidException e) {
163 LOG.error(e.getMessage(), e);
164 throw new IOException(e.getMessage(), e);
165 }
166 repoConfig = new FileBasedConfig(userConfig, getFS().resolve(
167 getDirectory(), Constants.CONFIG),
168 getFS());
169 loadRepoConfig();
170
171 repoConfig.addChangeListener(this::fireEvent);
172
173 final long repositoryFormatVersion = getConfig().getLong(
174 ConfigConstants.CONFIG_CORE_SECTION, null,
175 ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0);
176
177 String reftype = repoConfig.getString(
178 ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
179 ConfigConstants.CONFIG_KEY_REF_STORAGE);
180 if (repositoryFormatVersion >= 1 && reftype != null) {
181 if (StringUtils.equalsIgnoreCase(reftype,
182 ConfigConstants.CONFIG_REF_STORAGE_REFTABLE)) {
183 refs = new FileReftableDatabase(this);
184 } else {
185 throw new IOException(JGitText.get().unknownRepositoryFormat);
186 }
187 } else {
188 refs = new RefDirectory(this);
189 }
190
191 objectDatabase = new ObjectDirectory(repoConfig,
192 options.getObjectDirectory(),
193 options.getAlternateObjectDirectories(),
194 getFS(),
195 new File(getDirectory(), Constants.SHALLOW));
196
197 if (objectDatabase.exists()) {
198 if (repositoryFormatVersion > 1)
199 throw new IOException(MessageFormat.format(
200 JGitText.get().unknownRepositoryFormat2,
201 Long.valueOf(repositoryFormatVersion)));
202 }
203
204 if (!isBare()) {
205 snapshot = FileSnapshot.save(getIndexFile());
206 }
207 }
208
209 private void loadRepoConfig() throws IOException {
210 try {
211 repoConfig.load();
212 } catch (ConfigInvalidException e) {
213 throw new IOException(JGitText.get().unknownRepositoryFormat, e);
214 }
215 }
216
217
218
219
220
221
222
223 @Override
224 public void create(boolean bare) throws IOException {
225 final FileBasedConfig cfg = getConfig();
226 if (cfg.getFile().exists()) {
227 throw new IllegalStateException(MessageFormat.format(
228 JGitText.get().repositoryAlreadyExists, getDirectory()));
229 }
230 FileUtils.mkdirs(getDirectory(), true);
231 HideDotFiles hideDotFiles = getConfig().getEnum(
232 ConfigConstants.CONFIG_CORE_SECTION, null,
233 ConfigConstants.CONFIG_KEY_HIDEDOTFILES,
234 HideDotFiles.DOTGITONLY);
235 if (hideDotFiles != HideDotFiles.FALSE && !isBare()
236 && getDirectory().getName().startsWith("."))
237 getFS().setHidden(getDirectory(), true);
238 refs.create();
239 objectDatabase.create();
240
241 FileUtils.mkdir(new File(getDirectory(), "branches"));
242 FileUtils.mkdir(new File(getDirectory(), "hooks"));
243
244 RefUpdate head = updateRef(Constants.HEAD);
245 head.disableRefLog();
246 head.link(Constants.R_HEADS + getInitialBranch());
247
248 final boolean fileMode;
249 if (getFS().supportsExecute()) {
250 File tmp = File.createTempFile("try", "execute", getDirectory());
251
252 getFS().setExecute(tmp, true);
253 final boolean on = getFS().canExecute(tmp);
254
255 getFS().setExecute(tmp, false);
256 final boolean off = getFS().canExecute(tmp);
257 FileUtils.delete(tmp);
258
259 fileMode = on && !off;
260 } else {
261 fileMode = false;
262 }
263
264 SymLinks symLinks = SymLinks.FALSE;
265 if (getFS().supportsSymlinks()) {
266 File tmp = new File(getDirectory(), "tmplink");
267 try {
268 getFS().createSymLink(tmp, "target");
269 symLinks = null;
270 FileUtils.delete(tmp);
271 } catch (IOException e) {
272
273 }
274 }
275 if (symLinks != null)
276 cfg.setString(ConfigConstants.CONFIG_CORE_SECTION, null,
277 ConfigConstants.CONFIG_KEY_SYMLINKS, symLinks.name()
278 .toLowerCase(Locale.ROOT));
279 cfg.setInt(ConfigConstants.CONFIG_CORE_SECTION, null,
280 ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0);
281 cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
282 ConfigConstants.CONFIG_KEY_FILEMODE, fileMode);
283 if (bare)
284 cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
285 ConfigConstants.CONFIG_KEY_BARE, true);
286 cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
287 ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, !bare);
288 if (SystemReader.getInstance().isMacOS())
289
290 cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
291 ConfigConstants.CONFIG_KEY_PRECOMPOSEUNICODE, true);
292 if (!bare) {
293 File workTree = getWorkTree();
294 if (!getDirectory().getParentFile().equals(workTree)) {
295 cfg.setString(ConfigConstants.CONFIG_CORE_SECTION, null,
296 ConfigConstants.CONFIG_KEY_WORKTREE, getWorkTree()
297 .getAbsolutePath());
298 LockFile dotGitLockFile = new LockFile(new File(workTree,
299 Constants.DOT_GIT));
300 try {
301 if (dotGitLockFile.lock()) {
302 dotGitLockFile.write(Constants.encode(Constants.GITDIR
303 + getDirectory().getAbsolutePath()));
304 dotGitLockFile.commit();
305 }
306 } finally {
307 dotGitLockFile.unlock();
308 }
309 }
310 }
311 cfg.save();
312 }
313
314
315
316
317
318
319 public File getObjectsDirectory() {
320 return objectDatabase.getDirectory();
321 }
322
323
324 @Override
325 public ObjectDirectory getObjectDatabase() {
326 return objectDatabase;
327 }
328
329
330 @Override
331 public RefDatabase getRefDatabase() {
332 return refs;
333 }
334
335
336 @Override
337 public String getIdentifier() {
338 File directory = getDirectory();
339 if (directory != null) {
340 return directory.getPath();
341 }
342 throw new IllegalStateException();
343 }
344
345
346 @Override
347 public FileBasedConfig getConfig() {
348 try {
349 SystemReader.getInstance().getUserConfig();
350 if (repoConfig.isOutdated()) {
351 loadRepoConfig();
352 }
353 } catch (IOException | ConfigInvalidException e) {
354 throw new RuntimeException(e);
355 }
356 return repoConfig;
357 }
358
359
360 @Override
361 @Nullable
362 public String getGitwebDescription() throws IOException {
363 String d;
364 try {
365 d = RawParseUtils.decode(IO.readFully(descriptionFile()));
366 } catch (FileNotFoundException err) {
367 return null;
368 }
369 if (d != null) {
370 d = d.trim();
371 if (d.isEmpty() || UNNAMED.equals(d)) {
372 return null;
373 }
374 }
375 return d;
376 }
377
378
379 @Override
380 public void setGitwebDescription(@Nullable String description)
381 throws IOException {
382 String old = getGitwebDescription();
383 if (Objects.equals(old, description)) {
384 return;
385 }
386
387 File path = descriptionFile();
388 LockFile lock = new LockFile(path);
389 if (!lock.lock()) {
390 throw new IOException(MessageFormat.format(JGitText.get().lockError,
391 path.getAbsolutePath()));
392 }
393 try {
394 String d = description;
395 if (d != null) {
396 d = d.trim();
397 if (!d.isEmpty()) {
398 d += '\n';
399 }
400 } else {
401 d = "";
402 }
403 lock.write(Constants.encode(d));
404 lock.commit();
405 } finally {
406 lock.unlock();
407 }
408 }
409
410 private File descriptionFile() {
411 return new File(getDirectory(), "description");
412 }
413
414
415
416
417
418
419
420
421
422
423
424
425
426 @Override
427 public Set<ObjectId> getAdditionalHaves() throws IOException {
428 return getAdditionalHaves(null);
429 }
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446 private Set<ObjectId> getAdditionalHaves(Set<AlternateHandle.Id> skips)
447 throws IOException {
448 HashSet<ObjectId> r = new HashSet<>();
449 skips = objectDatabase.addMe(skips);
450 for (AlternateHandle d : objectDatabase.myAlternates()) {
451 if (d instanceof AlternateRepository && !skips.contains(d.getId())) {
452 FileRepository repo;
453
454 repo = ((AlternateRepository) d).repository;
455 for (Ref ref : repo.getRefDatabase().getRefs()) {
456 if (ref.getObjectId() != null)
457 r.add(ref.getObjectId());
458 if (ref.getPeeledObjectId() != null)
459 r.add(ref.getPeeledObjectId());
460 }
461 r.addAll(repo.getAdditionalHaves(skips));
462 }
463 }
464 return r;
465 }
466
467
468
469
470
471
472
473
474
475
476 public void openPack(File pack) throws IOException {
477 objectDatabase.openPack(pack);
478 }
479
480
481 @Override
482 public void scanForRepoChanges() throws IOException {
483 getRefDatabase().getRefs();
484 detectIndexChanges();
485 }
486
487
488 private void detectIndexChanges() {
489 if (isBare()) {
490 return;
491 }
492
493 File indexFile = getIndexFile();
494 synchronized (snapshotLock) {
495 if (snapshot == null) {
496 snapshot = FileSnapshot.save(indexFile);
497 return;
498 }
499 if (!snapshot.isModified(indexFile)) {
500 return;
501 }
502 }
503 notifyIndexChanged(false);
504 }
505
506
507 @Override
508 public void notifyIndexChanged(boolean internal) {
509 synchronized (snapshotLock) {
510 snapshot = FileSnapshot.save(getIndexFile());
511 }
512 fireEvent(new IndexChangedEvent(internal));
513 }
514
515
516 @Override
517 public ReflogReader getReflogReader(String refName) throws IOException {
518 if (refs instanceof FileReftableDatabase) {
519
520
521 return ((FileReftableDatabase)refs).getReflogReader(refName);
522 }
523
524
525
526 Ref ref = findRef(refName);
527 if (ref == null) {
528 return null;
529 }
530 return new ReflogReaderImpl(this, ref.getName());
531 }
532
533
534 @Override
535 public AttributesNodeProvider createAttributesNodeProvider() {
536 return new AttributesNodeProviderImpl(this);
537 }
538
539
540
541
542
543
544
545
546 static class AttributesNodeProviderImpl implements
547 AttributesNodeProvider {
548
549 private AttributesNode infoAttributesNode;
550
551 private AttributesNode globalAttributesNode;
552
553
554
555
556
557
558
559 protected AttributesNodeProviderImpl(Repository repo) {
560 infoAttributesNode = new InfoAttributesNode(repo);
561 globalAttributesNode = new GlobalAttributesNode(repo);
562 }
563
564 @Override
565 public AttributesNode getInfoAttributesNode() throws IOException {
566 if (infoAttributesNode instanceof InfoAttributesNode)
567 infoAttributesNode = ((InfoAttributesNode) infoAttributesNode)
568 .load();
569 return infoAttributesNode;
570 }
571
572 @Override
573 public AttributesNode getGlobalAttributesNode() throws IOException {
574 if (globalAttributesNode instanceof GlobalAttributesNode)
575 globalAttributesNode = ((GlobalAttributesNode) globalAttributesNode)
576 .load();
577 return globalAttributesNode;
578 }
579
580 static void loadRulesFromFile(AttributesNode r, File attrs)
581 throws FileNotFoundException, IOException {
582 if (attrs.exists()) {
583 try (FileInputStream in = new FileInputStream(attrs)) {
584 r.parse(in);
585 }
586 }
587 }
588
589 }
590
591 private boolean shouldAutoDetach() {
592 return getConfig().getBoolean(ConfigConstants.CONFIG_GC_SECTION,
593 ConfigConstants.CONFIG_KEY_AUTODETACH, true);
594 }
595
596
597 @SuppressWarnings("FutureReturnValueIgnored")
598 @Override
599 public void autoGC(ProgressMonitor monitor) {
600 GC gc = new GC(this);
601 gc.setPackConfig(new PackConfig(this));
602 gc.setProgressMonitor(monitor);
603 gc.setAuto(true);
604 gc.setBackground(shouldAutoDetach());
605 try {
606 gc.gc();
607 } catch (ParseException | IOException e) {
608 throw new JGitInternalException(JGitText.get().gcFailed, e);
609 }
610 }
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627 void convertToPackedRefs(boolean writeLogs, boolean backup) throws IOException {
628 List<Ref> all = refs.getRefs();
629 File packedRefs = new File(getDirectory(), Constants.PACKED_REFS);
630 if (packedRefs.exists()) {
631 throw new IOException(MessageFormat.format(JGitText.get().fileAlreadyExists,
632 packedRefs.getName()));
633 }
634
635 File refsFile = new File(getDirectory(), "refs");
636 File refsHeadsFile = new File(refsFile, "heads");
637 File headFile = new File(getDirectory(), Constants.HEAD);
638 FileReftableDatabase oldDb = (FileReftableDatabase) refs;
639
640
641
642 refsHeadsFile.delete();
643
644
645 refsFile.delete();
646
647 headFile.delete();
648
649
650
651 RefDirectory refDir = new RefDirectory(this);
652 refs = refDir;
653 refs.create();
654
655 ReflogWriter logWriter = refDir.newLogWriter(true);
656 List<Ref> symrefs = new ArrayList<>();
657 BatchRefUpdate bru = refs.newBatchUpdate();
658 for (Ref r : all) {
659 if (r.isSymbolic()) {
660 symrefs.add(r);
661 } else {
662 bru.addCommand(new ReceiveCommand(ObjectId.zeroId(),
663 r.getObjectId(), r.getName()));
664 }
665
666 if (writeLogs) {
667 List<ReflogEntry> logs = oldDb.getReflogReader(r.getName())
668 .getReverseEntries();
669 Collections.reverse(logs);
670 for (ReflogEntry e : logs) {
671 logWriter.log(r.getName(), e);
672 }
673 }
674 }
675
676 try (RevWalk rw = new RevWalk(this)) {
677 bru.execute(rw, NullProgressMonitor.INSTANCE);
678 }
679
680 oldDb.close();
681
682 List<String> failed = new ArrayList<>();
683 for (ReceiveCommand cmd : bru.getCommands()) {
684 if (cmd.getResult() != ReceiveCommand.Result.OK) {
685 failed.add(cmd.getRefName() + ": " + cmd.getResult());
686 }
687 }
688
689 if (!failed.isEmpty()) {
690 throw new IOException(String.format("%s: %s",
691 JGitText.get().failedToConvert,
692 StringUtils.join(failed, ", ")));
693 }
694
695 for (Ref s : symrefs) {
696 RefUpdate up = refs.newUpdate(s.getName(), false);
697 up.setForceUpdate(true);
698 RefUpdate.Result res = up.link(s.getTarget().getName());
699 if (res != RefUpdate.Result.NEW
700 && res != RefUpdate.Result.NO_CHANGE) {
701 throw new IOException(
702 String.format("ref %s: %s", s.getName(), res));
703 }
704 }
705
706 if (!backup) {
707 File reftableDir = new File(getDirectory(), Constants.REFTABLE);
708 FileUtils.delete(reftableDir,
709 FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
710 }
711 repoConfig.unset(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
712 ConfigConstants.CONFIG_KEY_REF_STORAGE);
713 repoConfig.save();
714 }
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733 @SuppressWarnings("nls")
734 void convertToReftable(boolean writeLogs, boolean backup)
735 throws IOException {
736 File reftableDir = new File(getDirectory(), Constants.REFTABLE);
737 File headFile = new File(getDirectory(), Constants.HEAD);
738 if (reftableDir.exists() && FileUtils.hasFiles(reftableDir.toPath())) {
739 throw new IOException(JGitText.get().reftableDirExists);
740 }
741
742
743 FileReftableDatabase.convertFrom(this, writeLogs);
744
745 File refsFile = new File(getDirectory(), "refs");
746
747
748 File packedRefs = new File(getDirectory(), Constants.PACKED_REFS);
749 File logsDir = new File(getDirectory(), Constants.LOGS);
750
751 List<String> additional = getRefDatabase().getAdditionalRefs().stream()
752 .map(Ref::getName).collect(toList());
753 additional.add(Constants.HEAD);
754 if (backup) {
755 FileUtils.rename(refsFile, new File(getDirectory(), "refs.old"));
756 if (packedRefs.exists()) {
757 FileUtils.rename(packedRefs, new File(getDirectory(),
758 Constants.PACKED_REFS + ".old"));
759 }
760 if (logsDir.exists()) {
761 FileUtils.rename(logsDir,
762 new File(getDirectory(), Constants.LOGS + ".old"));
763 }
764 for (String r : additional) {
765 FileUtils.rename(new File(getDirectory(), r),
766 new File(getDirectory(), r + ".old"));
767 }
768 } else {
769 FileUtils.delete(packedRefs, FileUtils.SKIP_MISSING);
770 FileUtils.delete(headFile, FileUtils.SKIP_MISSING);
771 FileUtils.delete(logsDir,
772 FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
773 FileUtils.delete(refsFile,
774 FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
775 for (String r : additional) {
776 new File(getDirectory(), r).delete();
777 }
778 }
779
780 FileUtils.mkdir(refsFile, true);
781
782
783
784 try (OutputStream os = new FileOutputStream(headFile)) {
785 os.write(Constants.encodeASCII("ref: refs/heads/.invalid"));
786 }
787
788
789
790 FileUtils.createNewFile(new File(refsFile, "heads"));
791
792 repoConfig.setString(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
793 ConfigConstants.CONFIG_KEY_REF_STORAGE,
794 ConfigConstants.CONFIG_REF_STORAGE_REFTABLE);
795 repoConfig.setLong(ConfigConstants.CONFIG_CORE_SECTION, null,
796 ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 1);
797 repoConfig.save();
798 refs.close();
799 refs = new FileReftableDatabase(this);
800 }
801
802
803
804
805
806
807
808
809
810
811
812
813
814 public void convertRefStorage(String format, boolean writeLogs,
815 boolean backup) throws IOException {
816 if (format.equals("reftable")) {
817 if (refs instanceof RefDirectory) {
818 convertToReftable(writeLogs, backup);
819 }
820 } else if (format.equals("refdir")) {
821 if (refs instanceof FileReftableDatabase) {
822 convertToPackedRefs(writeLogs, backup);
823 }
824 } else {
825 throw new IOException(MessageFormat
826 .format(JGitText.get().unknownRefStorageFormat, format));
827 }
828 }
829 }