org.eclipse.jgit.util.FS.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jgit.util.FS.java

Source

/*
 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.eclipse.jgit.util;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.Instant.EPOCH;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.MessageFormat;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.errors.CommandFailedException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.LockFailedException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.internal.storage.file.FileSnapshot;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry;
import org.eclipse.jgit.treewalk.FileTreeIterator.FileModeStrategy;
import org.eclipse.jgit.treewalk.WorkingTreeIterator.Entry;
import org.eclipse.jgit.util.ProcessResult.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Abstraction to support various file system operations not in Java.
 */
public abstract class FS {
    private static final Logger LOG = LoggerFactory.getLogger(FS.class);

    /**
     * An empty array of entries, suitable as a return value for
     * {@link #list(File, FileModeStrategy)}.
     *
     * @since 5.0
     */
    protected static final Entry[] NO_ENTRIES = {};

    private volatile Boolean supportSymlinks;

    /**
     * This class creates FS instances. It will be overridden by a Java7 variant
     * if such can be detected in {@link #detect(Boolean)}.
     *
     * @since 3.0
     */
    public static class FSFactory {
        /**
         * Constructor
         */
        protected FSFactory() {
            // empty
        }

        /**
         * Detect the file system
         *
         * @param cygwinUsed
         * @return FS instance
         */
        public FS detect(Boolean cygwinUsed) {
            if (SystemReader.getInstance().isWindows()) {
                if (cygwinUsed == null) {
                    cygwinUsed = Boolean.valueOf(FS_Win32_Cygwin.isCygwin());
                }
                if (cygwinUsed.booleanValue()) {
                    return new FS_Win32_Cygwin();
                }
                return new FS_Win32();
            }
            return new FS_POSIX();
        }
    }

    /**
     * Result of an executed process. The caller is responsible to close the
     * contained {@link TemporaryBuffer}s
     *
     * @since 4.2
     */
    public static class ExecutionResult {
        private TemporaryBuffer stdout;

        private TemporaryBuffer stderr;

        private int rc;

        /**
         * @param stdout
         * @param stderr
         * @param rc
         */
        public ExecutionResult(TemporaryBuffer stdout, TemporaryBuffer stderr, int rc) {
            this.stdout = stdout;
            this.stderr = stderr;
            this.rc = rc;
        }

        /**
         * @return buffered standard output stream
         */
        public TemporaryBuffer getStdout() {
            return stdout;
        }

        /**
         * @return buffered standard error stream
         */
        public TemporaryBuffer getStderr() {
            return stderr;
        }

        /**
         * @return the return code of the process
         */
        public int getRc() {
            return rc;
        }
    }

    /**
     * Attributes of FileStores on this system
     *
     * @since 5.1.9
     */
    public final static class FileStoreAttributes {

        private static final Duration UNDEFINED_DURATION = Duration.ofNanos(Long.MAX_VALUE);

        /**
         * Fallback filesystem timestamp resolution. The worst case timestamp
         * resolution on FAT filesystems is 2 seconds.
         */
        public static final Duration FALLBACK_TIMESTAMP_RESOLUTION = Duration.ofMillis(2000);

        /**
         * Fallback FileStore attributes used when we can't measure the
         * filesystem timestamp resolution. The last modified time granularity
         * of FAT filesystems is 2 seconds.
         */
        public static final FileStoreAttributes FALLBACK_FILESTORE_ATTRIBUTES = new FileStoreAttributes(
                FALLBACK_TIMESTAMP_RESOLUTION);

        private static final Map<FileStore, FileStoreAttributes> attributeCache = new ConcurrentHashMap<>();

        private static final SimpleLruCache<Path, FileStoreAttributes> attrCacheByPath = new SimpleLruCache<>(100,
                0.2f);

        private static AtomicBoolean background = new AtomicBoolean();

        private static Map<FileStore, Lock> locks = new ConcurrentHashMap<>();

        private static void setBackground(boolean async) {
            background.set(async);
        }

        private static final String javaVersionPrefix = SystemReader.getInstance().getHostname() + '|'
                + System.getProperty("java.vendor") + '|' //$NON-NLS-1$
                + System.getProperty("java.version") + '|'; //$NON-NLS-1$

        private static final Duration FALLBACK_MIN_RACY_INTERVAL = Duration.ofMillis(10);

        /**
         * Configures size and purge factor of the path-based cache for file
         * system attributes. Caching of file system attributes avoids recurring
         * lookup of @{code FileStore} of files which may be expensive on some
         * platforms.
         *
         * @param maxSize
         *            maximum size of the cache, default is 100
         * @param purgeFactor
         *            when the size of the map reaches maxSize the oldest
         *            entries will be purged to free up some space for new
         *            entries, {@code purgeFactor} is the fraction of
         *            {@code maxSize} to purge when this happens
         * @since 5.1.9
         */
        public static void configureAttributesPathCache(int maxSize, float purgeFactor) {
            FileStoreAttributes.attrCacheByPath.configure(maxSize, purgeFactor);
        }

        /**
         * Get the FileStoreAttributes for the given FileStore
         *
         * @param path
         *            file residing in the FileStore to get attributes for
         * @return FileStoreAttributes for the given path.
         */
        public static FileStoreAttributes get(Path path) {
            try {
                path = path.toAbsolutePath();
                Path dir = Files.isDirectory(path) ? path : path.getParent();
                FileStoreAttributes cached = attrCacheByPath.get(dir);
                if (cached != null) {
                    return cached;
                }
                FileStoreAttributes attrs = getFileStoreAttributes(dir);
                attrCacheByPath.put(dir, attrs);
                return attrs;
            } catch (SecurityException e) {
                return FALLBACK_FILESTORE_ATTRIBUTES;
            }
        }

        private static FileStoreAttributes getFileStoreAttributes(Path dir) {
            FileStore s;
            try {
                if (Files.exists(dir)) {
                    s = Files.getFileStore(dir);
                    FileStoreAttributes c = attributeCache.get(s);
                    if (c != null) {
                        return c;
                    }
                    if (!Files.isWritable(dir)) {
                        // cannot measure resolution in a read-only directory
                        LOG.debug("{}: cannot measure timestamp resolution in read-only directory {}", //$NON-NLS-1$
                                Thread.currentThread(), dir);
                        return FALLBACK_FILESTORE_ATTRIBUTES;
                    }
                } else {
                    // cannot determine FileStore of an unborn directory
                    LOG.debug("{}: cannot measure timestamp resolution of unborn directory {}", //$NON-NLS-1$
                            Thread.currentThread(), dir);
                    return FALLBACK_FILESTORE_ATTRIBUTES;
                }

                CompletableFuture<Optional<FileStoreAttributes>> f = CompletableFuture.supplyAsync(() -> {
                    Lock lock = locks.computeIfAbsent(s, l -> new ReentrantLock());
                    if (!lock.tryLock()) {
                        LOG.debug("{}: couldn't get lock to measure timestamp resolution in {}", //$NON-NLS-1$
                                Thread.currentThread(), dir);
                        return Optional.empty();
                    }
                    Optional<FileStoreAttributes> attributes = Optional.empty();
                    try {
                        // Some earlier future might have set the value
                        // and removed itself since we checked for the
                        // value above. Hence check cache again.
                        FileStoreAttributes c = attributeCache.get(s);
                        if (c != null) {
                            return Optional.of(c);
                        }
                        attributes = readFromConfig(s);
                        if (attributes.isPresent()) {
                            attributeCache.put(s, attributes.get());
                            return attributes;
                        }

                        Optional<Duration> resolution = measureFsTimestampResolution(s, dir);
                        if (resolution.isPresent()) {
                            c = new FileStoreAttributes(resolution.get());
                            attributeCache.put(s, c);
                            // for high timestamp resolution measure
                            // minimal racy interval
                            if (c.fsTimestampResolution.toNanos() < 100_000_000L) {
                                c.minimalRacyInterval = measureMinimalRacyInterval(dir);
                            }
                            if (LOG.isDebugEnabled()) {
                                LOG.debug(c.toString());
                            }
                            saveToConfig(s, c);
                        }
                        attributes = Optional.of(c);
                    } finally {
                        lock.unlock();
                        locks.remove(s);
                    }
                    return attributes;
                });
                f = f.exceptionally(e -> {
                    LOG.error(e.getLocalizedMessage(), e);
                    return Optional.empty();
                });
                // even if measuring in background wait a little - if the result
                // arrives, it's better than returning the large fallback
                Optional<FileStoreAttributes> d = background.get() ? f.get(100, TimeUnit.MILLISECONDS) : f.get();
                if (d.isPresent()) {
                    return d.get();
                }
                // return fallback until measurement is finished
            } catch (IOException | InterruptedException | ExecutionException | CancellationException e) {
                LOG.error(e.getMessage(), e);
            } catch (TimeoutException | SecurityException e) {
                // use fallback
            }
            LOG.debug("{}: use fallback timestamp resolution for directory {}", //$NON-NLS-1$
                    Thread.currentThread(), dir);
            return FALLBACK_FILESTORE_ATTRIBUTES;
        }

