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