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 @Override
425 public Set<ObjectId> getAdditionalHaves() {
426 return getAdditionalHaves(null);
427 }
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442 private Set<ObjectId> getAdditionalHaves(Set<AlternateHandle.Id> skips) {
443 HashSet<ObjectId> r = new HashSet<>();
444 skips = objectDatabase.addMe(skips);
445 for (AlternateHandle d : objectDatabase.myAlternates()) {
446 if (d instanceof AlternateRepository && !skips.contains(d.getId())) {
447 FileRepository repo;
448
449 repo = ((AlternateRepository) d).repository;
450 for (Ref ref : repo.getAllRefs().values()) {
451 if (ref.getObjectId() != null)
452 r.add(ref.getObjectId());
453 if (ref.getPeeledObjectId() != null)
454 r.add(ref.getPeeledObjectId());
455 }
456 r.addAll(repo.getAdditionalHaves(skips));
457 }
458 }
459 return r;
460 }
461
462
463
464
465
466
467
468
469
470
471 public void openPack(File pack) throws IOException {
472 objectDatabase.openPack(pack);
473 }
474
475
476 @Override
477 public void scanForRepoChanges() throws IOException {
478 getRefDatabase().getRefs();
479 detectIndexChanges();
480 }
481
482
483 private void detectIndexChanges() {
484 if (isBare()) {
485 return;
486 }
487
488 File indexFile = getIndexFile();
489 synchronized (snapshotLock) {
490 if (snapshot == null) {
491 snapshot = FileSnapshot.save(indexFile);
492 return;
493 }
494 if (!snapshot.isModified(indexFile)) {
495 return;
496 }
497 }
498 notifyIndexChanged(false);
499 }
500
501
502 @Override
503 public void notifyIndexChanged(boolean internal) {
504 synchronized (snapshotLock) {
505 snapshot = FileSnapshot.save(getIndexFile());
506 }
507 fireEvent(new IndexChangedEvent(internal));
508 }
509
510
511 @Override
512 public ReflogReader getReflogReader(String refName) throws IOException {
513 if (refs instanceof FileReftableDatabase) {
514
515
516 return ((FileReftableDatabase)refs).getReflogReader(refName);
517 }
518
519
520
521 Ref ref = findRef(refName);
522 if (ref == null) {
523 return null;
524 }
525 return new ReflogReaderImpl(this, ref.getName());
526 }
527
528
529 @Override
530 public AttributesNodeProvider createAttributesNodeProvider() {
531 return new AttributesNodeProviderImpl(this);
532 }
533
534
535
536
537
538
539
540
541 static class AttributesNodeProviderImpl implements
542 AttributesNodeProvider {
543
544 private AttributesNode infoAttributesNode;
545
546 private AttributesNode globalAttributesNode;
547
548
549
550
551
552
553
554 protected AttributesNodeProviderImpl(Repository repo) {
555 infoAttributesNode = new InfoAttributesNode(repo);
556 globalAttributesNode = new GlobalAttributesNode(repo);
557 }
558
559 @Override
560 public AttributesNode getInfoAttributesNode() throws IOException {
561 if (infoAttributesNode instanceof InfoAttributesNode)
562 infoAttributesNode = ((InfoAttributesNode) infoAttributesNode)
563 .load();
564 return infoAttributesNode;
565 }
566
567 @Override
568 public AttributesNode getGlobalAttributesNode() throws IOException {
569 if (globalAttributesNode instanceof GlobalAttributesNode)
570 globalAttributesNode = ((GlobalAttributesNode) globalAttributesNode)
571 .load();
572 return globalAttributesNode;
573 }
574
575 static void loadRulesFromFile(AttributesNode r, File attrs)
576 throws FileNotFoundException, IOException {
577 if (attrs.exists()) {
578 try (FileInputStream in = new FileInputStream(attrs)) {
579 r.parse(in);
580 }
581 }
582 }
583
584 }
585
586 private boolean shouldAutoDetach() {
587 return getConfig().getBoolean(ConfigConstants.CONFIG_GC_SECTION,
588 ConfigConstants.CONFIG_KEY_AUTODETACH, true);
589 }
590
591
592 @Override
593 public void autoGC(ProgressMonitor monitor) {
594 GC gc = new GC(this);
595 gc.setPackConfig(new PackConfig(this));
596 gc.setProgressMonitor(monitor);
597 gc.setAuto(true);
598 gc.setBackground(shouldAutoDetach());
599 try {
600 gc.gc();
601 } catch (ParseException | IOException e) {
602 throw new JGitInternalException(JGitText.get().gcFailed, e);
603 }
604 }
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621 void convertToPackedRefs(boolean writeLogs, boolean backup) throws IOException {
622 List<Ref> all = refs.getRefs();
623 File packedRefs = new File(getDirectory(), Constants.PACKED_REFS);
624 if (packedRefs.exists()) {
625 throw new IOException(MessageFormat.format(JGitText.get().fileAlreadyExists,
626 packedRefs.getName()));
627 }
628
629 File refsFile = new File(getDirectory(), "refs");
630 File refsHeadsFile = new File(refsFile, "heads");
631 File headFile = new File(getDirectory(), Constants.HEAD);
632 FileReftableDatabase oldDb = (FileReftableDatabase) refs;
633
634
635
636 refsHeadsFile.delete();
637
638
639 refsFile.delete();
640
641 headFile.delete();
642
643
644
645 RefDirectory refDir = new RefDirectory(this);
646 refs = refDir;
647 refs.create();
648
649 ReflogWriter logWriter = refDir.newLogWriter(true);
650 List<Ref> symrefs = new ArrayList<>();
651 BatchRefUpdate bru = refs.newBatchUpdate();
652 for (Ref r : all) {
653 if (r.isSymbolic()) {
654 symrefs.add(r);
655 } else {
656 bru.addCommand(new ReceiveCommand(ObjectId.zeroId(),
657 r.getObjectId(), r.getName()));
658 }
659
660 if (writeLogs) {
661 List<ReflogEntry> logs = oldDb.getReflogReader(r.getName())
662 .getReverseEntries();
663 Collections.reverse(logs);
664 for (ReflogEntry e : logs) {
665 logWriter.log(r.getName(), e);
666 }
667 }
668 }
669
670 try (RevWalkvwalk/RevWalk.html#RevWalk">RevWalk rw = new RevWalk(this)) {
671 bru.execute(rw, NullProgressMonitor.INSTANCE);
672 }
673
674 List<String> failed = new ArrayList<>();
675 for (ReceiveCommand cmd : bru.getCommands()) {
676 if (cmd.getResult() != ReceiveCommand.Result.OK) {
677 failed.add(cmd.getRefName() + ": " + cmd.getResult());
678 }
679 }
680
681 if (!failed.isEmpty()) {
682 throw new IOException(String.format("%s: %s",
683 JGitText.get().failedToConvert,
684 StringUtils.join(failed, ", ")));
685 }
686
687 for (Ref s : symrefs) {
688 RefUpdate up = refs.newUpdate(s.getName(), false);
689 up.setForceUpdate(true);
690 RefUpdate.Result res = up.link(s.getTarget().getName());
691 if (res != RefUpdate.Result.NEW
692 && res != RefUpdate.Result.NO_CHANGE) {
693 throw new IOException(
694 String.format("ref %s: %s", s.getName(), res));
695 }
696 }
697
698 if (!backup) {
699 File reftableDir = new File(getDirectory(), Constants.REFTABLE);
700 FileUtils.delete(reftableDir,
701 FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
702 }
703 repoConfig.unset(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
704 ConfigConstants.CONFIG_KEY_REF_STORAGE);
705 repoConfig.save();
706 }
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725 @SuppressWarnings("nls")
726 void convertToReftable(boolean writeLogs, boolean backup)
727 throws IOException {
728 File reftableDir = new File(getDirectory(), Constants.REFTABLE);
729 File headFile = new File(getDirectory(), Constants.HEAD);
730 if (reftableDir.exists() && FileUtils.hasFiles(reftableDir.toPath())) {
731 throw new IOException(JGitText.get().reftableDirExists);
732 }
733
734
735 FileReftableDatabase.convertFrom(this, writeLogs);
736
737 File refsFile = new File(getDirectory(), "refs");
738
739
740 File packedRefs = new File(getDirectory(), Constants.PACKED_REFS);
741 File logsDir = new File(getDirectory(), Constants.LOGS);
742
743 List<String> additional = getRefDatabase().getAdditionalRefs().stream()
744 .map(Ref::getName).collect(toList());
745 additional.add(Constants.HEAD);
746 if (backup) {
747 FileUtils.rename(refsFile, new File(getDirectory(), "refs.old"));
748 if (packedRefs.exists()) {
749 FileUtils.rename(packedRefs, new File(getDirectory(),
750 Constants.PACKED_REFS + ".old"));
751 }
752 if (logsDir.exists()) {
753 FileUtils.rename(logsDir,
754 new File(getDirectory(), Constants.LOGS + ".old"));
755 }
756 for (String r : additional) {
757 FileUtils.rename(new File(getDirectory(), r),
758 new File(getDirectory(), r + ".old"));
759 }
760 } else {
761 FileUtils.delete(packedRefs, FileUtils.SKIP_MISSING);
762 FileUtils.delete(headFile, FileUtils.SKIP_MISSING);
763 FileUtils.delete(logsDir,
764 FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
765 FileUtils.delete(refsFile,
766 FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
767 for (String r : additional) {
768 new File(getDirectory(), r).delete();
769 }
770 }
771
772 FileUtils.mkdir(refsFile, true);
773
774
775
776 try (OutputStream os = new FileOutputStream(headFile)) {
777 os.write(Constants.encodeASCII("ref: refs/heads/.invalid"));
778 }
779
780
781
782 FileUtils.createNewFile(new File(refsFile, "heads"));
783
784 repoConfig.setString(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
785 ConfigConstants.CONFIG_KEY_REF_STORAGE,
786 ConfigConstants.CONFIG_REF_STORAGE_REFTABLE);
787 repoConfig.setLong(ConfigConstants.CONFIG_CORE_SECTION, null,
788 ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 1);
789 repoConfig.save();
790 refs.close();
791 refs = new FileReftableDatabase(this);
792 }
793
794
795
796
797
798
799
800
801
802
803
804
805
806 public void convertRefStorage(String format, boolean writeLogs,
807 boolean backup) throws IOException {
808 if (format.equals("reftable")) {
809 if (refs instanceof RefDirectory) {
810 convertToReftable(writeLogs, backup);
811 }
812 } else if (format.equals("refdir")) {
813 if (refs instanceof FileReftableDatabase) {
814 convertToPackedRefs(writeLogs, backup);
815 }
816 } else {
817 throw new IOException(MessageFormat
818 .format(JGitText.get().unknownRefStorageFormat, format));
819 }
820 }
821 }