        @SuppressWarnings("boxing")
        private static Duration measureMinimalRacyInterval(Path dir) {
            LOG.debug("{}: start measure minimal racy interval in {}", //$NON-NLS-1$
                    Thread.currentThread(), dir);
            int n = 0;
            int failures = 0;
            long racyNanos = 0;
            ArrayList<Long> deltas = new ArrayList<>();
            Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$
            Instant end = Instant.now().plusSeconds(3);
            try {
                Files.createFile(probe);
                do {
                    n++;
                    write(probe, "a"); //$NON-NLS-1$
                    FileSnapshot snapshot = FileSnapshot.save(probe.toFile());
                    read(probe);
                    write(probe, "b"); //$NON-NLS-1$
                    if (!snapshot.isModified(probe.toFile())) {
                        deltas.add(Long.valueOf(snapshot.lastDelta()));
                        racyNanos = snapshot.lastRacyThreshold();
                        failures++;
                    }
                } while (Instant.now().compareTo(end) < 0);
            } catch (IOException e) {
                LOG.error(e.getMessage(), e);
                return FALLBACK_MIN_RACY_INTERVAL;
            } finally {
                deleteProbe(probe);
            }
            if (failures > 0) {
                Stats stats = new Stats();
                for (Long d : deltas) {
                    stats.add(d);
                }
                LOG.debug("delta [ns] since modification FileSnapshot failed to detect\n" //$NON-NLS-1$
                        + "count, failures, racy limit [ns], delta min [ns]," //$NON-NLS-1$
                        + " delta max [ns], delta avg [ns]," //$NON-NLS-1$
                        + " delta stddev [ns]\n" //$NON-NLS-1$
                        + "{}, {}, {}, {}, {}, {}, {}", //$NON-NLS-1$
                        n, failures, racyNanos, stats.min(), stats.max(), stats.avg(), stats.stddev());
                return Duration.ofNanos(Double.valueOf(stats.max()).longValue());
            }
            // since no failures occurred using the measured filesystem
            // timestamp resolution there is no need for minimal racy interval
            LOG.debug("{}: no failures when measuring minimal racy interval", //$NON-NLS-1$
                    Thread.currentThread());
            return Duration.ZERO;
        }

        private static void write(Path p, String body) throws IOException {
            FileUtils.mkdirs(p.getParent().toFile(), true);
            try (Writer w = new OutputStreamWriter(Files.newOutputStream(p), UTF_8)) {
                w.write(body);
            }
        }

        private static String read(Path p) throws IOException {
            final byte[] body = IO.readFully(p.toFile());
            return new String(body, 0, body.length, UTF_8);
        }

        private static Optional<Duration> measureFsTimestampResolution(FileStore s, Path dir) {
            LOG.debug("{}: start measure timestamp resolution {} in {}", //$NON-NLS-1$
                    Thread.currentThread(), s, dir);
            Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$
            try {
                Files.createFile(probe);
                FileTime t1 = Files.getLastModifiedTime(probe);
                FileTime t2 = t1;
                Instant t1i = t1.toInstant();
                for (long i = 1; t2.compareTo(t1) <= 0; i += 1 + i / 20) {
                    Files.setLastModifiedTime(probe, FileTime.from(t1i.plusNanos(i * 1000)));
                    t2 = Files.getLastModifiedTime(probe);
                }
                Duration fsResolution = Duration.between(t1.toInstant(), t2.toInstant());
                Duration clockResolution = measureClockResolution();
                fsResolution = fsResolution.plus(clockResolution);
                LOG.debug("{}: end measure timestamp resolution {} in {}", //$NON-NLS-1$
                        Thread.currentThread(), s, dir);
                return Optional.of(fsResolution);
            } catch (AccessDeniedException e) {
                LOG.warn(e.getLocalizedMessage(), e); // see bug 548648
            } catch (IOException e) {
                LOG.error(e.getLocalizedMessage(), e);
            } finally {
                deleteProbe(probe);
            }
            return Optional.empty();
        }

        private static Duration measureClockResolution() {
            Duration clockResolution = Duration.ZERO;
            for (int i = 0; i < 10; i++) {
                Instant t1 = Instant.now();
                Instant t2 = t1;
                while (t2.compareTo(t1) <= 0) {
                    t2 = Instant.now();
                }
                Duration r = Duration.between(t1, t2);
                if (r.compareTo(clockResolution) > 0) {
                    clockResolution = r;
                }
            }
            return clockResolution;
        }

        private static void deleteProbe(Path probe) {
            try {
                FileUtils.delete(probe.toFile(), FileUtils.SKIP_MISSING | FileUtils.RETRY);
            } catch (IOException e) {
                LOG.error(e.getMessage(), e);
            }
        }

        private static Optional<FileStoreAttributes> readFromConfig(FileStore s) {
            StoredConfig userConfig;
            try {
                userConfig = SystemReader.getInstance().getUserConfig();
            } catch (IOException | ConfigInvalidException e) {
                LOG.error(JGitText.get().readFileStoreAttributesFailed, e);
                return Optional.empty();
            }
            String key = getConfigKey(s);
            Duration resolution = Duration.ofNanos(userConfig.getTimeUnit(ConfigConstants.CONFIG_FILESYSTEM_SECTION,
                    key, ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION, UNDEFINED_DURATION.toNanos(),
                    TimeUnit.NANOSECONDS));
            if (UNDEFINED_DURATION.equals(resolution)) {
                return Optional.empty();
            }
            Duration minRacyThreshold = Duration.ofNanos(userConfig.getTimeUnit(
                    ConfigConstants.CONFIG_FILESYSTEM_SECTION, key, ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD,
                    UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS));
            FileStoreAttributes c = new FileStoreAttributes(resolution);
            if (!UNDEFINED_DURATION.equals(minRacyThreshold)) {
                c.minimalRacyInterval = minRacyThreshold;
            }
            return Optional.of(c);
        }

        private static void saveToConfig(FileStore s, FileStoreAttributes c) {
            StoredConfig jgitConfig;
            try {
                jgitConfig = SystemReader.getInstance().getJGitConfig();
            } catch (IOException | ConfigInvalidException e) {
                LOG.error(JGitText.get().saveFileStoreAttributesFailed, e);
                return;
            }
            long resolution = c.getFsTimestampResolution().toNanos();
            TimeUnit resolutionUnit = getUnit(resolution);
            long resolutionValue = resolutionUnit.convert(resolution, TimeUnit.NANOSECONDS);

            long minRacyThreshold = c.getMinimalRacyInterval().toNanos();
            TimeUnit minRacyThresholdUnit = getUnit(minRacyThreshold);
            long minRacyThresholdValue = minRacyThresholdUnit.convert(minRacyThreshold, TimeUnit.NANOSECONDS);

            final int max_retries = 5;
            int retries = 0;
            boolean succeeded = false;
            String key = getConfigKey(s);
            while (!succeeded && retries < max_retries) {
                try {
                    jgitConfig.setString(ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
                            ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION, String.format("%d %s", //$NON-NLS-1$
                                    Long.valueOf(resolutionValue), resolutionUnit.name().toLowerCase()));
                    jgitConfig.setString(ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
                            ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD, String.format("%d %s", //$NON-NLS-1$
                                    Long.valueOf(minRacyThresholdValue),
                                    minRacyThresholdUnit.name().toLowerCase()));
                    jgitConfig.save();
                    succeeded = true;
                } catch (LockFailedException e) {
                    // race with another thread, wait a bit and try again
                    try {
                        retries++;
                        if (retries < max_retries) {
                            Thread.sleep(100);
                            LOG.debug("locking {} failed, retries {}/{}", //$NON-NLS-1$
                                    jgitConfig, Integer.valueOf(retries), Integer.valueOf(max_retries));
                        } else {
                            LOG.warn(MessageFormat.format(JGitText.get().lockFailedRetry, jgitConfig,
                                    Integer.valueOf(retries)));
                        }
                    } catch (InterruptedException e1) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                } catch (IOException e) {
                    LOG.error(MessageFormat.format(JGitText.get().cannotSaveConfig, jgitConfig), e);
                    break;
                }
            }
        }

