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