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.util;
45
46 import static java.nio.charset.StandardCharsets.UTF_8;
47 import static java.time.Instant.EPOCH;
48
49 import java.io.BufferedReader;
50 import java.io.ByteArrayInputStream;
51 import java.io.Closeable;
52 import java.io.File;
53 import java.io.IOException;
54 import java.io.InputStream;
55 import java.io.InputStreamReader;
56 import java.io.OutputStream;
57 import java.io.OutputStreamWriter;
58 import java.io.PrintStream;
59 import java.io.Writer;
60 import java.nio.charset.Charset;
61 import java.nio.file.AccessDeniedException;
62 import java.nio.file.FileStore;
63 import java.nio.file.Files;
64 import java.nio.file.Path;
65 import java.nio.file.attribute.BasicFileAttributes;
66 import java.nio.file.attribute.FileTime;
67 import java.security.AccessController;
68 import java.security.PrivilegedAction;
69 import java.text.MessageFormat;
70 import java.time.Duration;
71 import java.time.Instant;
72 import java.util.ArrayList;
73 import java.util.Arrays;
74 import java.util.HashMap;
75 import java.util.Map;
76 import java.util.Objects;
77 import java.util.Optional;
78 import java.util.UUID;
79 import java.util.concurrent.CancellationException;
80 import java.util.concurrent.CompletableFuture;
81 import java.util.concurrent.ConcurrentHashMap;
82 import java.util.concurrent.ExecutionException;
83 import java.util.concurrent.ExecutorService;
84 import java.util.concurrent.Executors;
85 import java.util.concurrent.TimeUnit;
86 import java.util.concurrent.TimeoutException;
87 import java.util.concurrent.atomic.AtomicBoolean;
88 import java.util.concurrent.atomic.AtomicReference;
89 import java.util.concurrent.locks.Lock;
90 import java.util.concurrent.locks.ReentrantLock;
91
92 import org.eclipse.jgit.annotations.NonNull;
93 import org.eclipse.jgit.annotations.Nullable;
94 import org.eclipse.jgit.api.errors.JGitInternalException;
95 import org.eclipse.jgit.errors.CommandFailedException;
96 import org.eclipse.jgit.errors.ConfigInvalidException;
97 import org.eclipse.jgit.errors.LockFailedException;
98 import org.eclipse.jgit.internal.JGitText;
99 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
100 import org.eclipse.jgit.lib.ConfigConstants;
101 import org.eclipse.jgit.lib.Constants;
102 import org.eclipse.jgit.lib.Repository;
103 import org.eclipse.jgit.lib.StoredConfig;
104 import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry;
105 import org.eclipse.jgit.treewalk.FileTreeIterator.FileModeStrategy;
106 import org.eclipse.jgit.treewalk.WorkingTreeIterator.Entry;
107 import org.eclipse.jgit.util.ProcessResult.Status;
108 import org.slf4j.Logger;
109 import org.slf4j.LoggerFactory;
110
111
112
113
114 public abstract class FS {
115 private static final Logger LOG = LoggerFactory.getLogger(FS.class);
116
117
118
119
120
121
122
123 protected static final Entry[] NO_ENTRIES = {};
124
125
126
127
128
129
130
131 public static class FSFactory {
132
133
134
135 protected FSFactory() {
136
137 }
138
139
140
141
142
143
144
145 public FS detect(Boolean cygwinUsed) {
146 if (SystemReader.getInstance().isWindows()) {
147 if (cygwinUsed == null)
148 cygwinUsed = Boolean.valueOf(FS_Win32_Cygwin.isCygwin());
149 if (cygwinUsed.booleanValue())
150 return new FS_Win32_Cygwin();
151 else
152 return new FS_Win32();
153 } else {
154 return new FS_POSIX();
155 }
156 }
157 }
158
159
160
161
162
163
164
165 public static class ExecutionResult {
166 private TemporaryBuffer stdout;
167
168 private TemporaryBuffer stderr;
169
170 private int rc;
171
172
173
174
175
176
177 public ExecutionResult(TemporaryBuffer stdout, TemporaryBuffer stderr,
178 int rc) {
179 this.stdout = stdout;
180 this.stderr = stderr;
181 this.rc = rc;
182 }
183
184
185
186
187 public TemporaryBuffer getStdout() {
188 return stdout;
189 }
190
191
192
193
194 public TemporaryBuffer getStderr() {
195 return stderr;
196 }
197
198
199
200
201 public int getRc() {
202 return rc;
203 }
204 }
205
206
207
208
209
210
211 public final static class FileStoreAttributes {
212
213 private static final Duration UNDEFINED_DURATION = Duration
214 .ofNanos(Long.MAX_VALUE);
215
216
217
218
219
220 public static final Duration FALLBACK_TIMESTAMP_RESOLUTION = Duration
221 .ofMillis(2000);
222
223
224
225
226
227
228 public static final FileStoreAttributes FALLBACK_FILESTORE_ATTRIBUTES = new FileStoreAttributes(
229 FALLBACK_TIMESTAMP_RESOLUTION);
230
231 private static final Map<FileStore, FileStoreAttributes> attributeCache = new ConcurrentHashMap<>();
232
233 private static final SimpleLruCache<Path, FileStoreAttributes> attrCacheByPath = new SimpleLruCache<>(
234 100, 0.2f);
235
236 private static AtomicBoolean background = new AtomicBoolean();
237
238 private static Map<FileStore, Lock> locks = new ConcurrentHashMap<>();
239
240 private static void setBackground(boolean async) {
241 background.set(async);
242 }
243
244 private static final String javaVersionPrefix = System
245 .getProperty("java.vendor") + '|'
246 + System.getProperty("java.version") + '|';
247
248 private static final Duration FALLBACK_MIN_RACY_INTERVAL = Duration
249 .ofMillis(10);
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266 public static void configureAttributesPathCache(int maxSize,
267 float purgeFactor) {
268 FileStoreAttributes.attrCacheByPath.configure(maxSize, purgeFactor);
269 }
270
271
272
273
274
275
276
277
278 public static FileStoreAttributes get(Path path) {
279 path = path.toAbsolutePath();
280 Path dir = Files.isDirectory(path) ? path : path.getParent();
281 FileStoreAttributes cached = attrCacheByPath.get(dir);
282 if (cached != null) {
283 return cached;
284 }
285 FileStoreAttributes attrs = getFileStoreAttributes(dir);
286 attrCacheByPath.put(dir, attrs);
287 return attrs;
288 }
289
290 private static FileStoreAttributes getFileStoreAttributes(Path dir) {
291 FileStore s;
292 try {
293 if (Files.exists(dir)) {
294 s = Files.getFileStore(dir);
295 FileStoreAttributes c = attributeCache.get(s);
296 if (c != null) {
297 return c;
298 }
299 if (!Files.isWritable(dir)) {
300
301 LOG.debug(
302 "{}: cannot measure timestamp resolution in read-only directory {}",
303 Thread.currentThread(), dir);
304 return FALLBACK_FILESTORE_ATTRIBUTES;
305 }
306 } else {
307
308 LOG.debug(
309 "{}: cannot measure timestamp resolution of unborn directory {}",
310 Thread.currentThread(), dir);
311 return FALLBACK_FILESTORE_ATTRIBUTES;
312 }
313
314 CompletableFuture<Optional<FileStoreAttributes>> f = CompletableFuture
315 .supplyAsync(() -> {
316 Lock lock = locks.computeIfAbsent(s,
317 l -> new ReentrantLock());
318 if (!lock.tryLock()) {
319 LOG.debug(
320 "{}: couldn't get lock to measure timestamp resolution in {}",
321 Thread.currentThread(), dir);
322 return Optional.empty();
323 }
324 Optional<FileStoreAttributes> attributes = Optional
325 .empty();
326 try {
327
328
329
330 FileStoreAttributes c = attributeCache
331 .get(s);
332 if (c != null) {
333 return Optional.of(c);
334 }
335 attributes = readFromConfig(s);
336 if (attributes.isPresent()) {
337 attributeCache.put(s, attributes.get());
338 return attributes;
339 }
340
341 Optional<Duration> resolution = measureFsTimestampResolution(
342 s, dir);
343 if (resolution.isPresent()) {
344 c = new FileStoreAttributes(
345 resolution.get());
346 attributeCache.put(s, c);
347
348
349 if (c.fsTimestampResolution
350 .toNanos() < 100_000_000L) {
351 c.minimalRacyInterval = measureMinimalRacyInterval(
352 dir);
353 }
354 if (LOG.isDebugEnabled()) {
355 LOG.debug(c.toString());
356 }
357 saveToConfig(s, c);
358 }
359 attributes = Optional.of(c);
360 } finally {
361 lock.unlock();
362 locks.remove(s);
363 }
364 return attributes;
365 });
366 f.exceptionally(e -> {
367 LOG.error(e.getLocalizedMessage(), e);
368 return Optional.empty();
369 });
370
371
372 Optional<FileStoreAttributes> d = background.get() ? f.get(
373 100, TimeUnit.MILLISECONDS) : f.get();
374 if (d.isPresent()) {
375 return d.get();
376 }
377
378 } catch (IOException | InterruptedException
379 | ExecutionException | CancellationException e) {
380 LOG.error(e.getMessage(), e);
381 } catch (TimeoutException | SecurityException e) {
382
383 }
384 LOG.debug("{}: use fallback timestamp resolution for directory {}",
385 Thread.currentThread(), dir);
386 return FALLBACK_FILESTORE_ATTRIBUTES;
387 }
388
389 @SuppressWarnings("boxing")
390 private static Duration measureMinimalRacyInterval(Path dir) {
391 LOG.debug("{}: start measure minimal racy interval in {}",
392 Thread.currentThread(), dir);
393 int n = 0;
394 int failures = 0;
395 long racyNanos = 0;
396 ArrayList<Long> deltas = new ArrayList<>();
397 Path probe = dir.resolve(".probe-" + UUID.randomUUID());
398 Instant end = Instant.now().plusSeconds(3);
399 try {
400 Files.createFile(probe);
401 do {
402 n++;
403 write(probe, "a");
404 FileSnapshot snapshot = FileSnapshot.save(probe.toFile());
405 read(probe);
406 write(probe, "b");
407 if (!snapshot.isModified(probe.toFile())) {
408 deltas.add(Long.valueOf(snapshot.lastDelta()));
409 racyNanos = snapshot.lastRacyThreshold();
410 failures++;
411 }
412 } while (Instant.now().compareTo(end) < 0);
413 } catch (IOException e) {
414 LOG.error(e.getMessage(), e);
415 return FALLBACK_MIN_RACY_INTERVAL;
416 } finally {
417 deleteProbe(probe);
418 }
419 if (failures > 0) {
420 Stats stats = new Stats();
421 for (Long d : deltas) {
422 stats.add(d);
423 }
424 LOG.debug(
425 "delta [ns] since modification FileSnapshot failed to detect\n"
426 + "count, failures, racy limit [ns], delta min [ns],"
427 + " delta max [ns], delta avg [ns],"
428 + " delta stddev [ns]\n"
429 + "{}, {}, {}, {}, {}, {}, {}",
430 n, failures, racyNanos, stats.min(), stats.max(),
431 stats.avg(), stats.stddev());
432 return Duration
433 .ofNanos(Double.valueOf(stats.max()).longValue());
434 }
435
436
437 LOG.debug("{}: no failures when measuring minimal racy interval",
438 Thread.currentThread());
439 return Duration.ZERO;
440 }
441
442 private static void write(Path p, String body) throws IOException {
443 FileUtils.mkdirs(p.getParent().toFile(), true);
444 try (Writer w = new OutputStreamWriter(Files.newOutputStream(p),
445 UTF_8)) {
446 w.write(body);
447 }
448 }
449
450 private static String read(Path p) throws IOException {
451 final byte[] body = IO.readFully(p.toFile());
452 return new String(body, 0, body.length, UTF_8);
453 }
454
455 private static Optional<Duration> measureFsTimestampResolution(
456 FileStore s, Path dir) {
457 LOG.debug("{}: start measure timestamp resolution {} in {}",
458 Thread.currentThread(), s, dir);
459 Path probe = dir.resolve(".probe-" + UUID.randomUUID());
460 try {
461 Files.createFile(probe);
462 FileTime t1 = Files.getLastModifiedTime(probe);
463 FileTime t2 = t1;
464 Instant t1i = t1.toInstant();
465 for (long i = 1; t2.compareTo(t1) <= 0; i += 1 + i / 20) {
466 Files.setLastModifiedTime(probe,
467 FileTime.from(t1i.plusNanos(i * 1000)));
468 t2 = Files.getLastModifiedTime(probe);
469 }
470 Duration fsResolution = Duration.between(t1.toInstant(), t2.toInstant());
471 Duration clockResolution = measureClockResolution();
472 fsResolution = fsResolution.plus(clockResolution);
473 LOG.debug("{}: end measure timestamp resolution {} in {}",
474 Thread.currentThread(), s, dir);
475 return Optional.of(fsResolution);
476 } catch (AccessDeniedException e) {
477 LOG.warn(e.getLocalizedMessage(), e);
478 } catch (IOException e) {
479 LOG.error(e.getLocalizedMessage(), e);
480 } finally {
481 deleteProbe(probe);
482 }
483 return Optional.empty();
484 }
485
486 private static Duration measureClockResolution() {
487 Duration clockResolution = Duration.ZERO;
488 for (int i = 0; i < 10; i++) {
489 Instant t1 = Instant.now();
490 Instant t2 = t1;
491 while (t2.compareTo(t1) <= 0) {
492 t2 = Instant.now();
493 }
494 Duration r = Duration.between(t1, t2);
495 if (r.compareTo(clockResolution) > 0) {
496 clockResolution = r;
497 }
498 }
499 return clockResolution;
500 }
501
502 private static void deleteProbe(Path probe) {
503 try {
504 FileUtils.delete(probe.toFile(),
505 FileUtils.SKIP_MISSING | FileUtils.RETRY);
506 } catch (IOException e) {
507 LOG.error(e.getMessage(), e);
508 }
509 }
510
511 private static Optional<FileStoreAttributes> readFromConfig(
512 FileStore s) {
513 StoredConfig userConfig;
514 try {
515 userConfig = SystemReader.getInstance().getUserConfig();
516 } catch (IOException | ConfigInvalidException e) {
517 LOG.error(JGitText.get().readFileStoreAttributesFailed, e);
518 return Optional.empty();
519 }
520 String key = getConfigKey(s);
521 Duration resolution = Duration.ofNanos(userConfig.getTimeUnit(
522 ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
523 ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION,
524 UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS));
525 if (UNDEFINED_DURATION.equals(resolution)) {
526 return Optional.empty();
527 }
528 Duration minRacyThreshold = Duration.ofNanos(userConfig.getTimeUnit(
529 ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
530 ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD,
531 UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS));
532 FileStoreAttributes c = new FileStoreAttributes(resolution);
533 if (!UNDEFINED_DURATION.equals(minRacyThreshold)) {
534 c.minimalRacyInterval = minRacyThreshold;
535 }
536 return Optional.of(c);
537 }
538
539 private static void saveToConfig(FileStore s,
540 FileStoreAttributes c) {
541 StoredConfig userConfig;
542 try {
543 userConfig = SystemReader.getInstance().getUserConfig();
544 } catch (IOException | ConfigInvalidException e) {
545 LOG.error(JGitText.get().saveFileStoreAttributesFailed, e);
546 return;
547 }
548 long resolution = c.getFsTimestampResolution().toNanos();
549 TimeUnit resolutionUnit = getUnit(resolution);
550 long resolutionValue = resolutionUnit.convert(resolution,
551 TimeUnit.NANOSECONDS);
552
553 long minRacyThreshold = c.getMinimalRacyInterval().toNanos();
554 TimeUnit minRacyThresholdUnit = getUnit(minRacyThreshold);
555 long minRacyThresholdValue = minRacyThresholdUnit
556 .convert(minRacyThreshold, TimeUnit.NANOSECONDS);
557
558 final int max_retries = 5;
559 int retries = 0;
560 boolean succeeded = false;
561 String key = getConfigKey(s);
562 while (!succeeded && retries < max_retries) {
563 try {
564 userConfig.load();
565 userConfig.setString(
566 ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
567 ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION,
568 String.format("%d %s",
569 Long.valueOf(resolutionValue),
570 resolutionUnit.name().toLowerCase()));
571 userConfig.setString(
572 ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
573 ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD,
574 String.format("%d %s",
575 Long.valueOf(minRacyThresholdValue),
576 minRacyThresholdUnit.name().toLowerCase()));
577 userConfig.save();
578 succeeded = true;
579 } catch (LockFailedException e) {
580
581 try {
582 retries++;
583 if (retries < max_retries) {
584 Thread.sleep(100);
585 LOG.debug("locking {} failed, retries {}/{}",
586 userConfig, Integer.valueOf(retries),
587 Integer.valueOf(max_retries));
588 } else {
589 LOG.warn(MessageFormat.format(
590 JGitText.get().lockFailedRetry, userConfig,
591 Integer.valueOf(retries)));
592 }
593 } catch (InterruptedException e1) {
594 Thread.currentThread().interrupt();
595 break;
596 }
597 } catch (IOException e) {
598 LOG.error(MessageFormat.format(
599 JGitText.get().cannotSaveConfig, userConfig), e);
600 break;
601 } catch (ConfigInvalidException e) {
602 LOG.error(MessageFormat.format(
603 JGitText.get().repositoryConfigFileInvalid,
604 userConfig, e.getMessage()));
605 break;
606 }
607 }
608 }
609
610 private static String getConfigKey(FileStore s) {
611 final String storeKey;
612 if (SystemReader.getInstance().isWindows()) {
613 Object attribute = null;
614 try {
615 attribute = s.getAttribute("volume:vsn");
616 } catch (IOException ignored) {
617
618 }
619 if (attribute instanceof Integer) {
620 storeKey = attribute.toString();
621 } else {
622 storeKey = s.name();
623 }
624 } else {
625 storeKey = s.name();
626 }
627 return javaVersionPrefix + storeKey;
628 }
629
630 private static TimeUnit getUnit(long nanos) {
631 TimeUnit unit;
632 if (nanos < 200_000L) {
633 unit = TimeUnit.NANOSECONDS;
634 } else if (nanos < 200_000_000L) {
635 unit = TimeUnit.MICROSECONDS;
636 } else {
637 unit = TimeUnit.MILLISECONDS;
638 }
639 return unit;
640 }
641
642 private final @NonNull Duration fsTimestampResolution;
643
644 private Duration minimalRacyInterval;
645
646
647
648
649
650
651 public Duration getMinimalRacyInterval() {
652 return minimalRacyInterval;
653 }
654
655
656
657
658 @NonNull
659 public Duration getFsTimestampResolution() {
660 return fsTimestampResolution;
661 }
662
663
664
665
666
667
668
669 public FileStoreAttributes(
670 @NonNull Duration fsTimestampResolution) {
671 this.fsTimestampResolution = fsTimestampResolution;
672 this.minimalRacyInterval = Duration.ZERO;
673 }
674
675 @SuppressWarnings({ "nls", "boxing" })
676 @Override
677 public String toString() {
678 return String.format(
679 "FileStoreAttributes[fsTimestampResolution=%,d µs, "
680 + "minimalRacyInterval=%,d µs]",
681 fsTimestampResolution.toNanos() / 1000,
682 minimalRacyInterval.toNanos() / 1000);
683 }
684
685 }
686
687
688 public static final FS DETECTED = detect();
689
690 private volatile static FSFactory factory;
691
692
693
694
695
696
697 public static FS detect() {
698 return detect(null);
699 }
700
701
702
703
704
705
706
707
708
709
710 public static void setAsyncFileStoreAttributes(boolean asynch) {
711 FileStoreAttributes.setBackground(asynch);
712 }
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734 public static FS detect(Boolean cygwinUsed) {
735 if (factory == null) {
736 factory = new FS.FSFactory();
737 }
738 return factory.detect(cygwinUsed);
739 }
740
741
742
743
744
745
746
747
748
749
750
751 public static FileStoreAttributes getFileStoreAttributes(
752 @NonNull Path dir) {
753 return FileStoreAttributes.get(dir);
754 }
755
756 private volatile Holder<File> userHome;
757
758 private volatile Holder<File> gitSystemConfig;
759
760
761
762
763 protected FS() {
764
765 }
766
767
768
769
770
771
772
773 protected FS(FS src) {
774 userHome = src.userHome;
775 gitSystemConfig = src.gitSystemConfig;
776 }
777
778
779
780
781
782
783 public abstract FS newInstance();
784
785
786
787
788
789
790
791 public abstract boolean supportsExecute();
792
793
794
795
796
797
798
799
800
801
802
803
804 public boolean supportsAtomicCreateNewFile() {
805 return true;
806 }
807
808
809
810
811
812
813
814
815 public boolean supportsSymlinks() {
816 return false;
817 }
818
819
820
821
822
823
824 public abstract boolean isCaseSensitive();
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840 public abstract boolean canExecute(File f);
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855 public abstract boolean setExecute(File f, boolean canExec);
856
857
858
859
860
861
862
863
864
865
866
867
868
869 @Deprecated
870 public long lastModified(File f) throws IOException {
871 return FileUtils.lastModified(f);
872 }
873
874
875
876
877
878
879
880
881
882
883
884 public Instant lastModifiedInstant(Path p) {
885 return FileUtils.lastModifiedInstant(p);
886 }
887
888
889
890
891
892
893
894
895
896
897
898 public Instant lastModifiedInstant(File f) {
899 return FileUtils.lastModifiedInstant(f.toPath());
900 }
901
902
903
904
905
906
907
908
909
910
911
912
913
914 @Deprecated
915 public void setLastModified(File f, long time) throws IOException {
916 FileUtils.setLastModified(f, time);
917 }
918
919
920
921
922
923
924
925
926
927
928
929
930 public void setLastModified(Path p, Instant time) throws IOException {
931 FileUtils.setLastModified(p, time);
932 }
933
934
935
936
937
938
939
940
941
942
943
944 public long length(File path) throws IOException {
945 return FileUtils.getLength(path);
946 }
947
948
949
950
951
952
953
954
955
956
957 public void delete(File f) throws IOException {
958 FileUtils.delete(f);
959 }
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979 public File resolve(File dir, String name) {
980 final File abspn = new File(name);
981 if (abspn.isAbsolute())
982 return abspn;
983 return new File(dir, name);
984 }
985
986
987
988
989
990
991
992
993
994
995
996
997 public File userHome() {
998 Holder<File> p = userHome;
999 if (p == null) {
1000 p = new Holder<>(userHomeImpl());
1001 userHome = p;
1002 }
1003 return p.value;
1004 }
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014 public FS setUserHome(File path) {
1015 userHome = new Holder<>(path);
1016 return this;
1017 }
1018
1019
1020
1021
1022
1023
1024 public abstract boolean retryFailedLockFileCommit();
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035 public BasicFileAttributes fileAttributes(File file) throws IOException {
1036 return FileUtils.fileAttributes(file);
1037 }
1038
1039
1040
1041
1042
1043
1044 protected File userHomeImpl() {
1045 final String home = AccessController
1046 .doPrivileged(new PrivilegedAction<String>() {
1047 @Override
1048 public String run() {
1049 return System.getProperty("user.home");
1050 }
1051 });
1052 if (home == null || home.length() == 0)
1053 return null;
1054 return new File(home).getAbsoluteFile();
1055 }
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068 protected static File searchPath(String path, String... lookFor) {
1069 if (path == null)
1070 return null;
1071
1072 for (String p : path.split(File.pathSeparator)) {
1073 for (String command : lookFor) {
1074 final File e = new File(p, command);
1075 if (e.isFile())
1076 return e.getAbsoluteFile();
1077 }
1078 }
1079 return null;
1080 }
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096 @Nullable
1097 protected static String readPipe(File dir, String[] command,
1098 String encoding) throws CommandFailedException {
1099 return readPipe(dir, command, encoding, null);
1100 }
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120 @Nullable
1121 protected static String readPipe(File dir, String[] command,
1122 String encoding, Map<String, String> env)
1123 throws CommandFailedException {
1124 final boolean debug = LOG.isDebugEnabled();
1125 try {
1126 if (debug) {
1127 LOG.debug("readpipe " + Arrays.asList(command) + ","
1128 + dir);
1129 }
1130 ProcessBuilder pb = new ProcessBuilder(command);
1131 pb.directory(dir);
1132 if (env != null) {
1133 pb.environment().putAll(env);
1134 }
1135 Process p;
1136 try {
1137 p = pb.start();
1138 } catch (IOException e) {
1139
1140 throw new CommandFailedException(-1, e.getMessage(), e);
1141 }
1142 p.getOutputStream().close();
1143 GobblerThread gobbler = new GobblerThread(p, command, dir);
1144 gobbler.start();
1145 String r = null;
1146 try (BufferedReader lineRead = new BufferedReader(
1147 new InputStreamReader(p.getInputStream(), encoding))) {
1148 r = lineRead.readLine();
1149 if (debug) {
1150 LOG.debug("readpipe may return '" + r + "'");
1151 LOG.debug("remaining output:\n");
1152 String l;
1153 while ((l = lineRead.readLine()) != null) {
1154 LOG.debug(l);
1155 }
1156 }
1157 }
1158
1159 for (;;) {
1160 try {
1161 int rc = p.waitFor();
1162 gobbler.join();
1163 if (rc == 0 && !gobbler.fail.get()) {
1164 return r;
1165 } else {
1166 if (debug) {
1167 LOG.debug("readpipe rc=" + rc);
1168 }
1169 throw new CommandFailedException(rc,
1170 gobbler.errorMessage.get(),
1171 gobbler.exception.get());
1172 }
1173 } catch (InterruptedException ie) {
1174
1175 }
1176 }
1177 } catch (IOException e) {
1178 LOG.error("Caught exception in FS.readPipe()", e);
1179 }
1180 if (debug) {
1181 LOG.debug("readpipe returns null");
1182 }
1183 return null;
1184 }
1185
1186 private static class GobblerThread extends Thread {
1187
1188
1189 private static final int PROCESS_EXIT_TIMEOUT = 5;
1190
1191 private final Process p;
1192 private final String desc;
1193 private final String dir;
1194 final AtomicBoolean fail = new AtomicBoolean();
1195 final AtomicReference<String> errorMessage = new AtomicReference<>();
1196 final AtomicReference<Throwable> exception = new AtomicReference<>();
1197
1198 GobblerThread(Process p, String[] command, File dir) {
1199 this.p = p;
1200 this.desc = Arrays.toString(command);
1201 this.dir = Objects.toString(dir);
1202 }
1203
1204 @Override
1205 public void run() {
1206 StringBuilder err = new StringBuilder();
1207 try (InputStream is = p.getErrorStream()) {
1208 int ch;
1209 while ((ch = is.read()) != -1) {
1210 err.append((char) ch);
1211 }
1212 } catch (IOException e) {
1213 if (waitForProcessCompletion(e) && p.exitValue() != 0) {
1214 setError(e, e.getMessage(), p.exitValue());
1215 fail.set(true);
1216 } else {
1217
1218
1219 }
1220 } finally {
1221 if (waitForProcessCompletion(null) && err.length() > 0) {
1222 setError(null, err.toString(), p.exitValue());
1223 if (p.exitValue() != 0) {
1224 fail.set(true);
1225 }
1226 }
1227 }
1228 }
1229
1230 @SuppressWarnings("boxing")
1231 private boolean waitForProcessCompletion(IOException originalError) {
1232 try {
1233 if (!p.waitFor(PROCESS_EXIT_TIMEOUT, TimeUnit.SECONDS)) {
1234 setError(originalError, MessageFormat.format(
1235 JGitText.get().commandClosedStderrButDidntExit,
1236 desc, PROCESS_EXIT_TIMEOUT), -1);
1237 fail.set(true);
1238 return false;
1239 }
1240 } catch (InterruptedException e) {
1241 setError(originalError, MessageFormat.format(
1242 JGitText.get().threadInterruptedWhileRunning, desc), -1);
1243 fail.set(true);
1244 return false;
1245 }
1246 return true;
1247 }
1248
1249 private void setError(IOException e, String message, int exitCode) {
1250 exception.set(e);
1251 errorMessage.set(MessageFormat.format(
1252 JGitText.get().exceptionCaughtDuringExecutionOfCommand,
1253 desc, dir, Integer.valueOf(exitCode), message));
1254 }
1255 }
1256
1257
1258
1259
1260
1261
1262
1263
1264 protected abstract File discoverGitExe();
1265
1266
1267
1268
1269
1270
1271
1272
1273 protected File discoverGitSystemConfig() {
1274 File gitExe = discoverGitExe();
1275 if (gitExe == null) {
1276 return null;
1277 }
1278
1279
1280 String v;
1281 try {
1282 v = readPipe(gitExe.getParentFile(),
1283 new String[] { "git", "--version" },
1284 Charset.defaultCharset().name());
1285 } catch (CommandFailedException e) {
1286 LOG.warn(e.getMessage());
1287 return null;
1288 }
1289 if (StringUtils.isEmptyOrNull(v)
1290 || (v != null && v.startsWith("jgit"))) {
1291 return null;
1292 }
1293
1294
1295
1296 Map<String, String> env = new HashMap<>();
1297 env.put("GIT_EDITOR", "echo");
1298
1299 String w;
1300 try {
1301 w = readPipe(gitExe.getParentFile(),
1302 new String[] { "git", "config", "--system", "--edit" },
1303 Charset.defaultCharset().name(), env);
1304 } catch (CommandFailedException e) {
1305 LOG.warn(e.getMessage());
1306 return null;
1307 }
1308 if (StringUtils.isEmptyOrNull(w)) {
1309 return null;
1310 }
1311
1312 return new File(w);
1313 }
1314
1315
1316
1317
1318
1319
1320
1321
1322 public File getGitSystemConfig() {
1323 if (gitSystemConfig == null) {
1324 gitSystemConfig = new Holder<>(discoverGitSystemConfig());
1325 }
1326 return gitSystemConfig.value;
1327 }
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337 public FS setGitSystemConfig(File configFile) {
1338 gitSystemConfig = new Holder<>(configFile);
1339 return this;
1340 }
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351 protected static File resolveGrandparentFile(File grandchild) {
1352 if (grandchild != null) {
1353 File parent = grandchild.getParentFile();
1354 if (parent != null)
1355 return parent.getParentFile();
1356 }
1357 return null;
1358 }
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369 public String readSymLink(File path) throws IOException {
1370 return FileUtils.readSymLink(path);
1371 }
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382 public boolean isSymLink(File path) throws IOException {
1383 return FileUtils.isSymlink(path);
1384 }
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395 public boolean exists(File path) {
1396 return FileUtils.exists(path);
1397 }
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408 public boolean isDirectory(File path) {
1409 return FileUtils.isDirectory(path);
1410 }
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421 public boolean isFile(File path) {
1422 return FileUtils.isFile(path);
1423 }
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436 public boolean isHidden(File path) throws IOException {
1437 return FileUtils.isHidden(path);
1438 }
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450 public void setHidden(File path, boolean hidden) throws IOException {
1451 FileUtils.setHidden(path, hidden);
1452 }
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464 public void createSymLink(File path, String target) throws IOException {
1465 FileUtils.createSymLink(path, target);
1466 }
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481 @Deprecated
1482 public boolean createNewFile(File path) throws IOException {
1483 return path.createNewFile();
1484 }
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495 public static class LockToken implements Closeable {
1496 private boolean isCreated;
1497
1498 private Optional<Path> link;
1499
1500 LockToken(boolean isCreated, Optional<Path> link) {
1501 this.isCreated = isCreated;
1502 this.link = link;
1503 }
1504
1505
1506
1507
1508 public boolean isCreated() {
1509 return isCreated;
1510 }
1511
1512 @Override
1513 public void close() {
1514 if (!link.isPresent()) {
1515 return;
1516 }
1517 Path p = link.get();
1518 if (!Files.exists(p)) {
1519 return;
1520 }
1521 try {
1522 Files.delete(p);
1523 } catch (IOException e) {
1524 LOG.error(MessageFormat
1525 .format(JGitText.get().closeLockTokenFailed, this), e);
1526 }
1527 }
1528
1529 @Override
1530 public String toString() {
1531 return "LockToken [lockCreated=" + isCreated +
1532 ", link="
1533 + (link.isPresent() ? link.get().getFileName() + "]"
1534 : "<null>]");
1535 }
1536 }
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550 public LockToken createNewFileAtomic(File path) throws IOException {
1551 return new LockToken(path.createNewFile(), Optional.empty());
1552 }
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568 public String relativize(String base, String other) {
1569 return FileUtils.relativizePath(base, other, File.separator, this.isCaseSensitive());
1570 }
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583 public Entry[] list(File directory, FileModeStrategy fileModeStrategy) {
1584 final File[] all = directory.listFiles();
1585 if (all == null) {
1586 return NO_ENTRIES;
1587 }
1588 final Entry[] result = new Entry[all.length];
1589 for (int i = 0; i < result.length; i++) {
1590 result[i] = new FileEntry(all[i], this, fileModeStrategy);
1591 }
1592 return result;
1593 }
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617 public ProcessResult runHookIfPresent(Repository repository,
1618 final String hookName,
1619 String[] args) throws JGitInternalException {
1620 return runHookIfPresent(repository, hookName, args, System.out, System.err,
1621 null);
1622 }
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652 public ProcessResult runHookIfPresent(Repository repository,
1653 final String hookName,
1654 String[] args, PrintStream outRedirect, PrintStream errRedirect,
1655 String stdinArgs) throws JGitInternalException {
1656 return new ProcessResult(Status.NOT_SUPPORTED);
1657 }
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688 protected ProcessResult internalRunHookIfPresent(Repository repository,
1689 final String hookName, String[] args, PrintStream outRedirect,
1690 PrintStream errRedirect, String stdinArgs)
1691 throws JGitInternalException {
1692 final File hookFile = findHook(repository, hookName);
1693 if (hookFile == null)
1694 return new ProcessResult(Status.NOT_PRESENT);
1695
1696 final String hookPath = hookFile.getAbsolutePath();
1697 final File runDirectory;
1698 if (repository.isBare())
1699 runDirectory = repository.getDirectory();
1700 else
1701 runDirectory = repository.getWorkTree();
1702 final String cmd = relativize(runDirectory.getAbsolutePath(),
1703 hookPath);
1704 ProcessBuilder hookProcess = runInShell(cmd, args);
1705 hookProcess.directory(runDirectory);
1706 try {
1707 return new ProcessResult(runProcess(hookProcess, outRedirect,
1708 errRedirect, stdinArgs), Status.OK);
1709 } catch (IOException e) {
1710 throw new JGitInternalException(MessageFormat.format(
1711 JGitText.get().exceptionCaughtDuringExecutionOfHook,
1712 hookName), e);
1713 } catch (InterruptedException e) {
1714 throw new JGitInternalException(MessageFormat.format(
1715 JGitText.get().exceptionHookExecutionInterrupted,
1716 hookName), e);
1717 }
1718 }
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732 public File findHook(Repository repository, String hookName) {
1733 File gitDir = repository.getDirectory();
1734 if (gitDir == null)
1735 return null;
1736 final File hookFile = new File(new File(gitDir,
1737 Constants.HOOKS), hookName);
1738 return hookFile.isFile() ? hookFile : null;
1739 }
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766 public int runProcess(ProcessBuilder processBuilder,
1767 OutputStream outRedirect, OutputStream errRedirect, String stdinArgs)
1768 throws IOException, InterruptedException {
1769 InputStream in = (stdinArgs == null) ? null : new ByteArrayInputStream(
1770 stdinArgs.getBytes(UTF_8));
1771 return runProcess(processBuilder, outRedirect, errRedirect, in);
1772 }
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802 public int runProcess(ProcessBuilder processBuilder,
1803 OutputStream outRedirect, OutputStream errRedirect,
1804 InputStream inRedirect) throws IOException,
1805 InterruptedException {
1806 final ExecutorService executor = Executors.newFixedThreadPool(2);
1807 Process process = null;
1808
1809
1810 IOException ioException = null;
1811 try {
1812 process = processBuilder.start();
1813 executor.execute(
1814 new StreamGobbler(process.getErrorStream(), errRedirect));
1815 executor.execute(
1816 new StreamGobbler(process.getInputStream(), outRedirect));
1817 @SuppressWarnings("resource")
1818 OutputStream outputStream = process.getOutputStream();
1819 try {
1820 if (inRedirect != null) {
1821 new StreamGobbler(inRedirect, outputStream).copy();
1822 }
1823 } finally {
1824 try {
1825 outputStream.close();
1826 } catch (IOException e) {
1827
1828
1829
1830
1831
1832
1833 }
1834 }
1835 return process.waitFor();
1836 } catch (IOException e) {
1837 ioException = e;
1838 } finally {
1839 shutdownAndAwaitTermination(executor);
1840 if (process != null) {
1841 try {
1842 process.waitFor();
1843 } catch (InterruptedException e) {
1844
1845
1846
1847
1848 Thread.interrupted();
1849 }
1850
1851
1852
1853 if (inRedirect != null) {
1854 inRedirect.close();
1855 }
1856 try {
1857 process.getErrorStream().close();
1858 } catch (IOException e) {
1859 ioException = ioException != null ? ioException : e;
1860 }
1861 try {
1862 process.getInputStream().close();
1863 } catch (IOException e) {
1864 ioException = ioException != null ? ioException : e;
1865 }
1866 try {
1867 process.getOutputStream().close();
1868 } catch (IOException e) {
1869 ioException = ioException != null ? ioException : e;
1870 }
1871 process.destroy();
1872 }
1873 }
1874
1875 throw ioException;
1876 }
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891 private static boolean shutdownAndAwaitTermination(ExecutorService pool) {
1892 boolean hasShutdown = true;
1893 pool.shutdown();
1894 try {
1895
1896 if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
1897 pool.shutdownNow();
1898
1899 if (!pool.awaitTermination(60, TimeUnit.SECONDS))
1900 hasShutdown = false;
1901 }
1902 } catch (InterruptedException ie) {
1903
1904 pool.shutdownNow();
1905
1906 Thread.currentThread().interrupt();
1907 hasShutdown = false;
1908 }
1909 return hasShutdown;
1910 }
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924 public abstract ProcessBuilder runInShell(String cmd, String[] args);
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938 public ExecutionResult execute(ProcessBuilder pb, InputStream in)
1939 throws IOException, InterruptedException {
1940 try (TemporaryBuffer stdout = new TemporaryBuffer.LocalFile(null);
1941 TemporaryBuffer stderr = new TemporaryBuffer.Heap(1024,
1942 1024 * 1024)) {
1943 int rc = runProcess(pb, stdout, stderr, in);
1944 return new ExecutionResult(stdout, stderr, rc);
1945 }
1946 }
1947
1948 private static class Holder<V> {
1949 final V value;
1950
1951 Holder(V value) {
1952 this.value = value;
1953 }
1954 }
1955
1956
1957
1958
1959
1960
1961 public static class Attributes {
1962
1963
1964
1965
1966 public boolean isDirectory() {
1967 return isDirectory;
1968 }
1969
1970
1971
1972
1973 public boolean isExecutable() {
1974 return isExecutable;
1975 }
1976
1977
1978
1979
1980 public boolean isSymbolicLink() {
1981 return isSymbolicLink;
1982 }
1983
1984
1985
1986
1987 public boolean isRegularFile() {
1988 return isRegularFile;
1989 }
1990
1991
1992
1993
1994 public long getCreationTime() {
1995 return creationTime;
1996 }
1997
1998
1999
2000
2001
2002
2003 @Deprecated
2004 public long getLastModifiedTime() {
2005 return lastModifiedInstant.toEpochMilli();
2006 }
2007
2008
2009
2010
2011
2012 public Instant getLastModifiedInstant() {
2013 return lastModifiedInstant;
2014 }
2015
2016 private final boolean isDirectory;
2017
2018 private final boolean isSymbolicLink;
2019
2020 private final boolean isRegularFile;
2021
2022 private final long creationTime;
2023
2024 private final Instant lastModifiedInstant;
2025
2026 private final boolean isExecutable;
2027
2028 private final File file;
2029
2030 private final boolean exists;
2031
2032
2033
2034
2035 protected long length = -1;
2036
2037 final FS fs;
2038
2039 Attributes(FS fs, File file, boolean exists, boolean isDirectory,
2040 boolean isExecutable, boolean isSymbolicLink,
2041 boolean isRegularFile, long creationTime,
2042 Instant lastModifiedInstant, long length) {
2043 this.fs = fs;
2044 this.file = file;
2045 this.exists = exists;
2046 this.isDirectory = isDirectory;
2047 this.isExecutable = isExecutable;
2048 this.isSymbolicLink = isSymbolicLink;
2049 this.isRegularFile = isRegularFile;
2050 this.creationTime = creationTime;
2051 this.lastModifiedInstant = lastModifiedInstant;
2052 this.length = length;
2053 }
2054
2055
2056
2057
2058
2059
2060
2061
2062 public Attributes(File path, FS fs) {
2063 this(fs, path, false, false, false, false, false, 0L, EPOCH, 0L);
2064 }
2065
2066
2067
2068
2069 public long getLength() {
2070 if (length == -1)
2071 return length = file.length();
2072 return length;
2073 }
2074
2075
2076
2077
2078 public String getName() {
2079 return file.getName();
2080 }
2081
2082
2083
2084
2085 public File getFile() {
2086 return file;
2087 }
2088
2089 boolean exists() {
2090 return exists;
2091 }
2092 }
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102 public Attributes getAttributes(File path) {
2103 boolean isDirectory = isDirectory(path);
2104 boolean isFile = !isDirectory && path.isFile();
2105 assert path.exists() == isDirectory || isFile;
2106 boolean exists = isDirectory || isFile;
2107 boolean canExecute = exists && !isDirectory && canExecute(path);
2108 boolean isSymlink = false;
2109 Instant lastModified = exists ? lastModifiedInstant(path) : EPOCH;
2110 long createTime = 0L;
2111 return new Attributes(this, path, exists, isDirectory, canExecute,
2112 isSymlink, isFile, createTime, lastModified, -1);
2113 }
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123 public File normalize(File file) {
2124 return file;
2125 }
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135 public String normalize(String name) {
2136 return name;
2137 }
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151 private static class StreamGobbler implements Runnable {
2152 private InputStream in;
2153
2154 private OutputStream out;
2155
2156 public StreamGobbler(InputStream stream, OutputStream output) {
2157 this.in = stream;
2158 this.out = output;
2159 }
2160
2161 @Override
2162 public void run() {
2163 try {
2164 copy();
2165 } catch (IOException e) {
2166
2167 }
2168 }
2169
2170 void copy() throws IOException {
2171 boolean writeFailure = false;
2172 byte buffer[] = new byte[4096];
2173 int readBytes;
2174 while ((readBytes = in.read(buffer)) != -1) {
2175
2176
2177
2178 if (!writeFailure && out != null) {
2179 try {
2180 out.write(buffer, 0, readBytes);
2181 out.flush();
2182 } catch (IOException e) {
2183 writeFailure = true;
2184 }
2185 }
2186 }
2187 }
2188 }
2189 }