        private static String getConfigKey(FileStore s) {
            final String storeKey;
            if (SystemReader.getInstance().isWindows()) {
                Object attribute = null;
                try {
                    attribute = s.getAttribute("volume:vsn"); //$NON-NLS-1$
                } catch (IOException ignored) {
                    // ignore
                }
                if (attribute instanceof Integer) {
                    storeKey = attribute.toString();
                } else {
                    storeKey = s.name();
                }
            } else {
                storeKey = s.name();
            }
            return javaVersionPrefix + storeKey;
        }

        private static TimeUnit getUnit(long nanos) {
            TimeUnit unit;
            if (nanos < 200_000L) {
                unit = TimeUnit.NANOSECONDS;
            } else if (nanos < 200_000_000L) {
                unit = TimeUnit.MICROSECONDS;
            } else {
                unit = TimeUnit.MILLISECONDS;
            }
            return unit;
        }

        private final @NonNull Duration fsTimestampResolution;

        private Duration minimalRacyInterval;

        /**
         * @return the measured minimal interval after a file has been modified
         *         in which we cannot rely on lastModified to detect
         *         modifications
         */
        public Duration getMinimalRacyInterval() {
            return minimalRacyInterval;
        }

        /**
         * @return the measured filesystem timestamp resolution
         */
        @NonNull
        public Duration getFsTimestampResolution() {
            return fsTimestampResolution;
        }

        /**
         * Construct a FileStoreAttributeCache entry for the given filesystem
         * timestamp resolution
         *
         * @param fsTimestampResolution
         */
        public FileStoreAttributes(@NonNull Duration fsTimestampResolution) {
            this.fsTimestampResolution = fsTimestampResolution;
            this.minimalRacyInterval = Duration.ZERO;
        }

        @SuppressWarnings({ "nls", "boxing" })
        @Override
        public String toString() {
            return String.format(
                    "FileStoreAttributes[fsTimestampResolution=%,d s, " + "minimalRacyInterval=%,d s]",
                    fsTimestampResolution.toNanos() / 1000, minimalRacyInterval.toNanos() / 1000);
        }

    }

    /** The auto-detected implementation selected for this operating system and JRE. */
    public static final FS DETECTED = detect();

    private volatile static FSFactory factory;

    /**
     * Auto-detect the appropriate file system abstraction.
     *
     * @return detected file system abstraction
     */
    public static FS detect() {
        return detect(null);
    }

    /**
     * Whether FileStore attributes should be determined asynchronously
     *
     * @param asynch
     *            whether FileStore attributes should be determined
     *            asynchronously. If false access to cached attributes may block
     *            for some seconds for the first call per FileStore
     * @since 5.1.9
     */
    public static void setAsyncFileStoreAttributes(boolean asynch) {
        FileStoreAttributes.setBackground(asynch);
    }

    /**
     * Auto-detect the appropriate file system abstraction, taking into account
     * the presence of a Cygwin installation on the system. Using jgit in
     * combination with Cygwin requires a more elaborate (and possibly slower)
     * resolution of file system paths.
     *
     * @param cygwinUsed
     *            <ul>
     *            <li><code>Boolean.TRUE</code> to assume that Cygwin is used in
     *            combination with jgit</li>
     *            <li><code>Boolean.FALSE</code> to assume that Cygwin is
     *            <b>not</b> used with jgit</li>
     *            <li><code>null</code> to auto-detect whether a Cygwin
     *            installation is present on the system and in this case assume
     *            that Cygwin is used</li>
     *            </ul>
     *
     *            Note: this parameter is only relevant on Windows.
     * @return detected file system abstraction
     */
    public static FS detect(Boolean cygwinUsed) {
        if (factory == null) {
            factory = new FS.FSFactory();
        }
        return factory.detect(cygwinUsed);
    }

    /**
     * Get cached FileStore attributes, if not yet available measure them using
     * a probe file under the given directory.
     *
     * @param dir
     *            the directory under which the probe file will be created to
     *            measure the timer resolution.
     * @return measured filesystem timestamp resolution
     * @since 5.1.9
     */
    public static FileStoreAttributes getFileStoreAttributes(@NonNull Path dir) {
        return FileStoreAttributes.get(dir);
    }

    private volatile Holder<File> userHome;

    private volatile Holder<File> gitSystemConfig;

    /**
     * Constructs a file system abstraction.
     */
    protected FS() {
        // Do nothing by default.
    }

    /**
     * Initialize this FS using another's current settings.
     *
     * @param src
     *            the source FS to copy from.
     */
    protected FS(FS src) {
        userHome = src.userHome;
        gitSystemConfig = src.gitSystemConfig;
    }

    /**
     * Create a new instance of the same type of FS.
     *
     * @return a new instance of the same type of FS.
     */
    public abstract FS newInstance();

    /**
     * Does this operating system and JRE support the execute flag on files?
     *
     * @return true if this implementation can provide reasonably accurate
     *         executable bit information; false otherwise.
     */
    public abstract boolean supportsExecute();

    /**
     * Does this file system support atomic file creation via
     * java.io.File#createNewFile()? In certain environments (e.g. on NFS) it is
     * not guaranteed that when two file system clients run createNewFile() in
     * parallel only one will succeed. In such cases both clients may think they
     * created a new file.
     *
     * @return true if this implementation support atomic creation of new Files
     *         by {@link java.io.File#createNewFile()}
     * @since 4.5
     */
    public boolean supportsAtomicCreateNewFile() {
        return true;
    }

    /**
     * Does this operating system and JRE supports symbolic links. The
     * capability to handle symbolic links is detected at runtime.
     *
     * @return true if symbolic links may be used
     * @since 3.0
     */
    public boolean supportsSymlinks() {
        if (supportSymlinks == null) {
            detectSymlinkSupport();
        }
        return Boolean.TRUE.equals(supportSymlinks);
    }

    private void detectSymlinkSupport() {
        File tempFile = null;
        try {
            tempFile = File.createTempFile("tempsymlinktarget", ""); //$NON-NLS-1$ //$NON-NLS-2$
            File linkName = new File(tempFile.getParentFile(), "tempsymlink"); //$NON-NLS-1$
            createSymLink(linkName, tempFile.getPath());
            supportSymlinks = Boolean.TRUE;
            linkName.delete();
        } catch (IOException | UnsupportedOperationException | SecurityException | InternalError e) {
            supportSymlinks = Boolean.FALSE;
        } finally {
            if (tempFile != null) {
                try {
                    FileUtils.delete(tempFile);
                } catch (IOException e) {
                    LOG.error(JGitText.get().cannotDeleteFile, tempFile);
                }
            }
        }
    }

    /**
     * Is this file system case sensitive
     *
     * @return true if this implementation is case sensitive
     */
    public abstract boolean isCaseSensitive();

    /**
     * Determine if the file is executable (or not).
     * <p>
     * Not all platforms and JREs support executable flags on files. If the
     * feature is unsupported this method will always return false.
     * <p>
     * <em>If the platform supports symbolic links and <code>f</code> is a symbolic link
     * this method returns false, rather than the state of the executable flags
     * on the target file.</em>
     *
     * @param f
     *            abstract path to test.
     * @return true if the file is believed to be executable by the user.
     */
    public abstract boolean canExecute(File f);

