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 @Override
598 public void autoGC(ProgressMonitor monitor) {
599 GC gc = new GC(this);
600 gc.setPackConfig(new PackConfig(this));
601 gc.setProgressMonitor(monitor);
602 gc.setAuto(true);
603 gc.setBackground(shouldAutoDetach());
604 try {
605 gc.gc();
606 } catch (ParseException | IOException e) {
607 throw new JGitInternalException(JGitText.get().gcFailed, e);
608 }
609 }
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626 void convertToPackedRefs(boolean writeLogs, boolean backup) throws IOException {
627 List<Ref> all = refs.getRefs();
628 File packedRefs = new File(getDirectory(), Constants.PACKED_REFS);
629 if (packedRefs.exists()) {
630 throw new IOException(MessageFormat.format(JGitText.get().fileAlreadyExists,
631 packedRefs.getName()));
632 }
633
634 File refsFile = new File(getDirectory(), "refs");
635 File refsHeadsFile = new File(refsFile, "heads");
636 File headFile = new File(getDirectory(), Constants.HEAD);
637 FileReftableDatabase oldDb = (FileReftableDatabase) refs;
638
639
640
641 refsHeadsFile.delete();
642
643
644 refsFile.delete();
645
646 headFile.delete();
647
648
649
650 RefDirectory refDir = new RefDirectory(this);
651 refs = refDir;
652 refs.create();
653
654 ReflogWriter logWriter = refDir.newLogWriter(true);
655 List<Ref> symrefs = new ArrayList<>();
656 BatchRefUpdate bru = refs.newBatchUpdate();
657 for (Ref r : all) {
658 if (r.isSymbolic()) {
659 symrefs.add(r);
660 } else {
661 bru.addCommand(new ReceiveCommand(ObjectId.zeroId(),
662 r.getObjectId(), r.getName()));
663 }
664
665 if (writeLogs) {
666 List<ReflogEntry> logs = oldDb.getReflogReader(r.getName())
667 .getReverseEntries();
668 Collections.reverse(logs);
669 for (ReflogEntry e : logs) {
670 logWriter.log(r.getName(), e);
671 }
672 }
673 }
674
675 try (RevWalk rw = new RevWalk(this)) {
676 bru.execute(rw, NullProgressMonitor.INSTANCE);
677 }
678
679 List<String> failed = new ArrayList<>();
680 for (ReceiveCommand cmd : bru.getCommands()) {
681 if (cmd.getResult() != ReceiveCommand.Result.OK) {
682 failed.add(cmd.getRefName() + ": " + cmd.getResult());
683 }
684 }
685
686 if (!failed.isEmpty()) {
687 throw new IOException(String.format("%s: %s",
688 JGitText.get().failedToConvert,
689 StringUtils.join(failed, ", ")));
690 }
691
692 for (Ref s : symrefs) {
693 RefUpdate up = refs.newUpdate(s.getName(), false);
694 up.setForceUpdate(true);
695 RefUpdate.Result res = up.link(s.getTarget().getName());
696 if (res != RefUpdate.Result.NEW
697 && res != RefUpdate.Result.NO_CHANGE) {
698 throw new IOException(
699 String.format("ref %s: %s", s.getName(), res));
700 }
701 }
702
703 if (!backup) {
704 File reftableDir = new File(getDirectory(), Constants.REFTABLE);
705 FileUtils.delete(reftableDir,
706 FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
707 }
708 repoConfig.unset(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
709 ConfigConstants.CONFIG_KEY_REF_STORAGE);
710 repoConfig.save();
711 }
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730 @SuppressWarnings("nls")
731 void convertToReftable(boolean writeLogs, boolean backup)
732 throws IOException {
733 File reftableDir = new File(getDirectory(), Constants.REFTABLE);
734 File headFile = new File(getDirectory(), Constants.HEAD);
735 if (reftableDir.exists() && FileUtils.hasFiles(reftableDir.toPath())) {
736 throw new IOException(JGitText.get().reftableDirExists);
737 }
738
739
740 FileReftableDatabase.convertFrom(this, writeLogs);
741
742 File refsFile = new File(getDirectory(), "refs");
743
744
745 File packedRefs = new File(getDirectory(), Constants.PACKED_REFS);
746 File logsDir = new File(getDirectory(), Constants.LOGS);
747
748 List<String> additional = getRefDatabase().getAdditionalRefs().stream()
749 .map(Ref::getName).collect(toList());
750 additional.add(Constants.HEAD);
751 if (backup) {
752 FileUtils.rename(refsFile, new File(getDirectory(), "refs.old"));
753 if (packedRefs.exists()) {
754 FileUtils.rename(packedRefs, new File(getDirectory(),
755 Constants.PACKED_REFS + ".old"));
756 }
757 if (logsDir.exists()) {
758 FileUtils.rename(logsDir,
759 new File(getDirectory(), Constants.LOGS + ".old"));
760 }
761 for (String r : additional) {
762 FileUtils.rename(new File(getDirectory(), r),
763 new File(getDirectory(), r + ".old"));
764 }
765 } else {
766 FileUtils.delete(packedRefs, FileUtils.SKIP_MISSING);
767 FileUtils.delete(headFile, FileUtils.SKIP_MISSING);
768 FileUtils.delete(logsDir,
769 FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
770 FileUtils.delete(refsFile,
771 FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
772 for (String r : additional) {
773 new File(getDirectory(), r).delete();
774 }
775 }
776
777 FileUtils.mkdir(refsFile, true);
778
779
780
781 try (OutputStream os = new FileOutputStream(headFile)) {
782 os.write(Constants.encodeASCII("ref: refs/heads/.invalid"));
783 }
784
785
786
787 FileUtils.createNewFile(new File(refsFile, "heads"));
788
789 repoConfig.setString(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
790 ConfigConstants.CONFIG_KEY_REF_STORAGE,
791 ConfigConstants.CONFIG_REF_STORAGE_REFTABLE);
792 repoConfig.setLong(ConfigConstants.CONFIG_CORE_SECTION, null,
793 ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 1);
794 repoConfig.save();
795 refs.close();
796 refs = new FileReftableDatabase(this);
797 }
798
799
800
801
802
803
804
805
806
807
808
809
810
811 public void convertRefStorage(String format, boolean writeLogs,
812 boolean backup) throws IOException {
813 if (format.equals("reftable")) {
814 if (refs instanceof RefDirectory) {
815 convertToReftable(writeLogs, backup);
816 }
817 } else if (format.equals("refdir")) {
818 if (refs instanceof FileReftableDatabase) {
819 convertToPackedRefs(writeLogs, backup);
820 }
821 } else {
822 throw new IOException(MessageFormat
823 .format(JGitText.get().unknownRefStorageFormat, format));
824 }
825 }
826 }