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