    /**
     * Set a file to be executable by the user.
     * <p>
     * Not all platforms and JREs support executable flags on files. If the
     * feature is unsupported this method will always return false and no
     * changes will be made to the file specified.
     *
     * @param f
     *            path to modify the executable status of.
     * @param canExec
     *            true to enable execution; false to disable it.
     * @return true if the change succeeded; false otherwise.
     */
    public abstract boolean setExecute(File f, boolean canExec);

    /**
     * Get the last modified time of a file system object. If the OS/JRE support
     * symbolic links, the modification time of the link is returned, rather
     * than that of the link target.
     *
     * @param f
     *            a {@link java.io.File} object.
     * @return last modified time of f
     * @throws java.io.IOException
     * @since 3.0
     * @deprecated use {@link #lastModifiedInstant(Path)} instead
     */
    @Deprecated
    public long lastModified(File f) throws IOException {
        return FileUtils.lastModified(f);
    }

    /**
     * Get the last modified time of a file system object. If the OS/JRE support
     * symbolic links, the modification time of the link is returned, rather
     * than that of the link target.
     *
     * @param p
     *            a {@link Path} object.
     * @return last modified time of p
     * @since 5.1.9
     */
    public Instant lastModifiedInstant(Path p) {
        return FileUtils.lastModifiedInstant(p);
    }

    /**
     * Get the last modified time of a file system object. If the OS/JRE support
     * symbolic links, the modification time of the link is returned, rather
     * than that of the link target.
     *
     * @param f
     *            a {@link File} object.
     * @return last modified time of p
     * @since 5.1.9
     */
    public Instant lastModifiedInstant(File f) {
        return FileUtils.lastModifiedInstant(f.toPath());
    }

    /**
     * Set the last modified time of a file system object. If the OS/JRE support
     * symbolic links, the link is modified, not the target,
     *
     * @param f
     *            a {@link java.io.File} object.
     * @param time
     *            last modified time
     * @throws java.io.IOException
     * @since 3.0
     * @deprecated use {@link #setLastModified(Path, Instant)} instead
     */
    @Deprecated
    public void setLastModified(File f, long time) throws IOException {
        FileUtils.setLastModified(f, time);
    }

    /**
     * Set the last modified time of a file system object. If the OS/JRE support
     * symbolic links, the link is modified, not the target,
     *
     * @param p
     *            a {@link Path} object.
     * @param time
     *            last modified time
     * @throws java.io.IOException
     * @since 5.1.9
     */
    public void setLastModified(Path p, Instant time) throws IOException {
        FileUtils.setLastModified(p, time);
    }

    /**
     * Get the length of a file or link, If the OS/JRE supports symbolic links
     * it's the length of the link, else the length of the target.
     *
     * @param path
     *            a {@link java.io.File} object.
     * @return length of a file
     * @throws java.io.IOException
     * @since 3.0
     */
    public long length(File path) throws IOException {
        return FileUtils.getLength(path);
    }

    /**
     * Delete a file. Throws an exception if delete fails.
     *
     * @param f
     *            a {@link java.io.File} object.
     * @throws java.io.IOException
     *             this may be a Java7 subclass with detailed information
     * @since 3.3
     */
    public void delete(File f) throws IOException {
        FileUtils.delete(f);
    }

    /**
     * Resolve this file to its actual path name that the JRE can use.
     * <p>
     * This method can be relatively expensive. Computing a translation may
     * require forking an external process per path name translated. Callers
     * should try to minimize the number of translations necessary by caching
     * the results.
     * <p>
     * Not all platforms and JREs require path name translation. Currently only
     * Cygwin on Win32 require translation for Cygwin based paths.
     *
     * @param dir
     *            directory relative to which the path name is.
     * @param name
     *            path name to translate.
     * @return the translated path. <code>new File(dir,name)</code> if this
     *         platform does not require path name translation.
     */
    public File resolve(File dir, String name) {
        final File abspn = new File(name);
        if (abspn.isAbsolute())
            return abspn;
        return new File(dir, name);
    }

    /**
     * Determine the user's home directory (location where preferences are).
     * <p>
     * This method can be expensive on the first invocation if path name
     * translation is required. Subsequent invocations return a cached result.
     * <p>
     * Not all platforms and JREs require path name translation. Currently only
     * Cygwin on Win32 requires translation of the Cygwin HOME directory.
     *
     * @return the user's home directory; null if the user does not have one.
     */
    public File userHome() {
        Holder<File> p = userHome;
        if (p == null) {
            p = new Holder<>(userHomeImpl());
            userHome = p;
        }
        return p.value;
    }

    /**
     * Set the user's home directory location.
     *
     * @param path
     *            the location of the user's preferences; null if there is no
     *            home directory for the current user.
     * @return {@code this}.
     */
    public FS setUserHome(File path) {
        userHome = new Holder<>(path);
        return this;
    }

    /**
     * Does this file system have problems with atomic renames?
     *
     * @return true if the caller should retry a failed rename of a lock file.
     */
    public abstract boolean retryFailedLockFileCommit();

    /**
     * Return all the attributes of a file, without following symbolic links.
     *
     * @param file
     * @return {@link BasicFileAttributes} of the file
     * @throws IOException in case of any I/O errors accessing the file
     *
     * @since 4.5.6
     */
    public BasicFileAttributes fileAttributes(File file) throws IOException {
        return FileUtils.fileAttributes(file);
    }

    /**
     * Determine the user's home directory (location where preferences are).
     *
     * @return the user's home directory; null if the user does not have one.
     */
    protected File userHomeImpl() {
        final String home = AccessController
                .doPrivileged((PrivilegedAction<String>) () -> System.getProperty("user.home") //$NON-NLS-1$
        );
        if (home == null || home.length() == 0)
            return null;
        return new File(home).getAbsoluteFile();
    }

    /**
     * Searches the given path to see if it contains one of the given files.
     * Returns the first it finds. Returns null if not found or if path is null.
     *
     * @param path
     *            List of paths to search separated by File.pathSeparator
     * @param lookFor
     *            Files to search for in the given path
     * @return the first match found, or null
     * @since 3.0
     */
    protected static File searchPath(String path, String... lookFor) {
        if (path == null)
            return null;

        for (String p : path.split(File.pathSeparator)) {
            for (String command : lookFor) {
                final File file = new File(p, command);
                try {
                    if (file.isFile()) {
                        return file.getAbsoluteFile();
                    }
                } catch (SecurityException e) {
                    LOG.warn(MessageFormat.format(JGitText.get().skipNotAccessiblePath, file.getPath()));
                }
            }
        }
        return null;
    }

    /**
     * Execute a command and return a single line of output as a String
     *
     * @param dir
     *            Working directory for the command
     * @param command
     *            as component array
     * @param encoding
     *            to be used to parse the command's output
     * @return the one-line output of the command or {@code null} if there is
     *         none
     * @throws org.eclipse.jgit.errors.CommandFailedException
     *             thrown when the command failed (return code was non-zero)
     */
    @Nullable
    protected static String readPipe(File dir, String[] command, String encoding) throws CommandFailedException {
        return readPipe(dir, command, encoding, null);
    }

    /**
     * Execute a command and return a single line of output as a String
     *
     * @param dir
     *            Working directory for the command
     * @param command
     *            as component array
     * @param encoding
     *            to be used to parse the command's output
     * @param env
     *            Map of environment variables to be merged with those of the
     *            current process
     * @return the one-line output of the command or {@code null} if there is
     *         none
     * @throws org.eclipse.jgit.errors.CommandFailedException
     *             thrown when the command failed (return code was non-zero)
     * @since 4.0
     */
    @Nullable
    protected static String readPipe(File dir, String[] command, String encoding, Map<String, String> env)
            throws CommandFailedException {
        final boolean debug = LOG.isDebugEnabled();
        try {
            if (debug) {
                LOG.debug("readpipe " + Arrays.asList(command) + "," //$NON-NLS-1$ //$NON-NLS-2$
                        + dir);
            }
            ProcessBuilder pb = new ProcessBuilder(command);
            pb.directory(dir);
            if (env != null) {
                pb.environment().putAll(env);
            }
            Process p;
            try {
                p = pb.start();
            } catch (IOException e) {
                // Process failed to start
                throw new CommandFailedException(-1, e.getMessage(), e);
            }
            p.getOutputStream().close();
            GobblerThread gobbler = new GobblerThread(p, command, dir);
            gobbler.start();
            String r = null;
            try (BufferedReader lineRead = new BufferedReader(
                    new InputStreamReader(p.getInputStream(), encoding))) {
                r = lineRead.readLine();
                if (debug) {
                    LOG.debug("readpipe may return '" + r + "'"); //$NON-NLS-1$ //$NON-NLS-2$
                    LOG.debug("remaining output:\n"); //$NON-NLS-1$
                    String l;
                    while ((l = lineRead.readLine()) != null) {
                        LOG.debug(l);
                    }
                }
            }

            for (;;) {
                try {
                    int rc = p.waitFor();
                    gobbler.join();
                    if (rc == 0 && !gobbler.fail.get()) {
                        return r;
                    }
                    if (debug) {
                        LOG.debug("readpipe rc=" + rc); //$NON-NLS-1$
                    }
                    throw new CommandFailedException(rc, gobbler.errorMessage.get(), gobbler.exception.get());
                } catch (InterruptedException ie) {
                    // Stop bothering me, I have a zombie to reap.
                }
            }
        } catch (IOException e) {
            LOG.error("Caught exception in FS.readPipe()", e); //$NON-NLS-1$
        } catch (AccessControlException e) {
            LOG.warn(MessageFormat.format(JGitText.get().readPipeIsNotAllowedRequiredPermission, command, dir,
                    e.getPermission()));
        } catch (SecurityException e) {
            LOG.warn(MessageFormat.format(JGitText.get().readPipeIsNotAllowed, command, dir));
        }
        if (debug) {
            LOG.debug("readpipe returns null"); //$NON-NLS-1$
        }
        return null;
    }

    private static class GobblerThread extends Thread {

        /* The process has 5 seconds to exit after closing stderr */
        private static final int PROCESS_EXIT_TIMEOUT = 5;

        private final Process p;
        private final String desc;
        private final String dir;
        final AtomicBoolean fail = new AtomicBoolean();
        final AtomicReference<String> errorMessage = new AtomicReference<>();
        final AtomicReference<Throwable> exception = new AtomicReference<>();

        GobblerThread(Process p, String[] command, File dir) {
            this.p = p;
            this.desc = Arrays.toString(command);
            this.dir = Objects.toString(dir);
        }

        @Override
        public void run() {
            StringBuilder err = new StringBuilder();
            try (InputStream is = p.getErrorStream()) {
                int ch;
                while ((ch = is.read()) != -1) {
                    err.append((char) ch);
                }
            } catch (IOException e) {
                if (waitForProcessCompletion(e) && p.exitValue() != 0) {
                    setError(e, e.getMessage(), p.exitValue());
                    fail.set(true);
                } else {
                    // ignore. command terminated faster and stream was just closed
                    // or the process didn't terminate within timeout
                }
            } finally {
                if (waitForProcessCompletion(null) && err.length() > 0) {
                    setError(null, err.toString(), p.exitValue());
                    if (p.exitValue() != 0) {
                        fail.set(true);
                    }
                }
            }
        }

        @SuppressWarnings("boxing")
        private boolean waitForProcessCompletion(IOException originalError) {
            try {
                if (!p.waitFor(PROCESS_EXIT_TIMEOUT, TimeUnit.SECONDS)) {
                    setError(originalError, MessageFormat.format(JGitText.get().commandClosedStderrButDidntExit,
                            desc, PROCESS_EXIT_TIMEOUT), -1);
                    fail.set(true);
                    return false;
                }
            } catch (InterruptedException e) {
                setError(originalError, MessageFormat.format(JGitText.get().threadInterruptedWhileRunning, desc),
                        -1);
                fail.set(true);
                return false;
            }
            return true;
        }

        private void setError(IOException e, String message, int exitCode) {
            exception.set(e);
            errorMessage.set(MessageFormat.format(JGitText.get().exceptionCaughtDuringExecutionOfCommand, desc, dir,
                    Integer.valueOf(exitCode), message));
        }
    }

    /**
     * Discover the path to the Git executable.
     *
     * @return the path to the Git executable or {@code null} if it cannot be
     *         determined.
     * @since 4.0
     */
    protected abstract File discoverGitExe();

    /**
     * Discover the path to the system-wide Git configuration file
     *
     * @return the path to the system-wide Git configuration file or
     *         {@code null} if it cannot be determined.
     * @since 4.0
     */
    protected File discoverGitSystemConfig() {
        File gitExe = discoverGitExe();
        if (gitExe == null) {
            return null;
        }

        // Bug 480782: Check if the discovered git executable is JGit CLI
        String v;
        try {
            v = readPipe(gitExe.getParentFile(), new String[] { "git", "--version" }, //$NON-NLS-1$ //$NON-NLS-2$
                    Charset.defaultCharset().name());
        } catch (CommandFailedException e) {
            LOG.warn(e.getMessage());
            return null;
        }
        if (StringUtils.isEmptyOrNull(v) || (v != null && v.startsWith("jgit"))) { //$NON-NLS-1$
            return null;
        }

        // Trick Git into printing the path to the config file by using "echo"
        // as the editor.
        Map<String, String> env = new HashMap<>();
        env.put("GIT_EDITOR", "echo"); //$NON-NLS-1$ //$NON-NLS-2$

        String w;
        try {
            w = readPipe(gitExe.getParentFile(), new String[] { "git", "config", "--system", "--edit" }, //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
                    Charset.defaultCharset().name(), env);
        } catch (CommandFailedException e) {
            LOG.warn(e.getMessage());
            return null;
        }
        if (StringUtils.isEmptyOrNull(w)) {
            return null;
        }

        return new File(w);
    }

    /**
     * Get the currently used path to the system-wide Git configuration file.
     *
     * @return the currently used path to the system-wide Git configuration file
     *         or {@code null} if none has been set.
     * @since 4.0
     */
    public File getGitSystemConfig() {
        if (gitSystemConfig == null) {
            gitSystemConfig = new Holder<>(discoverGitSystemConfig());
        }
        return gitSystemConfig.value;
    }

    /**
     * Set the path to the system-wide Git configuration file to use.
     *
     * @param configFile
     *            the path to the config file.
     * @return {@code this}
     * @since 4.0
     */
    public FS setGitSystemConfig(File configFile) {
        gitSystemConfig = new Holder<>(configFile);
        return this;
    }

    /**
     * Get the parent directory of this file's parent directory
     *
     * @param grandchild
     *            a {@link java.io.File} object.
     * @return the parent directory of this file's parent directory or
     *         {@code null} in case there's no grandparent directory
     * @since 4.0
     */
    protected static File resolveGrandparentFile(File grandchild) {
        if (grandchild != null) {
            File parent = grandchild.getParentFile();
            if (parent != null)
                return parent.getParentFile();
        }
        return null;
    }

    /**
     * Check if a file is a symbolic link and read it
     *
     * @param path
     *            a {@link java.io.File} object.
     * @return target of link or null
     * @throws java.io.IOException
     * @since 3.0
     */
    public String readSymLink(File path) throws IOException {
        return FileUtils.readSymLink(path);
    }

    /**
     * Whether the path is a symbolic link (and we support these).
     *
     * @param path
     *            a {@link java.io.File} object.
     * @return true if the path is a symbolic link (and we support these)
     * @throws java.io.IOException
     * @since 3.0
     */
    public boolean isSymLink(File path) throws IOException {
        return FileUtils.isSymlink(path);
    }

    /**
     * Tests if the path exists, in case of a symbolic link, true even if the
     * target does not exist
     *
     * @param path
     *            a {@link java.io.File} object.
     * @return true if path exists
     * @since 3.0
     */
    public boolean exists(File path) {
        return FileUtils.exists(path);
    }

    /**
     * Check if path is a directory. If the OS/JRE supports symbolic links and
     * path is a symbolic link to a directory, this method returns false.
     *
     * @param path
     *            a {@link java.io.File} object.
     * @return true if file is a directory,
     * @since 3.0
     */
    public boolean isDirectory(File path) {
        return FileUtils.isDirectory(path);
    }

    /**
     * Examine if path represents a regular file. If the OS/JRE supports
     * symbolic links the test returns false if path represents a symbolic link.
     *
     * @param path
     *            a {@link java.io.File} object.
     * @return true if path represents a regular file
     * @since 3.0
     */
    public boolean isFile(File path) {
        return FileUtils.isFile(path);
    }

    /**
     * Whether path is hidden, either starts with . on unix or has the hidden
     * attribute in windows
     *
     * @param path
     *            a {@link java.io.File} object.
     * @return true if path is hidden, either starts with . on unix or has the
     *         hidden attribute in windows
     * @throws java.io.IOException
     * @since 3.0
     */
    public boolean isHidden(File path) throws IOException {
        return FileUtils.isHidden(path);
    }

    /**
     * Set the hidden attribute for file whose name starts with a period.
     *
     * @param path
     *            a {@link java.io.File} object.
     * @param hidden
     *            whether to set the file hidden
     * @throws java.io.IOException
     * @since 3.0
     */
    public void setHidden(File path, boolean hidden) throws IOException {
        FileUtils.setHidden(path, hidden);
    }

    /**
     * Create a symbolic link
     *
     * @param path
     *            a {@link java.io.File} object.
     * @param target
     *            target path of the symlink
     * @throws java.io.IOException
     * @since 3.0
     */
    public void createSymLink(File path, String target) throws IOException {
        FileUtils.createSymLink(path, target);
    }

    /**
     * Create a new file. See {@link java.io.File#createNewFile()}. Subclasses
     * of this class may take care to provide a safe implementation for this
     * even if {@link #supportsAtomicCreateNewFile()} is <code>false</code>
     *
     * @param path
     *            the file to be created
     * @return <code>true</code> if the file was created, <code>false</code> if
     *         the file already existed
     * @throws java.io.IOException
     * @deprecated use {@link #createNewFileAtomic(File)} instead
     * @since 4.5
     */
    @Deprecated
    public boolean createNewFile(File path) throws IOException {
        return path.createNewFile();
    }

    /**
     * A token representing a file created by
     * {@link #createNewFileAtomic(File)}. The token must be retained until the
     * file has been deleted in order to guarantee that the unique file was
     * created atomically. As soon as the file is no longer needed the lock
     * token must be closed.
     *
     * @since 4.7
     */
    public static class LockToken implements Closeable {
        private boolean isCreated;

        private Optional<Path> link;

        LockToken(boolean isCreated, Optional<Path> link) {
            this.isCreated = isCreated;
            this.link = link;
        }

        /**
         * @return {@code true} if the file was created successfully
         */
        public boolean isCreated() {
            return isCreated;
        }

        @Override
        public void close() {
            if (!link.isPresent()) {
                return;
            }
            Path p = link.get();
            if (!Files.exists(p)) {
                return;
            }
            try {
                Files.delete(p);
            } catch (IOException e) {
                LOG.error(MessageFormat.format(JGitText.get().closeLockTokenFailed, this), e);
            }
        }

        @Override
        public String toString() {
            return "LockToken [lockCreated=" + isCreated + //$NON-NLS-1$
                    ", link=" //$NON-NLS-1$
                    + (link.isPresent() ? link.get().getFileName() + "]" //$NON-NLS-1$
                            : "<null>]"); //$NON-NLS-1$
        }
    }

    /**
     * Create a new file. See {@link java.io.File#createNewFile()}. Subclasses
     * of this class may take care to provide a safe implementation for this
     * even if {@link #supportsAtomicCreateNewFile()} is <code>false</code>
     *
     * @param path
     *            the file to be created
     * @return LockToken this token must be closed after the created file was
     *         deleted
     * @throws IOException
     * @since 4.7
     */
    public LockToken createNewFileAtomic(File path) throws IOException {
        return new LockToken(path.createNewFile(), Optional.empty());
    }

    /**
     * See
     * {@link org.eclipse.jgit.util.FileUtils#relativizePath(String, String, String, boolean)}.
     *
     * @param base
     *            The path against which <code>other</code> should be
     *            relativized.
     * @param other
     *            The path that will be made relative to <code>base</code>.
     * @return A relative path that, when resolved against <code>base</code>,
     *         will yield the original <code>other</code>.
     * @see FileUtils#relativizePath(String, String, String, boolean)
     * @since 3.7
     */
    public String relativize(String base, String other) {
        return FileUtils.relativizePath(base, other, File.separator, this.isCaseSensitive());
    }

    /**
     * Enumerates children of a directory.
     *
     * @param directory
     *            to get the children of
     * @param fileModeStrategy
     *            to use to calculate the git mode of a child
     * @return an array of entries for the children
     *
     * @since 5.0
     */
    public Entry[] list(File directory, FileModeStrategy fileModeStrategy) {
        final File[] all = directory.listFiles();
        if (all == null) {
            return NO_ENTRIES;
        }
        final Entry[] result = new Entry[all.length];
        for (int i = 0; i < result.length; i++) {
            result[i] = new FileEntry(all[i], this, fileModeStrategy);
        }
        return result;
    }

    /**
     * Checks whether the given hook is defined for the given repository, then
     * runs it with the given arguments.
     * <p>
     * The hook's standard output and error streams will be redirected to
     * <code>System.out</code> and <code>System.err</code> respectively. The
     * hook will have no stdin.
     * </p>
     *
     * @param repository
     *            The repository for which a hook should be run.
     * @param hookName
     *            The name of the hook to be executed.
     * @param args
     *            Arguments to pass to this hook. Cannot be <code>null</code>,
     *            but can be an empty array.
     * @return The ProcessResult describing this hook's execution.
     * @throws org.eclipse.jgit.api.errors.JGitInternalException
     *             if we fail to run the hook somehow. Causes may include an
     *             interrupted process or I/O errors.
     * @since 4.0
     */
    public ProcessResult runHookIfPresent(Repository repository, final String hookName, String[] args)
            throws JGitInternalException {
        return runHookIfPresent(repository, hookName, args, System.out, System.err, null);
    }

    /**
     * Checks whether the given hook is defined for the given repository, then
     * runs it with the given arguments.
     *
     * @param repository
     *            The repository for which a hook should be run.
     * @param hookName
     *            The name of the hook to be executed.
     * @param args
     *            Arguments to pass to this hook. Cannot be <code>null</code>,
     *            but can be an empty array.
     * @param outRedirect
     *            A print stream on which to redirect the hook's stdout. Can be
     *            <code>null</code>, in which case the hook's standard output
     *            will be lost.
     * @param errRedirect
     *            A print stream on which to redirect the hook's stderr. Can be
     *            <code>null</code>, in which case the hook's standard error
     *            will be lost.
     * @param stdinArgs
     *            A string to pass on to the standard input of the hook. May be
     *            <code>null</code>.
     * @return The ProcessResult describing this hook's execution.
     * @throws org.eclipse.jgit.api.errors.JGitInternalException
     *             if we fail to run the hook somehow. Causes may include an
     *             interrupted process or I/O errors.
     * @since 4.0
     */
    public ProcessResult runHookIfPresent(Repository repository, final String hookName, String[] args,
            PrintStream outRedirect, PrintStream errRedirect, String stdinArgs) throws JGitInternalException {
        return new ProcessResult(Status.NOT_SUPPORTED);
    }

    /**
     * See
     * {@link #runHookIfPresent(Repository, String, String[], PrintStream, PrintStream, String)}
     * . Should only be called by FS supporting shell scripts execution.
     *
     * @param repository
     *            The repository for which a hook should be run.
     * @param hookName
     *            The name of the hook to be executed.
     * @param args
     *            Arguments to pass to this hook. Cannot be <code>null</code>,
     *            but can be an empty array.
     * @param outRedirect
     *            A print stream on which to redirect the hook's stdout. Can be
     *            <code>null</code>, in which case the hook's standard output
     *            will be lost.
     * @param errRedirect
     *            A print stream on which to redirect the hook's stderr. Can be
     *            <code>null</code>, in which case the hook's standard error
     *            will be lost.
     * @param stdinArgs
     *            A string to pass on to the standard input of the hook. May be
     *            <code>null</code>.
     * @return The ProcessResult describing this hook's execution.
     * @throws org.eclipse.jgit.api.errors.JGitInternalException
     *             if we fail to run the hook somehow. Causes may include an
     *             interrupted process or I/O errors.
     * @since 4.0
     */
    protected ProcessResult internalRunHookIfPresent(Repository repository, final String hookName, String[] args,
            PrintStream outRedirect, PrintStream errRedirect, String stdinArgs) throws JGitInternalException {
        File hookFile = findHook(repository, hookName);
        if (hookFile == null || hookName == null) {
            return new ProcessResult(Status.NOT_PRESENT);
        }

        File runDirectory = getRunDirectory(repository, hookName);
        if (runDirectory == null) {
            return new ProcessResult(Status.NOT_PRESENT);
        }
        String cmd = hookFile.getAbsolutePath();
        ProcessBuilder hookProcess = runInShell(cmd, args);
        hookProcess.directory(runDirectory.getAbsoluteFile());
        Map<String, String> environment = hookProcess.environment();
        environment.put(Constants.GIT_DIR_KEY, repository.getDirectory().getAbsolutePath());
        if (!repository.isBare()) {
            environment.put(Constants.GIT_WORK_TREE_KEY, repository.getWorkTree().getAbsolutePath());
        }
        try {
            return new ProcessResult(runProcess(hookProcess, outRedirect, errRedirect, stdinArgs), Status.OK);
        } catch (IOException e) {
            throw new JGitInternalException(
                    MessageFormat.format(JGitText.get().exceptionCaughtDuringExecutionOfHook, hookName), e);
        } catch (InterruptedException e) {
            throw new JGitInternalException(
                    MessageFormat.format(JGitText.get().exceptionHookExecutionInterrupted, hookName), e);
        }
    }

    /**
     * Tries to find a hook matching the given one in the given repository.
     *
     * @param repository
     *            The repository within which to find a hook.
     * @param hookName
     *            The name of the hook we're trying to find.
     * @return The {@link java.io.File} containing this particular hook if it
     *         exists in the given repository, <code>null</code> otherwise.
     * @since 4.0
     */
    public File findHook(Repository repository, String hookName) {
        if (hookName == null) {
            return null;
        }
        File hookDir = getHooksDirectory(repository);
        if (hookDir == null) {
            return null;
        }
        File hookFile = new File(hookDir, hookName);
        if (hookFile.isAbsolute()) {
            if (!hookFile.exists() || (FS.DETECTED.supportsExecute() && !FS.DETECTED.canExecute(hookFile))) {
                return null;
            }
        } else {
            try {
                File runDirectory = getRunDirectory(repository, hookName);
                if (runDirectory == null) {
                    return null;
                }
                Path hookPath = runDirectory.getAbsoluteFile().toPath().resolve(hookFile.toPath());
                FS fs = repository.getFS();
                if (fs == null) {
                    fs = FS.DETECTED;
                }
                if (!Files.exists(hookPath) || (fs.supportsExecute() && !fs.canExecute(hookPath.toFile()))) {
                    return null;
                }
                hookFile = hookPath.toFile();
            } catch (InvalidPathException e) {
                LOG.warn(MessageFormat.format(JGitText.get().invalidHooksPath, hookFile));
                return null;
            }
        }
        return hookFile;
    }

    private File getRunDirectory(Repository repository, @NonNull String hookName) {
        if (repository.isBare()) {
            return repository.getDirectory();
        }
        switch (hookName) {
        case "pre-receive": //$NON-NLS-1$
        case "update": //$NON-NLS-1$
        case "post-receive": //$NON-NLS-1$
        case "post-update": //$NON-NLS-1$
        case "push-to-checkout": //$NON-NLS-1$
            return repository.getDirectory();
        default:
            return repository.getWorkTree();
        }
    }

    private File getHooksDirectory(Repository repository) {
        Config config = repository.getConfig();
        String hooksDir = config.getString(ConfigConstants.CONFIG_CORE_SECTION, null,
                ConfigConstants.CONFIG_KEY_HOOKS_PATH);
        if (hooksDir != null) {
            return new File(hooksDir);
        }
        File dir = repository.getDirectory();
        return dir == null ? null : new File(dir, Constants.HOOKS);
    }

    /**
     * Runs the given process until termination, clearing its stdout and stderr
     * streams on-the-fly.
     *
     * @param processBuilder
     *            The process builder configured for this process.
     * @param outRedirect
     *            A OutputStream on which to redirect the processes stdout. Can
     *            be <code>null</code>, in which case the processes standard
     *            output will be lost.
     * @param errRedirect
     *            A OutputStream on which to redirect the processes stderr. Can
     *            be <code>null</code>, in which case the processes standard
     *            error will be lost.
     * @param stdinArgs
     *            A string to pass on to the standard input of the hook. Can be
     *            <code>null</code>.
     * @return the exit value of this process.
     * @throws java.io.IOException
     *             if an I/O error occurs while executing this process.
     * @throws java.lang.InterruptedException
     *             if the current thread is interrupted while waiting for the
     *             process to end.
     * @since 4.2
     */
    public int runProcess(ProcessBuilder processBuilder, OutputStream outRedirect, OutputStream errRedirect,
            String stdinArgs) throws IOException, InterruptedException {
        InputStream in = (stdinArgs == null) ? null : new ByteArrayInputStream(stdinArgs.getBytes(UTF_8));
        return runProcess(processBuilder, outRedirect, errRedirect, in);
    }

    /**
     * Runs the given process until termination, clearing its stdout and stderr
     * streams on-the-fly.
     *
     * @param processBuilder
     *            The process builder configured for this process.
     * @param outRedirect
     *            An OutputStream on which to redirect the processes stdout. Can
     *            be <code>null</code>, in which case the processes standard
     *            output will be lost.
     * @param errRedirect
     *            An OutputStream on which to redirect the processes stderr. Can
     *            be <code>null</code>, in which case the processes standard
     *            error will be lost.
     * @param inRedirect
     *            An InputStream from which to redirect the processes stdin. Can
     *            be <code>null</code>, in which case the process doesn't get
     *            any data over stdin. It is assumed that the whole InputStream
     *            will be consumed by the process. The method will close the
     *            inputstream after all bytes are read.
     * @return the return code of this process.
     * @throws java.io.IOException
     *             if an I/O error occurs while executing this process.
     * @throws java.lang.InterruptedException
     *             if the current thread is interrupted while waiting for the
     *             process to end.
     * @since 4.2
     */
    public int runProcess(ProcessBuilder processBuilder, OutputStream outRedirect, OutputStream errRedirect,
            InputStream inRedirect) throws IOException, InterruptedException {
        final ExecutorService executor = Executors.newFixedThreadPool(2);
        Process process = null;
        // We'll record the first I/O exception that occurs, but keep on trying
        // to dispose of our open streams and file handles
        IOException ioException = null;
        try {
            process = processBuilder.start();
            executor.execute(new StreamGobbler(process.getErrorStream(), errRedirect));
            executor.execute(new StreamGobbler(process.getInputStream(), outRedirect));
            @SuppressWarnings("resource") // Closed in the finally block
            OutputStream outputStream = process.getOutputStream();
            try {
                if (inRedirect != null) {
                    new StreamGobbler(inRedirect, outputStream).copy();
                }
            } finally {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    // When the process exits before consuming the input, the OutputStream
                    // is replaced with the null output stream. This null output stream
                    // throws IOException for all write calls. When StreamGobbler fails to
                    // flush the buffer because of this, this close call tries to flush it
                    // again. This causes another IOException. Since we ignore the
                    // IOException in StreamGobbler, we also ignore the exception here.
                }
            }
            return process.waitFor();
        } catch (IOException e) {
            ioException = e;
        } finally {
            shutdownAndAwaitTermination(executor);
            if (process != null) {
                try {
                    process.waitFor();
                } catch (InterruptedException e) {
                    // Thrown by the outer try.
                    // Swallow this one to carry on our cleanup, and clear the
                    // interrupted flag (processes throw the exception without
                    // clearing the flag).
                    Thread.interrupted();
                }
                // A process doesn't clean its own resources even when destroyed
                // Explicitly try and close all three streams, preserving the
                // outer I/O exception if any.
                if (inRedirect != null) {
                    inRedirect.close();
                }
                try {
                    process.getErrorStream().close();
                } catch (IOException e) {
                    ioException = ioException != null ? ioException : e;
                }
                try {
                    process.getInputStream().close();
                } catch (IOException e) {
                    ioException = ioException != null ? ioException : e;
                }
                try {
                    process.getOutputStream().close();
                } catch (IOException e) {
                    ioException = ioException != null ? ioException : e;
                }
                process.destroy();
            }
        }
        // We can only be here if the outer try threw an IOException.
        throw ioException;
    }

    /**
     * Shuts down an {@link ExecutorService} in two phases, first by calling
     * {@link ExecutorService#shutdown() shutdown} to reject incoming tasks, and
     * then calling {@link ExecutorService#shutdownNow() shutdownNow}, if
     * necessary, to cancel any lingering tasks. Returns true if the pool has
     * been properly shutdown, false otherwise.
     * <p>
     *
     * @param pool
     *            the pool to shutdown
     * @return <code>true</code> if the pool has been properly shutdown,
     *         <code>false</code> otherwise.
     */
    private static boolean shutdownAndAwaitTermination(ExecutorService pool) {
        boolean hasShutdown = true;
        pool.shutdown(); // Disable new tasks from being submitted
        try {
            // Wait a while for existing tasks to terminate
            if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
                pool.shutdownNow(); // Cancel currently executing tasks
                // Wait a while for tasks to respond to being canceled
                if (!pool.awaitTermination(60, TimeUnit.SECONDS))
                    hasShutdown = false;
            }
        } catch (InterruptedException ie) {
            // (Re-)Cancel if current thread also interrupted
            pool.shutdownNow();
            // Preserve interrupt status
            Thread.currentThread().interrupt();
            hasShutdown = false;
        }
        return hasShutdown;
    }

    /**
     * Initialize a ProcessBuilder to run a command using the system shell.
     *
     * @param cmd
     *            command to execute. This string should originate from the
     *            end-user, and thus is platform specific.
     * @param args
     *            arguments to pass to command. These should be protected from
     *            shell evaluation.
     * @return a partially completed process builder. Caller should finish
     *         populating directory, environment, and then start the process.
     */
    public abstract ProcessBuilder runInShell(String cmd, String[] args);

    /**
     * Execute a command defined by a {@link java.lang.ProcessBuilder}.
     *
     * @param pb
     *            The command to be executed
     * @param in
     *            The standard input stream passed to the process
     * @return The result of the executed command
     * @throws java.lang.InterruptedException
     * @throws java.io.IOException
     * @since 4.2
     */
    public ExecutionResult execute(ProcessBuilder pb, InputStream in) throws IOException, InterruptedException {
        try (TemporaryBuffer stdout = new TemporaryBuffer.LocalFile(null);
                TemporaryBuffer stderr = new TemporaryBuffer.Heap(1024, 1024 * 1024)) {
            int rc = runProcess(pb, stdout, stderr, in);
            return new ExecutionResult(stdout, stderr, rc);
        }
    }

    private static class Holder<V> {
        final V value;

        Holder(V value) {
            this.value = value;
        }
    }

    /**
     * File attributes we typically care for.
     *
     * @since 3.3
     */
    public static class Attributes {

        /**
         * @return true if this are the attributes of a directory
         */
        public boolean isDirectory() {
            return isDirectory;
        }

        /**
         * @return true if this are the attributes of an executable file
         */
        public boolean isExecutable() {
            return isExecutable;
        }

        /**
         * @return true if this are the attributes of a symbolic link
         */
        public boolean isSymbolicLink() {
            return isSymbolicLink;
        }

        /**
         * @return true if this are the attributes of a regular file
         */
        public boolean isRegularFile() {
            return isRegularFile;
        }

        /**
         * @return the time when the file was created
         */
        public long getCreationTime() {
            return creationTime;
        }

        /**
         * @return the time (milliseconds since 1970-01-01) when this object was
         *         last modified
         * @deprecated use getLastModifiedInstant instead
         */
        @Deprecated
        public long getLastModifiedTime() {
            return lastModifiedInstant.toEpochMilli();
        }

        /**
         * @return the time when this object was last modified
         * @since 5.1.9
         */
        public Instant getLastModifiedInstant() {
            return lastModifiedInstant;
        }

        private final boolean isDirectory;

        private final boolean isSymbolicLink;

        private final boolean isRegularFile;

        private final long creationTime;

        private final Instant lastModifiedInstant;

        private final boolean isExecutable;

        private final File file;

        private final boolean exists;

        /**
         * file length
         */
        protected long length = -1;

        final FS fs;

        Attributes(FS fs, File file, boolean exists, boolean isDirectory, boolean isExecutable,
                boolean isSymbolicLink, boolean isRegularFile, long creationTime, Instant lastModifiedInstant,
                long length) {
            this.fs = fs;
            this.file = file;
            this.exists = exists;
            this.isDirectory = isDirectory;
            this.isExecutable = isExecutable;
            this.isSymbolicLink = isSymbolicLink;
            this.isRegularFile = isRegularFile;
            this.creationTime = creationTime;
            this.lastModifiedInstant = lastModifiedInstant;
            this.length = length;
        }

        /**
         * Constructor when there are issues with reading. All attributes except
         * given will be set to the default values.
         *
         * @param fs
         * @param path
         */
        public Attributes(File path, FS fs) {
            this(fs, path, false, false, false, false, false, 0L, EPOCH, 0L);
        }

        /**
         * @return length of this file object
         */
        public long getLength() {
            if (length == -1)
                return length = file.length();
            return length;
        }

        /**
         * @return the filename
         */
        public String getName() {
            return file.getName();
        }

        /**
         * @return the file the attributes apply to
         */
        public File getFile() {
            return file;
        }

        boolean exists() {
            return exists;
        }
    }

    /**
     * Get the file attributes we care for.
     *
     * @param path
     *            a {@link java.io.File} object.
     * @return the file attributes we care for.
     * @since 3.3
     */
    public Attributes getAttributes(File path) {
        boolean isDirectory = isDirectory(path);
        boolean isFile = !isDirectory && path.isFile();
        assert path.exists() == isDirectory || isFile;
        boolean exists = isDirectory || isFile;
        boolean canExecute = exists && !isDirectory && canExecute(path);
        boolean isSymlink = false;
        Instant lastModified = exists ? lastModifiedInstant(path) : EPOCH;
        long createTime = 0L;
        return new Attributes(this, path, exists, isDirectory, canExecute, isSymlink, isFile, createTime,
                lastModified, -1);
    }

    /**
     * Normalize the unicode path to composed form.
     *
     * @param file
     *            a {@link java.io.File} object.
     * @return NFC-format File
     * @since 3.3
     */
    public File normalize(File file) {
        return file;
    }

    /**
     * Normalize the unicode path to composed form.
     *
     * @param name
     *            path name
     * @return NFC-format string
     * @since 3.3
     */
    public String normalize(String name) {
        return name;
    }

    /**
     * This runnable will consume an input stream's content into an output
     * stream as soon as it gets available.
     * <p>
     * Typically used to empty processes' standard output and error, preventing
     * them to choke.
     * </p>
     * <p>
     * <b>Note</b> that a {@link StreamGobbler} will never close either of its
     * streams.
     * </p>
     */
    private static class StreamGobbler implements Runnable {
        private InputStream in;

        private OutputStream out;

        public StreamGobbler(InputStream stream, OutputStream output) {
            this.in = stream;
            this.out = output;
        }

        @Override
        public void run() {
            try {
                copy();
            } catch (IOException e) {
                // Do nothing on read failure; leave streams open.
            }
        }

        void copy() throws IOException {
            boolean writeFailure = false;
            byte buffer[] = new byte[4096];
            int readBytes;
            while ((readBytes = in.read(buffer)) != -1) {
                // Do not try to write again after a failure, but keep
                // reading as long as possible to prevent the input stream
                // from choking.
                if (!writeFailure && out != null) {
                    try {
                        out.write(buffer, 0, readBytes);
                        out.flush();
                    } catch (IOException e) {
                        writeFailure = true;
                    }
                }
            }
        }
    }
}