Merge branch 'stable-5.4'

* stable-5.4: (82 commits)
  Export all packages of o.e.j.ant and o.e.j.archive bundles
  Do not require test bundles to export all packages
  Fix API problem filters
  Increase severity of AmbiguousMethodReference to ERROR
  [error prone] suppress AmbiguousMethodReference in AnyLongObjectId
  [error prone] fix ReferenceEquality warning in CommitBuilder
  [error prone] suppress NonAtomicVolatileUpdate warning in SimpleLruCache
  [error prone] fix ReferenceEquality warning in CommitGraphPane#authorFor
  [error prone] fix ReferenceEquality warning in RevWalk#isMergedInto
  [error prone] fix ReferenceEquality warning in RefUpdate#updateImpl
  [error prone] fix ReferenceEquality warning in static equals methods
  [error prone] suppress AmbiguousMethodReference in AnyObjectId
  [error prone] fix "FutureReturnValueIgnored" error in FS
  Fix formatting and add missing braces in Repository#stripWorkDir
  Repository: fix reference comparison of Files
  MergeAlgorithm: Suppress Error Prone warning about reference equality
  Fix NarrowingCompoundAssignment warnings from Error Prone
  FS_POSIX: handle Files.getFileStore() failures
  Fix OpenSshConfigTest#config
  FileSnapshot: fix bug with timestamp thresholding
  In LockFile#waitForStatChange wait in units of file time resolution
  Cache FileStoreAttributeCache per directory
  Fix FileSnapshot#save(long) and FileSnapshot#save(Instant)
  Persist minimal racy threshold and allow manual configuration
  Measure minimum racy interval to auto-configure FileSnapshot
  Reuse FileUtils to recursively delete files created by tests
  Fix FileAttributeCache.toString()
  Add test for racy git detection in FileSnapshot
  Repeat RefDirectoryTest.testGetRef_DiscoversModifiedLoose 100 times
  Fix org.eclipse.jdt.core.prefs of org.eclipse.jgit.junit
  Add missing javadoc in org.eclipse.jgit.junit
  Enhance RepeatRule to report number of failures at the end
  Fix FileSnapshotTests for filesystem with high timestamp resolution
  Retry deleting test files in FileBasedConfigTest
  Measure filesystem timestamp resolution already in test setup
  Refactor FileSnapshotTest to use NIO APIs
  Measure stored timestamp resolution instead of time to touch file
  Handle CancellationException in FileStoreAttributeCache
  Fix FileSnapshot#saveNoConfig
  Use Instant for smudge time in DirCache and DirCacheEntry
  Use Instant instead of milliseconds for filesystem timestamp handling
  Workaround SecurityException in FS#getFsTimestampResolution
  Fix NPE in FS$FileStoreAttributeCache.getFsTimestampResolution
  FS: ignore AccessDeniedException when measuring timestamp resolution
  Add debug trace for FileSnapshot
  Use FileChannel.open to touch file and set mtime to now
  Persist filesystem timestamp resolution and allow manual configuration
  Increase bazel timeout for long running tests
  Bazel: Fix lint warning flagged by buildifier
  Update bazlets to latest version
  Bazel: Add missing dependencies for ArchiveCommandTest
  Bazel: Remove FileTreeIteratorWithTimeControl from BUILD file
  Add support for nanoseconds and microseconds for Config#getTimeUnit
  Optionally measure filesystem timestamp resolution asynchronously
  Delete unused FileTreeIteratorWithTimeControl
  FileSnapshot#equals: consider UNKNOWN_SIZE
  Timeout measuring file timestamp resolution after 2 seconds
  Fix RacyGitTests#testRacyGitDetection
  GlobalBundleCache: Fix ClassNewInstance warning from Error Prone
  IncorrectObjectTypeException: Fix typos in constructors' Javadoc
  Change RacyGitTests to create a racy git situation in a stable way
  Deprecate Constants.CHARACTER_ENCODING in favor of StandardCharsets.UTF_8
  Fix non-deterministic hash of archives created by ArchiveCommand
  Update Maven plugins ecj, plexus, error-prone
  Update Maven plugins and cleanup Maven warnings
  Make inner classes static where possible
  Error Prone: Increase severity of NonOverridingEquals to ERROR
  Error Prone: Increase severity of ImmutableEnumChecker to ERROR
  GitDateParser#ParseableSimpleDateFormat: Make formatStr private final
  BatchRefUpdateTest: Suppress ImmutableEnumChecker warning
  PacketLineIn: Suppress comparison warnings for END and DELIM
  FileSnapshot#toString: Suppress ReferenceEquality warnings
  Blame: Suppress ReferenceEquality warning for RevCommit instances
  Fix API problem filters
  pgm: add missing optional dependency to org.tukaani:xz
  NetscapeCookieFile: Make hash static and group overloaded write
  NetscapeCookieFile: Javadoc fixes
  Config: Handle reference-equality warning (and empty javadoc)
  Error Prone: Increase severity of ShortCircuitBoolean to ERROR
  ObjectWalk: Prefer boolean operators over logical operators in comparisons
  BasePackFetchConnection: Prefer boolean operators over logical operators in comparisons
  PackWriter: Prefer boolean operators over logical operators in comparisons

Change-Id: I825fd55bcb5345fb7afe066bf54ca50325f40acb
Signed-off-by: David Pursehouse <david.pursehouse@gmail.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
diff --git a/lib/BUILD b/lib/BUILD
index f41ad16..932d7df 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -12,6 +12,7 @@
     visibility = [
         "//org.eclipse.jgit.archive:__pkg__",
         "//org.eclipse.jgit.pgm.test:__pkg__",
+        "//org.eclipse.jgit.test:__pkg__",
     ],
     exports = ["@commons-compress//jar"],
 )
diff --git a/org.eclipse.jgit.ant.test/src/org/eclipse/jgit/ant/tasks/GitCloneTaskTest.java b/org.eclipse.jgit.ant.test/src/org/eclipse/jgit/ant/tasks/GitCloneTaskTest.java
index 3ce0663..8043d2b 100644
--- a/org.eclipse.jgit.ant.test/src/org/eclipse/jgit/ant/tasks/GitCloneTaskTest.java
+++ b/org.eclipse.jgit.ant.test/src/org/eclipse/jgit/ant/tasks/GitCloneTaskTest.java
@@ -65,12 +65,13 @@
 
 	@Before
 	public void before() throws IOException {
+		dest = createTempFile();
+		FS.getFileStoreAttributes(dest.toPath().getParent());
 		project = new Project();
 		project.init();
 		enableLogging();
 		project.addTaskDefinition("git-clone", GitCloneTask.class);
 		task = (GitCloneTask) project.createTask("git-clone");
-		dest = createTempFile();
 		task.setDest(dest);
 	}
 
diff --git a/org.eclipse.jgit.ant/META-INF/MANIFEST.MF b/org.eclipse.jgit.ant/META-INF/MANIFEST.MF
index 8dd0daa..41a0422 100644
--- a/org.eclipse.jgit.ant/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ant/META-INF/MANIFEST.MF
@@ -9,5 +9,7 @@
   org.eclipse.jgit.storage.file;version="[5.5.0,5.6.0)"
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.ant,
- org.eclipse.jgit.ant.tasks;version="5.5.0";uses:="org.apache.tools.ant.types,org.apache.tools.ant"
+Export-Package: org.eclipse.jgit.ant;version="5.5.0",
+ org.eclipse.jgit.ant.tasks;version="5.5.0";
+  uses:="org.apache.tools.ant,
+   org.apache.tools.ant.types"
diff --git a/org.eclipse.jgit.archive/META-INF/MANIFEST.MF b/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
index 23e32fc..965c798 100644
--- a/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
@@ -26,4 +26,4 @@
    org.eclipse.jgit.api,
    org.apache.commons.compress.archivers,
    org.osgi.framework",
- org.eclipse.jgit.archive.internal;x-internal:=true
+ org.eclipse.jgit.archive.internal;version="5.5.0";x-internal:=true
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java
index 05510a0..946fb15 100644
--- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java
+++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java
@@ -58,12 +58,14 @@
 import java.io.OutputStream;
 import java.io.RandomAccessFile;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.Enumeration;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.util.FS;
 
 /**
  * Dumps a file over HTTP GET (or its information via HEAD).
@@ -76,7 +78,7 @@
 
 	private final RandomAccessFile source;
 
-	private final long lastModified;
+	private final Instant lastModified;
 
 	private final long fileLen;
 
@@ -89,7 +91,7 @@
 		this.source = new RandomAccessFile(path, "r");
 
 		try {
-			this.lastModified = path.lastModified();
+			this.lastModified = FS.DETECTED.lastModifiedInstant(path);
 			this.fileLen = source.getChannel().size();
 			this.end = fileLen;
 		} catch (IOException e) {
@@ -114,7 +116,7 @@
 		}
 	}
 
-	long getLastModified() {
+	Instant getLastModified() {
 		return lastModified;
 	}
 
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ObjectFileServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ObjectFileServlet.java
index 62f075c..5a27be6 100644
--- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ObjectFileServlet.java
+++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ObjectFileServlet.java
@@ -54,6 +54,7 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.time.Instant;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
@@ -76,7 +77,9 @@
 
 		@Override
 		String etag(FileSender sender) throws IOException {
-			return Long.toHexString(sender.getLastModified());
+			Instant lastModified = sender.getLastModified();
+			return Long.toHexString(lastModified.getEpochSecond())
+					+ Long.toHexString(lastModified.getNano());
 		}
 	}
 
@@ -145,7 +148,9 @@
 
 		try {
 			final String etag = etag(sender);
-			final long lastModified = (sender.getLastModified() / 1000) * 1000;
+			// HTTP header Last-Modified header has a resolution of 1 sec, see
+			// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.29
+			final long lastModified = sender.getLastModified().getEpochSecond();
 
 			String ifNoneMatch = req.getHeader(HDR_IF_NONE_MATCH);
 			if (etag != null && etag.equals(ifNoneMatch)) {
diff --git a/org.eclipse.jgit.junit/.settings/org.eclipse.jdt.core.prefs b/org.eclipse.jgit.junit/.settings/org.eclipse.jdt.core.prefs
index 2ca78ff..b675029 100644
--- a/org.eclipse.jgit.junit/.settings/org.eclipse.jdt.core.prefs
+++ b/org.eclipse.jgit.junit/.settings/org.eclipse.jdt.core.prefs
@@ -1,10 +1,13 @@
 eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
+org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=enabled
 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
-org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
-org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
-org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
-org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jgit.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnull.secondary=
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jgit.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jgit.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
@@ -14,6 +17,7 @@
 org.eclipse.jdt.core.compiler.debug.localVariable=generate
 org.eclipse.jdt.core.compiler.debug.sourceFile=generate
 org.eclipse.jdt.core.compiler.doc.comment.support=enabled
+org.eclipse.jdt.core.compiler.problem.APILeak=warning
 org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
 org.eclipse.jdt.core.compiler.problem.autoboxing=warning
@@ -48,7 +52,7 @@
 org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
 org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
 org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=error
-org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore
+org.eclipse.jdt.core.compiler.problem.missingJavadocComments=error
 org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled
 org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=protected
 org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=return_tag
@@ -64,14 +68,16 @@
 org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=error
 org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
 org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning
+org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning
 org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
 org.eclipse.jdt.core.compiler.problem.nullReference=error
 org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
-org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
 org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
 org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning
 org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=error
-org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=error
 org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
 org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore
 org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
@@ -86,16 +92,22 @@
 org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
 org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled
 org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning
 org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
 org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
 org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
 org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
 org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=warning
 org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled
+org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info
 org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
 org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=error
 org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
-org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=error
+org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
index f93424c..98e1f82 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
@@ -51,6 +51,8 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.PrintStream;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -126,6 +128,10 @@
 		if (!tmp.delete() || !tmp.mkdir())
 			throw new IOException("Cannot create " + tmp);
 
+		// measure timer resolution before the test to avoid time critical tests
+		// are affected by time needed for measurement
+		FS.getFileStoreAttributes(tmp.toPath().getParent());
+
 		mockSystemReader = new MockSystemReader();
 		mockSystemReader.userGitConfig = new FileBasedConfig(new File(tmp,
 				"usergitconfig"), FS.DETECTED);
@@ -232,36 +238,30 @@
 	private static boolean recursiveDelete(final File dir,
 			boolean silent, boolean failOnError) {
 		assert !(silent && failOnError);
-		if (!dir.exists())
-			return silent;
-		final File[] ls = dir.listFiles();
-		if (ls != null) {
-			for (File f : ls) {
-				if (f.isDirectory()) {
-					silent = recursiveDelete(f, silent, failOnError);
-				} else if (!f.delete()) {
-					if (!silent) {
-						reportDeleteFailure(failOnError, f);
-					}
-					silent = !failOnError;
-				}
-			}
+		int options = FileUtils.RECURSIVE | FileUtils.RETRY
+				| FileUtils.SKIP_MISSING;
+		if (silent) {
+			options |= FileUtils.IGNORE_ERRORS;
 		}
-		if (!dir.delete()) {
-			if (!silent)
-				reportDeleteFailure(failOnError, dir);
-			silent = !failOnError;
+		try {
+			FileUtils.delete(dir, options);
+		} catch (IOException e) {
+			reportDeleteFailure(failOnError, dir, e);
+			return !failOnError;
 		}
-		return silent;
+		return true;
 	}
 
-	private static void reportDeleteFailure(boolean failOnError, File e) {
+	private static void reportDeleteFailure(boolean failOnError, File f,
+			Exception cause) {
 		String severity = failOnError ? "ERROR" : "WARNING";
-		String msg = severity + ": Failed to delete " + e;
-		if (failOnError)
+		String msg = severity + ": Failed to delete " + f;
+		if (failOnError) {
 			fail(msg);
-		else
+		} else {
 			System.err.println(msg);
+		}
+		cause.printStackTrace(new PrintStream(System.err));
 	}
 
 	/** Constant <code>MOD_TIME=1</code> */
@@ -323,12 +323,13 @@
 			throws IllegalStateException, IOException {
 		DirCache dc = repo.readDirCache();
 		StringBuilder sb = new StringBuilder();
-		TreeSet<Long> timeStamps = new TreeSet<>();
+		TreeSet<Instant> timeStamps = new TreeSet<>();
 
 		// iterate once over the dircache just to collect all time stamps
 		if (0 != (includedOptions & MOD_TIME)) {
-			for (int i=0; i<dc.getEntryCount(); ++i)
-				timeStamps.add(Long.valueOf(dc.getEntry(i).getLastModified()));
+			for (int i = 0; i < dc.getEntryCount(); ++i) {
+				timeStamps.add(dc.getEntry(i).getLastModifiedInstant());
+			}
 		}
 
 		// iterate again, now produce the result string
@@ -340,7 +341,8 @@
 				sb.append(", stage:" + stage);
 			if (0 != (includedOptions & MOD_TIME)) {
 				sb.append(", time:t"+
-						timeStamps.headSet(Long.valueOf(entry.getLastModified())).size());
+						timeStamps.headSet(entry.getLastModifiedInstant())
+								.size());
 			}
 			if (0 != (includedOptions & SMUDGE))
 				if (entry.isSmudged())
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java
index 08220ce..94df554 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java
@@ -56,4 +56,13 @@
 	 * Number of repetitions
 	 */
 	public abstract int n();
+
+	/**
+	 * Whether to abort execution on first test failure
+	 *
+	 * @return {@code true} if execution should be aborted on the first failure,
+	 *         otherwise count failures and continue execution
+	 * @since 5.1.9
+	 */
+	public boolean abortOnFailure() default true;
 }
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java
index 8165738..8636f2a 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java
@@ -81,9 +81,31 @@
 	private static Logger LOG = Logger
 			.getLogger(RepeatRule.class.getName());
 
+	/**
+	 * Exception thrown if repeated execution of a test annotated with
+	 * {@code @Repeat} failed.
+	 */
 	public static class RepeatedTestException extends RuntimeException {
 		private static final long serialVersionUID = 1L;
 
+		/**
+		 * Constructor
+		 *
+		 * @param message
+		 *            the error message
+		 */
+		public RepeatedTestException(String message) {
+			super(message);
+		}
+
+		/**
+		 * Constructor
+		 *
+		 * @param message
+		 *            the error message
+		 * @param cause
+		 *            exception causing this exception
+		 */
 		public RepeatedTestException(String message, Throwable cause) {
 			super(message, cause);
 		}
@@ -93,28 +115,45 @@
 
 		private final int repetitions;
 
+		private boolean abortOnFailure;
+
 		private final Statement statement;
 
-		private RepeatStatement(int repetitions, Statement statement) {
+		private RepeatStatement(int repetitions, boolean abortOnFailure,
+				Statement statement) {
 			this.repetitions = repetitions;
+			this.abortOnFailure = abortOnFailure;
 			this.statement = statement;
 		}
 
 		@Override
 		public void evaluate() throws Throwable {
+			int failures = 0;
 			for (int i = 0; i < repetitions; i++) {
 				try {
 					statement.evaluate();
 				} catch (Throwable e) {
+					failures += 1;
 					RepeatedTestException ex = new RepeatedTestException(
 							MessageFormat.format(
 									"Repeated test failed when run for the {0}. time",
 									Integer.valueOf(i + 1)),
 							e);
 					LOG.log(Level.SEVERE, ex.getMessage(), ex);
-					throw ex;
+					if (abortOnFailure) {
+						throw ex;
+					}
 				}
 			}
+			if (failures > 0) {
+				RepeatedTestException e = new RepeatedTestException(
+						MessageFormat.format(
+								"Test failed {0} times out of {1} repeated executions",
+								Integer.valueOf(failures),
+								Integer.valueOf(repetitions)));
+				LOG.log(Level.SEVERE, e.getMessage(), e);
+				throw e;
+			}
 		}
 	}
 
@@ -125,7 +164,8 @@
 		Repeat repeat = description.getAnnotation(Repeat.class);
 		if (repeat != null) {
 			int n = repeat.n();
-			result = new RepeatStatement(n, statement);
+			boolean abortOnFailure = repeat.abortOnFailure();
+			result = new RepeatStatement(n, abortOnFailure, statement);
 		}
 		return result;
 	}
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
index a5270ed..23f49a4 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
@@ -57,7 +57,9 @@
 import java.io.InputStreamReader;
 import java.io.Reader;
 import java.nio.file.Path;
+import java.time.Instant;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -284,7 +286,7 @@
 
 				dce = new DirCacheEntry(treeItr.getEntryPathString());
 				dce.setFileMode(treeItr.getEntryFileMode());
-				dce.setLastModified(treeItr.getEntryLastModified());
+				dce.setLastModified(treeItr.getEntryLastModifiedInstant());
 				dce.setLength((int) len);
 				try (FileInputStream in = new FileInputStream(
 						treeItr.getEntryFile())) {
@@ -361,7 +363,8 @@
 	 * @throws InterruptedException
 	 * @throws IOException
 	 */
-	public static long fsTick(File lastFile) throws InterruptedException,
+	public static Instant fsTick(File lastFile)
+			throws InterruptedException,
 			IOException {
 		File tmp;
 		FS fs = FS.DETECTED;
@@ -375,15 +378,16 @@
 			tmp = File.createTempFile("fsTickTmpFile", null,
 					lastFile.getParentFile());
 		}
-		long res = FS.getFsTimerResolution(tmp.toPath()).toMillis();
+		long res = FS.getFileStoreAttributes(tmp.toPath())
+				.getFsTimestampResolution().toNanos();
 		long sleepTime = res / 10;
 		try {
-			long startTime = fs.lastModified(lastFile);
-			long actTime = fs.lastModified(tmp);
-			while (actTime <= startTime) {
-				Thread.sleep(sleepTime);
+			Instant startTime = fs.lastModifiedInstant(lastFile);
+			Instant actTime = fs.lastModifiedInstant(tmp);
+			while (actTime.compareTo(startTime) <= 0) {
+				TimeUnit.NANOSECONDS.sleep(sleepTime);
 				FileUtils.touch(tmp.toPath());
-				actTime = fs.lastModified(tmp);
+				actTime = fs.lastModifiedInstant(tmp);
 			}
 			return actTime;
 		} finally {
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
index 356d9d8..e89cf0f 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
@@ -728,7 +728,7 @@
 		ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(db, true);
 		merger.setBase(parent.getTree());
 		if (merger.merge(head, commit)) {
-			if (AnyObjectId.equals(head.getTree(), merger.getResultTreeId()))
+			if (AnyObjectId.isEqual(head.getTree(), merger.getResultTreeId()))
 				return null;
 			tick(1);
 			org.eclipse.jgit.lib.CommitBuilder b =
@@ -1076,6 +1076,14 @@
 			parents.add(prior.create());
 		}
 
+		/**
+		 * set parent commit
+		 *
+		 * @param p
+		 *            parent commit
+		 * @return this commit builder
+		 * @throws Exception
+		 */
 		public CommitBuilder parent(RevCommit p) throws Exception {
 			if (parents.isEmpty()) {
 				DirCacheBuilder b = tree.builder();
@@ -1088,29 +1096,71 @@
 			return this;
 		}
 
+		/**
+		 * Get parent commits
+		 *
+		 * @return parent commits
+		 */
 		public List<RevCommit> parents() {
 			return Collections.unmodifiableList(parents);
 		}
 
+		/**
+		 * Remove parent commits
+		 *
+		 * @return this commit builder
+		 */
 		public CommitBuilder noParents() {
 			parents.clear();
 			return this;
 		}
 
+		/**
+		 * Remove files
+		 *
+		 * @return this commit builder
+		 */
 		public CommitBuilder noFiles() {
 			tree.clear();
 			return this;
 		}
 
+		/**
+		 * Set top level tree
+		 *
+		 * @param treeId
+		 *            the top level tree
+		 * @return this commit builder
+		 */
 		public CommitBuilder setTopLevelTree(ObjectId treeId) {
 			topLevelTree = treeId;
 			return this;
 		}
 
+		/**
+		 * Add file with given content
+		 *
+		 * @param path
+		 *            path of the file
+		 * @param content
+		 *            the file content
+		 * @return this commit builder
+		 * @throws Exception
+		 */
 		public CommitBuilder add(String path, String content) throws Exception {
 			return add(path, blob(content));
 		}
 
+		/**
+		 * Add file with given path and blob
+		 *
+		 * @param path
+		 *            path of the file
+		 * @param id
+		 *            blob for this file
+		 * @return this commit builder
+		 * @throws Exception
+		 */
 		public CommitBuilder add(String path, RevBlob id)
 				throws Exception {
 			return edit(new PathEdit(path) {
@@ -1122,6 +1172,13 @@
 			});
 		}
 
+		/**
+		 * Edit the index
+		 *
+		 * @param edit
+		 *            the index record update
+		 * @return this commit builder
+		 */
 		public CommitBuilder edit(PathEdit edit) {
 			DirCacheEditor e = tree.editor();
 			e.add(edit);
@@ -1129,6 +1186,13 @@
 			return this;
 		}
 
+		/**
+		 * Remove a file
+		 *
+		 * @param path
+		 *            path of the file
+		 * @return this commit builder
+		 */
 		public CommitBuilder rm(String path) {
 			DirCacheEditor e = tree.editor();
 			e.add(new DeletePath(path));
@@ -1137,49 +1201,111 @@
 			return this;
 		}
 
+		/**
+		 * Set commit message
+		 *
+		 * @param m
+		 *            the message
+		 * @return this commit builder
+		 */
 		public CommitBuilder message(String m) {
 			message = m;
 			return this;
 		}
 
+		/**
+		 * Get the commit message
+		 *
+		 * @return the commit message
+		 */
 		public String message() {
 			return message;
 		}
 
+		/**
+		 * Tick the clock
+		 *
+		 * @param secs
+		 *            number of seconds
+		 * @return this commit builder
+		 */
 		public CommitBuilder tick(int secs) {
 			tick = secs;
 			return this;
 		}
 
+		/**
+		 * Set author and committer identity
+		 *
+		 * @param ident
+		 *            identity to set
+		 * @return this commit builder
+		 */
 		public CommitBuilder ident(PersonIdent ident) {
 			author = ident;
 			committer = ident;
 			return this;
 		}
 
+		/**
+		 * Set the author identity
+		 *
+		 * @param a
+		 *            the author's identity
+		 * @return this commit builder
+		 */
 		public CommitBuilder author(PersonIdent a) {
 			author = a;
 			return this;
 		}
 
+		/**
+		 * Get the author identity
+		 *
+		 * @return the author identity
+		 */
 		public PersonIdent author() {
 			return author;
 		}
 
+		/**
+		 * Set the committer identity
+		 *
+		 * @param c
+		 *            the committer identity
+		 * @return this commit builder
+		 */
 		public CommitBuilder committer(PersonIdent c) {
 			committer = c;
 			return this;
 		}
 
+		/**
+		 * Get the committer identity
+		 *
+		 * @return the committer identity
+		 */
 		public PersonIdent committer() {
 			return committer;
 		}
 
+		/**
+		 * Insert changeId
+		 *
+		 * @return this commit builder
+		 */
 		public CommitBuilder insertChangeId() {
 			changeId = "";
 			return this;
 		}
 
+		/**
+		 * Insert given changeId
+		 *
+		 * @param c
+		 *            changeId
+		 * @return this commit builder
+		 */
 		public CommitBuilder insertChangeId(String c) {
 			// Validate, but store as a string so we can use "" as a sentinel.
 			ObjectId.fromString(c);
@@ -1187,6 +1313,13 @@
 			return this;
 		}
 
+		/**
+		 * Create the commit
+		 *
+		 * @return the new commit
+		 * @throws Exception
+		 *             if creation failed
+		 */
 		public RevCommit create() throws Exception {
 			if (self == null) {
 				TestRepository.this.tick(tick);
@@ -1247,6 +1380,12 @@
 						+ cid.getName() + "\n"); //$NON-NLS-1$
 		}
 
+		/**
+		 * Create child commit builder
+		 *
+		 * @return child commit builder
+		 * @throws Exception
+		 */
 		public CommitBuilder child() throws Exception {
 			return new CommitBuilder(this);
 		}
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java
new file mode 100644
index 0000000..1f8070d
--- /dev/null
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com>
+ * 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.junit.time;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
+
+import org.eclipse.jgit.util.FS;
+
+/**
+ * Utility methods for handling timestamps
+ */
+public class TimeUtil {
+	/**
+	 * Set the lastModified time of a given file by adding a given offset to the
+	 * current lastModified time
+	 *
+	 * @param path
+	 *            path of a file to set last modified
+	 * @param offsetMillis
+	 *            offset in milliseconds, if negative the new lastModified time
+	 *            is offset before the original lastModified time, otherwise
+	 *            after the original time
+	 * @return the new lastModified time
+	 */
+	public static Instant setLastModifiedWithOffset(Path path,
+			long offsetMillis) {
+		Instant mTime = FS.DETECTED.lastModifiedInstant(path)
+				.plusMillis(offsetMillis);
+		try {
+			Files.setLastModifiedTime(path, FileTime.from(mTime));
+			return mTime;
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+	}
+
+	/**
+	 * Set the lastModified time of file a to the one from file b
+	 *
+	 * @param a
+	 *            file to set lastModified time
+	 * @param b
+	 *            file to read lastModified time from
+	 */
+	public static void setLastModifiedOf(Path a, Path b) {
+		Instant mTime = FS.DETECTED.lastModifiedInstant(b);
+		try {
+			Files.setLastModifiedTime(a, FileTime.from(mTime));
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+	}
+
+}
diff --git a/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/LfsServerTest.java b/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/LfsServerTest.java
index 10823b8..ec44da4 100644
--- a/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/LfsServerTest.java
+++ b/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/LfsServerTest.java
@@ -82,6 +82,7 @@
 import org.eclipse.jgit.lfs.server.LargeFileRepository;
 import org.eclipse.jgit.lfs.server.LfsProtocolServlet;
 import org.eclipse.jgit.lfs.test.LongObjectIdTestUtils;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.IO;
 import org.junit.After;
@@ -119,6 +120,11 @@
 	@Before
 	public void setup() throws Exception {
 		tmp = Files.createTempDirectory("jgit_test_");
+
+		// measure timer resolution before the test to avoid time critical tests
+		// are affected by time needed for measurement
+		FS.getFileStoreAttributes(tmp.getParent());
+
 		server = new AppServer();
 		ServletContextHandler app = server.addContext("/lfs");
 		dir = Paths.get(tmp.toString(), "lfs");
diff --git a/org.eclipse.jgit.lfs/.settings/.api_filters b/org.eclipse.jgit.lfs/.settings/.api_filters
new file mode 100644
index 0000000..9747df8
--- /dev/null
+++ b/org.eclipse.jgit.lfs/.settings/.api_filters
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<component id="org.eclipse.jgit.lfs" version="2">
+    <resource path="src/org/eclipse/jgit/lfs/lib/AnyLongObjectId.java" type="org.eclipse.jgit.lfs.lib.AnyLongObjectId">
+        <filter id="1141899266">
+            <message_arguments>
+                <message_argument value="5.4"/>
+                <message_argument value="5.5"/>
+                <message_argument value="isEqual(AnyLongObjectId, AnyLongObjectId)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+</component>
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/AnyLongObjectId.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/AnyLongObjectId.java
index 0788922..b095d20 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/AnyLongObjectId.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/AnyLongObjectId.java
@@ -50,6 +50,7 @@
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.util.NB;
+import org.eclipse.jgit.util.References;
 
 /**
  * A (possibly mutable) SHA-256 abstraction.
@@ -73,11 +74,31 @@
 	 * @param secondObjectId
 	 *            the second identifier to compare. Must not be null.
 	 * @return true if the two identifiers are the same.
+	 * @deprecated use {@link #isEqual(AnyLongObjectId, AnyLongObjectId)}
+	 *             instead.
 	 */
+	@Deprecated
+	@SuppressWarnings("AmbiguousMethodReference")
 	public static boolean equals(final AnyLongObjectId firstObjectId,
 			final AnyLongObjectId secondObjectId) {
-		if (firstObjectId == secondObjectId)
+		return isEqual(firstObjectId, secondObjectId);
+	}
+
+	/**
+	 * Compare two object identifier byte sequences for equality.
+	 *
+	 * @param firstObjectId
+	 *            the first identifier to compare. Must not be null.
+	 * @param secondObjectId
+	 *            the second identifier to compare. Must not be null.
+	 * @return true if the two identifiers are the same.
+	 * @since 5.4
+	 */
+	public static boolean isEqual(final AnyLongObjectId firstObjectId,
+			final AnyLongObjectId secondObjectId) {
+		if (References.isSameObject(firstObjectId, secondObjectId)) {
 			return true;
+		}
 
 		// We test word 2 first as odds are someone already used our
 		// word 1 as a hash code, and applying that came up with these
@@ -274,6 +295,7 @@
 	 *            the other id to compare to. May be null.
 	 * @return true only if both LongObjectIds have identical bits.
 	 */
+	@SuppressWarnings({ "NonOverridingEquals", "AmbiguousMethodReference" })
 	public final boolean equals(AnyLongObjectId other) {
 		return other != null ? equals(this, other) : false;
 	}
diff --git a/org.eclipse.jgit.packaging/pom.xml b/org.eclipse.jgit.packaging/pom.xml
index bbb0f90..1e17c9d 100644
--- a/org.eclipse.jgit.packaging/pom.xml
+++ b/org.eclipse.jgit.packaging/pom.xml
@@ -47,10 +47,6 @@
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
-  <prerequisites>
-    <maven>3.0</maven>
-  </prerequisites>
-
   <groupId>org.eclipse.jgit</groupId>
   <artifactId>jgit.tycho.parent</artifactId>
   <version>5.5.0-SNAPSHOT</version>
@@ -290,6 +286,21 @@
           <version>${tycho-version}</version>
         </plugin>
         <plugin>
+          <groupId>org.eclipse.tycho</groupId>
+          <artifactId>tycho-p2-publisher-plugin</artifactId>
+          <version>${tycho-version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.eclipse.tycho</groupId>
+          <artifactId>tycho-p2-repository-plugin</artifactId>
+          <version>${tycho-version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.eclipse.tycho</groupId>
+          <artifactId>tycho-packaging-plugin</artifactId>
+          <version>${tycho-version}</version>
+        </plugin>
+        <plugin>
           <groupId>org.eclipse.tycho.extras</groupId>
           <artifactId>tycho-pack200a-plugin</artifactId>
           <version>${tycho-extras-version}</version>
@@ -309,6 +320,25 @@
           <artifactId>build-helper-maven-plugin</artifactId>
           <version>3.0.0</version>
         </plugin>
+        <plugin>
+          <artifactId>maven-clean-plugin</artifactId>
+          <version>3.1.0</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-deploy-plugin</artifactId>
+          <version>3.0.0-M1</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-install-plugin</artifactId>
+          <version>3.0.0-M1</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-site-plugin</artifactId>
+          <version>3.7.1</version>
+        </plugin>
       </plugins>
     </pluginManagement>
   </build>
diff --git a/org.eclipse.jgit.pgm/pom.xml b/org.eclipse.jgit.pgm/pom.xml
index 12ccadd..c6af997 100644
--- a/org.eclipse.jgit.pgm/pom.xml
+++ b/org.eclipse.jgit.pgm/pom.xml
@@ -142,6 +142,12 @@
       <artifactId>org.eclipse.jgit.lfs.server</artifactId>
       <version>${project.version}</version>
     </dependency>
+
+    <dependency>
+      <groupId>org.tukaani</groupId>
+      <artifactId>xz</artifactId>
+      <optional>true</optional>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java
index 8794ca6..e38cb46 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java
@@ -218,7 +218,8 @@
 					dateWidth = Math.max(dateWidth, date(line).length());
 					pathWidth = Math.max(pathWidth, path(line).length());
 				}
-				while (line + 1 < end && blame.getSourceCommit(line + 1) == c) {
+				while (line + 1 < end
+						&& sameCommit(blame.getSourceCommit(line + 1), c)) {
 					line++;
 				}
 				maxSourceLine = Math.max(maxSourceLine, blame.getSourceLine(line));
@@ -257,13 +258,22 @@
 					blame.getResultContents().writeLine(outs, line);
 					outs.flush();
 					outw.print('\n');
-				} while (++line < end && blame.getSourceCommit(line) == c);
+				} while (++line < end
+						&& sameCommit(blame.getSourceCommit(line), c));
 			}
 		} catch (NoWorkTreeException | IOException e) {
 			throw die(e.getMessage(), e);
 		}
 	}
 
+	@SuppressWarnings("ReferenceEquality")
+	private static boolean sameCommit(RevCommit a, RevCommit b) {
+		// Reference comparison is intentional; BlameGenerator uses a single
+		// RevWalk which caches the RevCommit objects, and if a given commit
+		// is cached the RevWalk returns the same instance.
+		return a == b;
+	}
+
 	private int uniqueAbbrevLen(ObjectReader reader, RevCommit commit)
 			throws IOException {
 		return reader.abbreviate(commit, abbrev).length();
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
index 05f2378..d81a3ae 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
@@ -45,8 +45,8 @@
 package org.eclipse.jgit.pgm;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_SECTION_I18N;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_LOG_OUTPUT_ENCODING;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_SECTION_I18N;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 import static org.eclipse.jgit.lib.Constants.R_REMOTES;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowDirCache.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowDirCache.java
index 7f99d76..14a60a3 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowDirCache.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowDirCache.java
@@ -48,8 +48,10 @@
 
 import static java.lang.Integer.valueOf;
 
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheEntry;
@@ -67,25 +69,27 @@
 	/** {@inheritDoc} */
 	@Override
 	protected void run() throws Exception {
-		final SimpleDateFormat fmt;
-		fmt = new SimpleDateFormat("yyyy-MM-dd,HH:mm:ss.SSS"); //$NON-NLS-1$
+		final DateTimeFormatter fmt = DateTimeFormatter
+				.ofPattern("yyyy-MM-dd,HH:mm:ss.nnnnnnnnn") //$NON-NLS-1$
+				.withLocale(Locale.getDefault())
+				.withZone(ZoneId.systemDefault());
 
 		final DirCache cache = db.readDirCache();
 		for (int i = 0; i < cache.getEntryCount(); i++) {
 			final DirCacheEntry ent = cache.getEntry(i);
 			final FileMode mode = FileMode.fromBits(ent.getRawMode());
 			final int len = ent.getLength();
-			long lastModified = ent.getLastModified();
-			final Date mtime = new Date(lastModified);
+			Instant mtime = ent.getLastModifiedInstant();
 			final int stage = ent.getStage();
 
 			outw.print(mode);
 			outw.format(" %6d", valueOf(len)); //$NON-NLS-1$
 			outw.print(' ');
-			if (millis)
-				outw.print(lastModified);
-			else
+			if (millis) {
+				outw.print(mtime.toEpochMilli());
+			} else {
 				outw.print(fmt.format(mtime));
+			}
 			outw.print(' ');
 			outw.print(ent.getObjectId().name());
 			outw.print(' ');
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/VerifyReftable.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/VerifyReftable.java
index b38de14..15b6ff9 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/VerifyReftable.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/VerifyReftable.java
@@ -189,7 +189,7 @@
 			return;
 		}
 
-		if (!AnyObjectId.equals(exp.getObjectId(), act.getObjectId())) {
+		if (!AnyObjectId.isEqual(exp.getObjectId(), act.getObjectId())) {
 			throw die(String.format("expected %s to be %s, found %s",
 					exp.getName(),
 					id(exp.getObjectId()),
@@ -197,7 +197,8 @@
 		}
 
 		if (exp.getPeeledObjectId() != null
-				&& !AnyObjectId.equals(exp.getPeeledObjectId(), act.getPeeledObjectId())) {
+				&& !AnyObjectId.isEqual(exp.getPeeledObjectId(),
+						act.getPeeledObjectId())) {
 			throw die(String.format("expected %s to be %s, found %s",
 					exp.getName(),
 					id(exp.getPeeledObjectId()),
diff --git a/org.eclipse.jgit.test/BUILD b/org.eclipse.jgit.test/BUILD
index 1d03afe..fc8c53c 100644
--- a/org.eclipse.jgit.test/BUILD
+++ b/org.eclipse.jgit.test/BUILD
@@ -24,7 +24,6 @@
     "revwalk/RevWalkTestCase.java",
     "transport/ObjectIdMatcher.java",
     "transport/SpiTransport.java",
-    "treewalk/FileTreeIteratorWithTimeControl.java",
     "treewalk/filter/AlwaysCloneTreeFilter.java",
     "test/resources/SampleDataRepositoryTestCase.java",
     "util/CPUTimeStopWatch.java",
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 0f65c88..53b79b0 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -11,10 +11,17 @@
 Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
  com.jcraft.jsch;version="[0.1.54,0.2.0)",
  net.bytebuddy.dynamic.loading;version="[1.7.0,2.0.0)",
+ org.apache.commons.compress.archivers;version="[1.15.0,2.0)",
+ org.apache.commons.compress.archivers.tar;version="[1.15.0,2.0)",
+ org.apache.commons.compress.archivers.zip;version="[1.15.0,2.0)",
+ org.apache.commons.compress.compressors.bzip2;version="[1.15.0,2.0)",
+ org.apache.commons.compress.compressors.gzip;version="[1.15.0,2.0)",
+ org.apache.commons.compress.compressors.xz;version="[1.15.0,2.0)",
  org.bouncycastle.util.encoders;version="[1.61.0,2.0.0)",
  org.eclipse.jgit.annotations;version="[5.5.0,5.6.0)",
  org.eclipse.jgit.api;version="[5.5.0,5.6.0)",
  org.eclipse.jgit.api.errors;version="[5.5.0,5.6.0)",
+ org.eclipse.jgit.archive;version="[5.5.0,5.6.0)",
  org.eclipse.jgit.attributes;version="[5.5.0,5.6.0)",
  org.eclipse.jgit.awtui;version="[5.5.0,5.6.0)",
  org.eclipse.jgit.blame;version="[5.5.0,5.6.0)",
@@ -39,6 +46,7 @@
  org.eclipse.jgit.internal.transport.parser;version="[5.5.0,5.6.0)",
  org.eclipse.jgit.junit;version="[5.5.0,5.6.0)",
  org.eclipse.jgit.junit.ssh;version="[5.5.0,5.6.0)",
+ org.eclipse.jgit.junit.time;version="[5.5.0,5.6.0)",
  org.eclipse.jgit.lfs;version="[5.5.0,5.6.0)",
  org.eclipse.jgit.lib;version="[5.5.0,5.6.0)",
  org.eclipse.jgit.merge;version="[5.5.0,5.6.0)",
@@ -71,7 +79,8 @@
  org.mockito.junit;version="[2.13.0,3.0.0)",
  org.mockito.stubbing;version="[2.13.0,3.0.0)",
  org.objenesis;version="[2.6.0,3.0.0)",
- org.slf4j;version="[1.7.0,2.0.0)"
+ org.slf4j;version="[1.7.0,2.0.0)",
+ org.tukaani.xz;version="[1.6.0,2.0)"
 Require-Bundle: org.hamcrest.core;bundle-version="[1.1.0,2.0.0)",
  org.hamcrest.library;bundle-version="[1.1.0,2.0.0)"
 Export-Package: org.eclipse.jgit.transport.ssh;version="5.5.0";x-friends:="org.eclipse.jgit.ssh.apache.test"
diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml
index 99e6fe3..5e8a3e6 100644
--- a/org.eclipse.jgit.test/pom.xml
+++ b/org.eclipse.jgit.test/pom.xml
@@ -133,6 +133,12 @@
       <artifactId>org.eclipse.jgit.pgm</artifactId>
       <version>${project.version}</version>
     </dependency>
+
+    <dependency>
+      <groupId>org.tukaani</groupId>
+      <artifactId>xz</artifactId>
+      <optional>true</optional>
+    </dependency>
   </dependencies>
 
   <profiles>
@@ -144,6 +150,7 @@
           <plugin>
             <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-surefire-plugin</artifactId>
+            <version>${maven-surefire-plugin-version}</version>
             <configuration>
               <argLine>@{argLine} -Djgit.test.long=true</argLine>
             </configuration>
diff --git a/org.eclipse.jgit.test/tests.bzl b/org.eclipse.jgit.test/tests.bzl
index 17a45ca..f27efcc 100644
--- a/org.eclipse.jgit.test/tests.bzl
+++ b/org.eclipse.jgit.test/tests.bzl
@@ -58,6 +58,12 @@
             additional_deps = [
                 "//lib:mockito",
             ]
+        if src.endswith("ArchiveCommandTest.java"):
+            additional_deps = [
+                "//lib:commons-compress",
+                "//lib:xz",
+                "//org.eclipse.jgit.archive:jgit-archive",
+            ]
         heap_size = "-Xmx256m"
         if src.endswith("HugeCommitMessageTest.java"):
             heap_size = "-Xmx512m"
diff --git a/org.eclipse.jgit.test/tst-rsrc/log4j.properties b/org.eclipse.jgit.test/tst-rsrc/log4j.properties
index 14620ff..ee1ac35 100644
--- a/org.eclipse.jgit.test/tst-rsrc/log4j.properties
+++ b/org.eclipse.jgit.test/tst-rsrc/log4j.properties
@@ -7,3 +7,8 @@
 log4j.appender.stdout.Target=System.out
 log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
 log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
+#log4j.appender.fileLogger.bufferedIO = true
+#log4j.appender.fileLogger.bufferSize = 4096
+
+#log4j.logger.org.eclipse.jgit.util.FS = DEBUG
+#log4j.logger.org.eclipse.jgit.internal.storage.file.FileSnapshot = DEBUG
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
index 3fee51a..b28e26a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
@@ -1267,7 +1267,7 @@
 		DirCacheEntry entry = new DirCacheEntry(path, stage);
 		entry.setObjectId(id);
 		entry.setFileMode(FileMode.REGULAR_FILE);
-		entry.setLastModified(file.lastModified());
+		entry.setLastModified(FS.DETECTED.lastModifiedInstant(file));
 		entry.setLength((int) file.length());
 
 		builder.add(entry);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java
index 1c41018..0f2e6b8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java
@@ -47,20 +47,44 @@
 import static org.junit.Assert.assertNull;
 
 import java.beans.Statement;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.ArchiveInputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
+import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
+import org.eclipse.jgit.api.errors.AbortedByHookException;
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.NoFilepatternException;
+import org.eclipse.jgit.api.errors.NoHeadException;
+import org.eclipse.jgit.api.errors.NoMessageException;
+import org.eclipse.jgit.api.errors.UnmergedPathsException;
+import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
+import org.eclipse.jgit.archive.ArchiveFormats;
+import org.eclipse.jgit.errors.AmbiguousObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.StringUtils;
 import org.junit.After;
 import org.junit.Before;
@@ -68,9 +92,14 @@
 
 public class ArchiveCommandTest extends RepositoryTestCase {
 
+	// archives store timestamp with 1 second resolution
+	private static final int WAIT = 2000;
 	private static final String UNEXPECTED_ARCHIVE_SIZE  = "Unexpected archive size";
 	private static final String UNEXPECTED_FILE_CONTENTS = "Unexpected file contents";
 	private static final String UNEXPECTED_TREE_CONTENTS = "Unexpected tree contents";
+	private static final String UNEXPECTED_LAST_MODIFIED =
+			"Unexpected lastModified mocked by MockSystemReader, truncated to 1 second";
+	private static final String UNEXPECTED_DIFFERENT_HASH = "Unexpected different hash";
 
 	private MockFormat format = null;
 
@@ -78,25 +107,20 @@
 	public void setup() {
 		format = new MockFormat();
 		ArchiveCommand.registerFormat(format.SUFFIXES.get(0), format);
+		ArchiveFormats.registerAll();
 	}
 
 	@Override
 	@After
 	public void tearDown() {
 		ArchiveCommand.unregisterFormat(format.SUFFIXES.get(0));
+		ArchiveFormats.unregisterAll();
 	}
 
 	@Test
 	public void archiveHeadAllFiles() throws IOException, GitAPIException {
 		try (Git git = new Git(db)) {
-			writeTrashFile("file_1.txt", "content_1_1");
-			git.add().addFilepattern("file_1.txt").call();
-			git.commit().setMessage("create file").call();
-
-			writeTrashFile("file_1.txt", "content_1_2");
-			writeTrashFile("file_2.txt", "content_2_2");
-			git.add().addFilepattern(".").call();
-			git.commit().setMessage("updated file").call();
+			createTestContent(git);
 
 			git.archive().setOutputStream(new MockOutputStream())
 					.setFormat(format.SUFFIXES.get(0))
@@ -191,6 +215,157 @@
 		}
 	}
 
+	@Test
+	public void archiveHeadAllFilesTarTimestamps() throws Exception {
+		try (Git git = new Git(db)) {
+			createTestContent(git);
+			String fmt = "tar";
+			File archive = new File(getTemporaryDirectory(),
+					"archive." + format);
+			archive(git, archive, fmt);
+			ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive));
+
+			try (InputStream fi = Files.newInputStream(archive.toPath());
+					InputStream bi = new BufferedInputStream(fi);
+					ArchiveInputStream o = new TarArchiveInputStream(bi)) {
+				assertEntries(o);
+			}
+
+			Thread.sleep(WAIT);
+			archive(git, archive, fmt);
+			assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1,
+					ObjectId.fromRaw(IO.readFully(archive)));
+		}
+	}
+
+	@Test
+	public void archiveHeadAllFilesTgzTimestamps() throws Exception {
+		try (Git git = new Git(db)) {
+			createTestContent(git);
+			String fmt = "tgz";
+			File archive = new File(getTemporaryDirectory(),
+					"archive." + fmt);
+			archive(git, archive, fmt);
+			ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive));
+
+			try (InputStream fi = Files.newInputStream(archive.toPath());
+					InputStream bi = new BufferedInputStream(fi);
+					InputStream gzi = new GzipCompressorInputStream(bi);
+					ArchiveInputStream o = new TarArchiveInputStream(gzi)) {
+				assertEntries(o);
+			}
+
+			Thread.sleep(WAIT);
+			archive(git, archive, fmt);
+			assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1,
+					ObjectId.fromRaw(IO.readFully(archive)));
+		}
+	}
+
+	@Test
+	public void archiveHeadAllFilesTbz2Timestamps() throws Exception {
+		try (Git git = new Git(db)) {
+			createTestContent(git);
+			String fmt = "tbz2";
+			File archive = new File(getTemporaryDirectory(),
+					"archive." + fmt);
+			archive(git, archive, fmt);
+			ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive));
+
+			try (InputStream fi = Files.newInputStream(archive.toPath());
+					InputStream bi = new BufferedInputStream(fi);
+					InputStream gzi = new BZip2CompressorInputStream(bi);
+					ArchiveInputStream o = new TarArchiveInputStream(gzi)) {
+				assertEntries(o);
+			}
+
+			Thread.sleep(WAIT);
+			archive(git, archive, fmt);
+			assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1,
+					ObjectId.fromRaw(IO.readFully(archive)));
+		}
+	}
+
+	@Test
+	public void archiveHeadAllFilesTxzTimestamps() throws Exception {
+		try (Git git = new Git(db)) {
+			createTestContent(git);
+			String fmt = "txz";
+			File archive = new File(getTemporaryDirectory(), "archive." + fmt);
+			archive(git, archive, fmt);
+			ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive));
+
+			try (InputStream fi = Files.newInputStream(archive.toPath());
+					InputStream bi = new BufferedInputStream(fi);
+					InputStream gzi = new XZCompressorInputStream(bi);
+					ArchiveInputStream o = new TarArchiveInputStream(gzi)) {
+				assertEntries(o);
+			}
+
+			Thread.sleep(WAIT);
+			archive(git, archive, fmt);
+			assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1,
+					ObjectId.fromRaw(IO.readFully(archive)));
+		}
+	}
+
+	@Test
+	public void archiveHeadAllFilesZipTimestamps() throws Exception {
+		try (Git git = new Git(db)) {
+			createTestContent(git);
+			String fmt = "zip";
+			File archive = new File(getTemporaryDirectory(), "archive." + fmt);
+			archive(git, archive, fmt);
+			ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive));
+
+			try (InputStream fi = Files.newInputStream(archive.toPath());
+					InputStream bi = new BufferedInputStream(fi);
+					ArchiveInputStream o = new ZipArchiveInputStream(bi)) {
+				assertEntries(o);
+			}
+
+			Thread.sleep(WAIT);
+			archive(git, archive, fmt);
+			assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1,
+					ObjectId.fromRaw(IO.readFully(archive)));
+		}
+	}
+
+	private void createTestContent(Git git) throws IOException, GitAPIException,
+			NoFilepatternException, NoHeadException, NoMessageException,
+			UnmergedPathsException, ConcurrentRefUpdateException,
+			WrongRepositoryStateException, AbortedByHookException {
+		writeTrashFile("file_1.txt", "content_1_1");
+		git.add().addFilepattern("file_1.txt").call();
+		git.commit().setMessage("create file").call();
+
+		writeTrashFile("file_1.txt", "content_1_2");
+		writeTrashFile("file_2.txt", "content_2_2");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("updated file").call();
+	}
+
+	private static void archive(Git git, File archive, String fmt)
+			throws GitAPIException,
+			FileNotFoundException, AmbiguousObjectException,
+			IncorrectObjectTypeException, IOException {
+		git.archive().setOutputStream(new FileOutputStream(archive))
+				.setFormat(fmt)
+				.setTree(git.getRepository().resolve("HEAD")).call();
+	}
+
+	private static void assertEntries(ArchiveInputStream o) throws IOException {
+		ArchiveEntry e;
+		int n = 0;
+		while ((e = o.getNextEntry()) != null) {
+			n++;
+			assertEquals(UNEXPECTED_LAST_MODIFIED,
+					(1250379778668L / 1000L) * 1000L,
+					e.getLastModifiedDate().getTime());
+		}
+		assertEquals(UNEXPECTED_ARCHIVE_SIZE, 2, n);
+	}
+
 	private static class MockFormat
 			implements ArchiveCommand.Format<MockOutputStream> {
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java
index 98a8adc..1f3527f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java
@@ -43,6 +43,7 @@
  */
 package org.eclipse.jgit.api;
 
+import static java.time.Instant.EPOCH;
 import static org.eclipse.jgit.lib.Constants.MASTER;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 import static org.hamcrest.CoreMatchers.is;
@@ -60,6 +61,9 @@
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
 
 import org.eclipse.jgit.api.CheckoutResult.Status;
 import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode;
@@ -75,6 +79,7 @@
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.time.TimeUtil;
 import org.eclipse.jgit.lfs.BuiltinLFS;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
@@ -87,6 +92,7 @@
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.transport.RemoteConfig;
 import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.SystemReader;
 import org.junit.Before;
@@ -376,14 +382,14 @@
 
 		File file = new File(db.getWorkTree(), "Test.txt");
 		long size = file.length();
-		long mTime = file.lastModified() - 5000L;
-		assertTrue(file.setLastModified(mTime));
+		Instant mTime = TimeUtil.setLastModifiedWithOffset(file.toPath(),
+				-5000L);
 
 		DirCache cache = DirCache.lock(db.getIndexFile(), db.getFS());
 		DirCacheEntry entry = cache.getEntry("Test.txt");
 		assertNotNull(entry);
 		entry.setLength(0);
-		entry.setLastModified(0);
+		entry.setLastModified(EPOCH);
 		cache.write();
 		assertTrue(cache.commit());
 
@@ -391,10 +397,12 @@
 		entry = cache.getEntry("Test.txt");
 		assertNotNull(entry);
 		assertEquals(0, entry.getLength());
-		assertEquals(0, entry.getLastModified());
+		assertEquals(EPOCH, entry.getLastModifiedInstant());
 
-		db.getIndexFile().setLastModified(
-				db.getIndexFile().lastModified() - 5000);
+		Files.setLastModifiedTime(db.getIndexFile().toPath(),
+				FileTime.from(FS.DETECTED
+						.lastModifiedInstant(db.getIndexFile())
+						.minusMillis(5000L)));
 
 		assertNotNull(git.checkout().setName("test").call());
 
@@ -402,7 +410,7 @@
 		entry = cache.getEntry("Test.txt");
 		assertNotNull(entry);
 		assertEquals(size, entry.getLength());
-		assertEquals(mTime, entry.getLastModified());
+		assertEquals(mTime, entry.getLastModifiedInstant());
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
index 18fed4b..3bde0eb 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
@@ -65,6 +65,7 @@
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.time.TimeUtil;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
@@ -319,11 +320,11 @@
 	public void commitUpdatesSmudgedEntries() throws Exception {
 		try (Git git = new Git(db)) {
 			File file1 = writeTrashFile("file1.txt", "content1");
-			assertTrue(file1.setLastModified(file1.lastModified() - 5000));
+			TimeUtil.setLastModifiedWithOffset(file1.toPath(), -5000L);
 			File file2 = writeTrashFile("file2.txt", "content2");
-			assertTrue(file2.setLastModified(file2.lastModified() - 5000));
+			TimeUtil.setLastModifiedWithOffset(file2.toPath(), -5000L);
 			File file3 = writeTrashFile("file3.txt", "content3");
-			assertTrue(file3.setLastModified(file3.lastModified() - 5000));
+			TimeUtil.setLastModifiedWithOffset(file3.toPath(), -5000L);
 
 			assertNotNull(git.add().addFilepattern("file1.txt")
 					.addFilepattern("file2.txt").addFilepattern("file3.txt").call());
@@ -354,11 +355,12 @@
 			assertEquals(0, cache.getEntry("file2.txt").getLength());
 			assertEquals(0, cache.getEntry("file3.txt").getLength());
 
-			long indexTime = db.getIndexFile().lastModified();
-			db.getIndexFile().setLastModified(indexTime - 5000);
+			TimeUtil.setLastModifiedWithOffset(db.getIndexFile().toPath(),
+					-5000L);
 
 			write(file1, "content4");
-			assertTrue(file1.setLastModified(file1.lastModified() + 2500));
+
+			TimeUtil.setLastModifiedWithOffset(file1.toPath(), 2500L);
 			assertNotNull(git.commit().setMessage("edit file").setOnly("file1.txt")
 					.call());
 
@@ -376,9 +378,9 @@
 	public void commitIgnoresSmudgedEntryWithDifferentId() throws Exception {
 		try (Git git = new Git(db)) {
 			File file1 = writeTrashFile("file1.txt", "content1");
-			assertTrue(file1.setLastModified(file1.lastModified() - 5000));
+			TimeUtil.setLastModifiedWithOffset(file1.toPath(), -5000L);
 			File file2 = writeTrashFile("file2.txt", "content2");
-			assertTrue(file2.setLastModified(file2.lastModified() - 5000));
+			TimeUtil.setLastModifiedWithOffset(file2.toPath(), -5000L);
 
 			assertNotNull(git.add().addFilepattern("file1.txt")
 					.addFilepattern("file2.txt").call());
@@ -407,11 +409,11 @@
 			assertEquals(0, cache.getEntry("file1.txt").getLength());
 			assertEquals(0, cache.getEntry("file2.txt").getLength());
 
-			long indexTime = db.getIndexFile().lastModified();
-			db.getIndexFile().setLastModified(indexTime - 5000);
+			TimeUtil.setLastModifiedWithOffset(db.getIndexFile().toPath(),
+					-5000L);
 
 			write(file1, "content5");
-			assertTrue(file1.setLastModified(file1.lastModified() + 1000));
+			TimeUtil.setLastModifiedWithOffset(file1.toPath(), 1000L);
 
 			assertNotNull(git.commit().setMessage("edit file").setOnly("file1.txt")
 					.call());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DiffCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DiffCommandTest.java
index 43c3a8c..3a93839 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DiffCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DiffCommandTest.java
@@ -44,7 +44,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -55,6 +54,7 @@
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.time.TimeUtil;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -230,7 +230,7 @@
 	@Test
 	public void testNoOutputStreamSet() throws Exception {
 		File file = writeTrashFile("test.txt", "a");
-		assertTrue(file.setLastModified(file.lastModified() - 5000));
+		TimeUtil.setLastModifiedWithOffset(file.toPath(), -5000L);
 		try (Git git = new Git(db)) {
 			git.add().addFilepattern(".").call();
 			write(file, "b");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
index 333ebd3..563b32d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
@@ -42,6 +42,7 @@
  */
 package org.eclipse.jgit.api;
 
+import static java.time.Instant.EPOCH;
 import static org.eclipse.jgit.api.ResetCommand.ResetType.HARD;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -52,6 +53,9 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
 
 import org.eclipse.jgit.api.ResetCommand.ResetType;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -288,13 +292,13 @@
 	public void testMixedResetRetainsSizeAndModifiedTime() throws Exception {
 		git = new Git(db);
 
-		writeTrashFile("a.txt", "a").setLastModified(
-				System.currentTimeMillis() - 60 * 1000);
+		Files.setLastModifiedTime(writeTrashFile("a.txt", "a").toPath(),
+				FileTime.from(Instant.now().minusSeconds(60)));
 		assertNotNull(git.add().addFilepattern("a.txt").call());
 		assertNotNull(git.commit().setMessage("a commit").call());
 
-		writeTrashFile("b.txt", "b").setLastModified(
-				System.currentTimeMillis() - 60 * 1000);
+		Files.setLastModifiedTime(writeTrashFile("b.txt", "b").toPath(),
+				FileTime.from(Instant.now().minusSeconds(60)));
 		assertNotNull(git.add().addFilepattern("b.txt").call());
 		RevCommit commit2 = git.commit().setMessage("b commit").call();
 		assertNotNull(commit2);
@@ -304,12 +308,12 @@
 		DirCacheEntry aEntry = cache.getEntry("a.txt");
 		assertNotNull(aEntry);
 		assertTrue(aEntry.getLength() > 0);
-		assertTrue(aEntry.getLastModified() > 0);
+		assertTrue(aEntry.getLastModifiedInstant().compareTo(EPOCH) > 0);
 
 		DirCacheEntry bEntry = cache.getEntry("b.txt");
 		assertNotNull(bEntry);
 		assertTrue(bEntry.getLength() > 0);
-		assertTrue(bEntry.getLastModified() > 0);
+		assertTrue(bEntry.getLastModifiedInstant().compareTo(EPOCH) > 0);
 
 		assertSameAsHead(git.reset().setMode(ResetType.MIXED)
 				.setRef(commit2.getName()).call());
@@ -318,13 +322,17 @@
 
 		DirCacheEntry mixedAEntry = cache.getEntry("a.txt");
 		assertNotNull(mixedAEntry);
-		assertEquals(aEntry.getLastModified(), mixedAEntry.getLastModified());
-		assertEquals(aEntry.getLastModified(), mixedAEntry.getLastModified());
+		assertEquals(aEntry.getLastModifiedInstant(),
+				mixedAEntry.getLastModifiedInstant());
+		assertEquals(aEntry.getLastModifiedInstant(),
+				mixedAEntry.getLastModifiedInstant());
 
 		DirCacheEntry mixedBEntry = cache.getEntry("b.txt");
 		assertNotNull(mixedBEntry);
-		assertEquals(bEntry.getLastModified(), mixedBEntry.getLastModified());
-		assertEquals(bEntry.getLastModified(), mixedBEntry.getLastModified());
+		assertEquals(bEntry.getLastModifiedInstant(),
+				mixedBEntry.getLastModifiedInstant());
+		assertEquals(bEntry.getLastModifiedInstant(),
+				mixedBEntry.getLastModifiedInstant());
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java
index 0c1131b..cdf86f0 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java
@@ -53,6 +53,7 @@
 import static org.junit.Assert.fail;
 
 import java.io.File;
+import java.time.Instant;
 
 import org.eclipse.jgit.events.IndexChangedEvent;
 import org.eclipse.jgit.events.IndexChangedListener;
@@ -99,7 +100,7 @@
 	public void testBuildOneFile_FinishWriteCommit() throws Exception {
 		final String path = "a-file-path";
 		final FileMode mode = FileMode.REGULAR_FILE;
-		final long lastModified = 1218123387057L;
+		final Instant lastModified = Instant.ofEpochMilli(1218123387057L);
 		final int length = 1342;
 		final DirCacheEntry entOrig;
 		{
@@ -117,7 +118,7 @@
 			assertEquals(ObjectId.zeroId(), entOrig.getObjectId());
 			assertEquals(mode.getBits(), entOrig.getRawMode());
 			assertEquals(0, entOrig.getStage());
-			assertEquals(lastModified, entOrig.getLastModified());
+			assertEquals(lastModified, entOrig.getLastModifiedInstant());
 			assertEquals(length, entOrig.getLength());
 			assertFalse(entOrig.isAssumeValid());
 			b.add(entOrig);
@@ -139,7 +140,7 @@
 			assertEquals(ObjectId.zeroId(), entOrig.getObjectId());
 			assertEquals(mode.getBits(), entOrig.getRawMode());
 			assertEquals(0, entOrig.getStage());
-			assertEquals(lastModified, entOrig.getLastModified());
+			assertEquals(lastModified, entOrig.getLastModifiedInstant());
 			assertEquals(length, entOrig.getLength());
 			assertFalse(entOrig.isAssumeValid());
 		}
@@ -149,7 +150,7 @@
 	public void testBuildOneFile_Commit() throws Exception {
 		final String path = "a-file-path";
 		final FileMode mode = FileMode.REGULAR_FILE;
-		final long lastModified = 1218123387057L;
+		final Instant lastModified = Instant.ofEpochMilli(1218123387057L);
 		final int length = 1342;
 		final DirCacheEntry entOrig;
 		{
@@ -167,7 +168,7 @@
 			assertEquals(ObjectId.zeroId(), entOrig.getObjectId());
 			assertEquals(mode.getBits(), entOrig.getRawMode());
 			assertEquals(0, entOrig.getStage());
-			assertEquals(lastModified, entOrig.getLastModified());
+			assertEquals(lastModified, entOrig.getLastModifiedInstant());
 			assertEquals(length, entOrig.getLength());
 			assertFalse(entOrig.isAssumeValid());
 			b.add(entOrig);
@@ -187,7 +188,7 @@
 			assertEquals(ObjectId.zeroId(), entOrig.getObjectId());
 			assertEquals(mode.getBits(), entOrig.getRawMode());
 			assertEquals(0, entOrig.getStage());
-			assertEquals(lastModified, entOrig.getLastModified());
+			assertEquals(lastModified, entOrig.getLastModifiedInstant());
 			assertEquals(length, entOrig.getLength());
 			assertFalse(entOrig.isAssumeValid());
 		}
@@ -204,7 +205,7 @@
 		final String path = "a-file-path";
 		final FileMode mode = FileMode.REGULAR_FILE;
 		// "old" date in 2008
-		final long lastModified = 1218123387057L;
+		final Instant lastModified = Instant.ofEpochMilli(1218123387057L);
 		final int length = 1342;
 		DirCacheEntry entOrig;
 		boolean receivedEvent = false;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
index 86e2852..475819d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
@@ -43,6 +43,7 @@
 
 package org.eclipse.jgit.dircache;
 
+import static java.time.Instant.EPOCH;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertSame;
@@ -188,7 +189,7 @@
 		e.setAssumeValid(false);
 		e.setCreationTime(2L);
 		e.setFileMode(FileMode.EXECUTABLE_FILE);
-		e.setLastModified(3L);
+		e.setLastModified(EPOCH.plusMillis(3L));
 		e.setLength(100L);
 		e.setObjectId(ObjectId
 				.fromString("0123456789012345678901234567890123456789"));
@@ -199,7 +200,7 @@
 		f.setAssumeValid(true);
 		f.setCreationTime(10L);
 		f.setFileMode(FileMode.SYMLINK);
-		f.setLastModified(20L);
+		f.setLastModified(EPOCH.plusMillis(20L));
 		f.setLength(100000000L);
 		f.setObjectId(ObjectId
 				.fromString("1234567890123456789012345678901234567890"));
@@ -212,7 +213,7 @@
 				ObjectId.fromString("1234567890123456789012345678901234567890"),
 				e.getObjectId());
 		assertEquals(FileMode.SYMLINK, e.getFileMode());
-		assertEquals(20L, e.getLastModified());
+		assertEquals(EPOCH.plusMillis(20L), e.getLastModifiedInstant());
 		assertEquals(100000000L, e.getLength());
 		if (keepStage)
 			assertEquals(DirCacheEntry.STAGE_2, e.getStage());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
index bfa30d5..5a5ae1d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
@@ -976,7 +976,7 @@
 				rw.markStart(rw.parseCommit(ref.getObjectId()));
 			}
 			for (RevCommit next; (next = rw.next()) != null;) {
-				if (AnyObjectId.equals(next, id)) {
+				if (AnyObjectId.isEqual(next, id)) {
 					return true;
 				}
 			}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
index 3c4b8cf..501b788 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
@@ -938,6 +938,7 @@
 		REJECTED_MISSING_OBJECT(ReceiveCommand.Result.REJECTED_MISSING_OBJECT),
 		TRANSACTION_ABORTED(ReceiveCommand::isTransactionAborted);
 
+		@SuppressWarnings("ImmutableEnumChecker")
 		final Predicate<? super ReceiveCommand> p;
 
 		private Result(Predicate<? super ReceiveCommand> p) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java
index 643daa5..6bec056 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java
@@ -56,6 +56,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.time.Instant;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -71,6 +72,7 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.storage.file.WindowCacheConfig;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
 import org.junit.After;
 import org.junit.Before;
@@ -235,7 +237,8 @@
 
 	private static void write(File[] files, PackWriter pw)
 			throws IOException {
-		final long begin = files[0].getParentFile().lastModified();
+		final Instant begin = FS.DETECTED
+				.lastModifiedInstant(files[0].getParentFile());
 		NullProgressMonitor m = NullProgressMonitor.INSTANCE;
 
 		try (OutputStream out = new BufferedOutputStream(
@@ -252,7 +255,8 @@
 	}
 
 	private static void delete(File[] list) throws IOException {
-		final long begin = list[0].getParentFile().lastModified();
+		final Instant begin = FS.DETECTED
+				.lastModifiedInstant(list[0].getParentFile());
 		for (File f : list) {
 			FileUtils.delete(f);
 			assertFalse(f + " was removed", f.exists());
@@ -260,14 +264,14 @@
 		touch(begin, list[0].getParentFile());
 	}
 
-	private static void touch(long begin, File dir) {
-		while (begin >= dir.lastModified()) {
+	private static void touch(Instant begin, File dir) throws IOException {
+		while (begin.compareTo(FS.DETECTED.lastModifiedInstant(dir)) >= 0) {
 			try {
 				Thread.sleep(25);
 			} catch (InterruptedException ie) {
 				//
 			}
-			dir.setLastModified(System.currentTimeMillis());
+			FS.DETECTED.setLastModified(dir.toPath(), Instant.now());
 		}
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java
index 5ebdeb6..6fa35d6 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java
@@ -42,49 +42,68 @@
  */
 package org.eclipse.jgit.internal.storage.file;
 
+import static org.eclipse.jgit.junit.JGitTestUtil.read;
+import static org.eclipse.jgit.junit.JGitTestUtil.write;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.FileTime;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.ArrayList;
-import java.util.List;
+import java.util.concurrent.TimeUnit;
 
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.FileStoreAttributes;
 import org.eclipse.jgit.util.FileUtils;
+import org.eclipse.jgit.util.Stats;
 import org.eclipse.jgit.util.SystemReader;
 import org.junit.After;
 import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class FileSnapshotTest {
+	private static final Logger LOG = LoggerFactory
+			.getLogger(FileSnapshotTest.class);
 
-	private List<File> files = new ArrayList<>();
+	private Path trash;
 
-	private File trash;
+	private FileStoreAttributes fsAttrCache;
 
 	@Before
 	public void setUp() throws Exception {
-		trash = File.createTempFile("tmp_", "");
-		trash.delete();
-		assertTrue("mkdir " + trash, trash.mkdir());
+		trash = Files.createTempDirectory("tmp_");
+		// measure timer resolution before the test to avoid time critical tests
+		// are affected by time needed for measurement
+		fsAttrCache = FS
+				.getFileStoreAttributes(trash.getParent());
 	}
 
 	@Before
 	@After
 	public void tearDown() throws Exception {
-		FileUtils.delete(trash, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
+		FileUtils.delete(trash.toFile(),
+				FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
 	}
 
-	private static void waitNextSec(File f) {
-		long initialLastModified = f.lastModified();
+	private static void waitNextTick(Path f) throws IOException {
+		Instant initialLastModified = FS.DETECTED.lastModifiedInstant(f);
 		do {
-			f.setLastModified(System.currentTimeMillis());
-		} while (f.lastModified() == initialLastModified);
+			FS.DETECTED.setLastModified(f, Instant.now());
+		} while (FS.DETECTED.lastModifiedInstant(f)
+				.equals(initialLastModified));
 	}
 
 	/**
@@ -94,12 +113,12 @@
 	 */
 	@Test
 	public void testActuallyIsModifiedTrivial() throws Exception {
-		File f1 = createFile("simple");
-		waitNextSec(f1);
-		FileSnapshot save = FileSnapshot.save(f1);
+		Path f1 = createFile("simple");
+		waitNextTick(f1);
+		FileSnapshot save = FileSnapshot.save(f1.toFile());
 		append(f1, (byte) 'x');
-		waitNextSec(f1);
-		assertTrue(save.isModified(f1));
+		waitNextTick(f1);
+		assertTrue(save.isModified(f1.toFile()));
 	}
 
 	/**
@@ -112,11 +131,17 @@
 	 */
 	@Test
 	public void testNewFileWithWait() throws Exception {
-		File f1 = createFile("newfile");
-		waitNextSec(f1);
-		FileSnapshot save = FileSnapshot.save(f1);
-		Thread.sleep(1500);
-		assertTrue(save.isModified(f1));
+		// if filesystem timestamp resolution is high the snapshot won't be
+		// racily clean
+		Assume.assumeTrue(
+				fsAttrCache.getFsTimestampResolution()
+						.compareTo(Duration.ofMillis(10)) > 0);
+		Path f1 = createFile("newfile");
+		waitNextTick(f1);
+		FileSnapshot save = FileSnapshot.save(f1.toFile());
+		TimeUnit.NANOSECONDS.sleep(
+				fsAttrCache.getFsTimestampResolution().dividedBy(2).toNanos());
+		assertTrue(save.isModified(f1.toFile()));
 	}
 
 	/**
@@ -126,9 +151,33 @@
 	 */
 	@Test
 	public void testNewFileNoWait() throws Exception {
-		File f1 = createFile("newfile");
-		FileSnapshot save = FileSnapshot.save(f1);
-		assertTrue(save.isModified(f1));
+		// if filesystem timestamp resolution is smaller than time needed to
+		// create a file and FileSnapshot the snapshot won't be racily clean
+		Assume.assumeTrue(fsAttrCache.getFsTimestampResolution()
+				.compareTo(Duration.ofMillis(10)) > 0);
+		for (int i = 0; i < 50; i++) {
+			Instant start = Instant.now();
+			Path f1 = createFile("newfile");
+			FileSnapshot save = FileSnapshot.save(f1.toFile());
+			Duration res = FS.getFileStoreAttributes(f1)
+					.getFsTimestampResolution();
+			Instant end = Instant.now();
+			if (Duration.between(start, end)
+					.compareTo(res.multipliedBy(2)) > 0) {
+				// This test is racy: under load, there may be a delay between createFile() and
+				// FileSnapshot.save(). This can stretch the time between the read TS and FS
+				// creation TS to the point that it exceeds the FS granularity, and we
+				// conclude it cannot be racily clean, and therefore must be really clean.
+				//
+				// This should be relatively uncommon.
+				continue;
+			}
+			// The file wasn't really modified, but it looks just like a "maybe racily clean"
+			// file.
+			assertTrue(save.isModified(f1.toFile()));
+			return;
+		}
+		fail("too much load for this test");
 	}
 
 	/**
@@ -142,19 +191,19 @@
 	@Test
 	public void testSimulatePackfileReplacement() throws Exception {
 		Assume.assumeFalse(SystemReader.getInstance().isWindows());
-		File f1 = createFile("file"); // inode y
-		File f2 = createFile("fool"); // Guarantees new inode x
+		Path f1 = createFile("file"); // inode y
+		Path f2 = createFile("fool"); // Guarantees new inode x
 		// wait on f2 since this method resets lastModified of the file
 		// and leaves lastModified of f1 untouched
-		waitNextSec(f2);
-		waitNextSec(f2);
-		FileTime timestamp = Files.getLastModifiedTime(f1.toPath());
-		FileSnapshot save = FileSnapshot.save(f1);
-		Files.move(f2.toPath(), f1.toPath(), // Now "file" is inode x
+		waitNextTick(f2);
+		waitNextTick(f2);
+		FileTime timestamp = Files.getLastModifiedTime(f1);
+		FileSnapshot save = FileSnapshot.save(f1.toFile());
+		Files.move(f2, f1, // Now "file" is inode x
 				StandardCopyOption.REPLACE_EXISTING,
 				StandardCopyOption.ATOMIC_MOVE);
-		Files.setLastModifiedTime(f1.toPath(), timestamp);
-		assertTrue(save.isModified(f1));
+		Files.setLastModifiedTime(f1, timestamp);
+		assertTrue(save.isModified(f1.toFile()));
 		assertTrue("unexpected change of fileKey", save.wasFileKeyChanged());
 		assertFalse("unexpected size change", save.wasSizeChanged());
 		assertFalse("unexpected lastModified change",
@@ -171,24 +220,83 @@
 	 */
 	@Test
 	public void testFileSizeChanged() throws Exception {
-		File f = createFile("file");
-		FileTime timestamp = Files.getLastModifiedTime(f.toPath());
-		FileSnapshot save = FileSnapshot.save(f);
+		Path f = createFile("file");
+		FileTime timestamp = Files.getLastModifiedTime(f);
+		FileSnapshot save = FileSnapshot.save(f.toFile());
 		append(f, (byte) 'x');
-		Files.setLastModifiedTime(f.toPath(), timestamp);
-		assertTrue(save.isModified(f));
+		Files.setLastModifiedTime(f, timestamp);
+		assertTrue(save.isModified(f.toFile()));
 		assertTrue(save.wasSizeChanged());
 	}
 
-	private File createFile(String string) throws IOException {
-		trash.mkdirs();
-		File f = File.createTempFile(string, "tdat", trash);
-		files.add(f);
-		return f;
+	@Test
+	public void fileSnapshotEquals() throws Exception {
+		// 0 sized FileSnapshot.
+		FileSnapshot fs1 = FileSnapshot.MISSING_FILE;
+		// UNKNOWN_SIZE FileSnapshot.
+		FileSnapshot fs2 = FileSnapshot.save(fs1.lastModifiedInstant());
+
+		assertTrue(fs1.equals(fs2));
+		assertTrue(fs2.equals(fs1));
 	}
 
-	private static void append(File f, byte b) throws IOException {
-		try (FileOutputStream os = new FileOutputStream(f, true)) {
+	@SuppressWarnings("boxing")
+	@Test
+	public void detectFileModified() throws IOException {
+		int failures = 0;
+		long racyNanos = 0;
+		final int COUNT = 10000;
+		ArrayList<Long> deltas = new ArrayList<>();
+		File f = createFile("test").toFile();
+		for (int i = 0; i < COUNT; i++) {
+			write(f, "a");
+			FileSnapshot snapshot = FileSnapshot.save(f);
+			assertEquals("file should contain 'a'", "a", read(f));
+			write(f, "b");
+			if (!snapshot.isModified(f)) {
+				deltas.add(snapshot.lastDelta());
+				racyNanos = snapshot.lastRacyThreshold();
+				failures++;
+			}
+			assertEquals("file should contain 'b'", "b", read(f));
+		}
+		if (failures > 0) {
+			Stats stats = new Stats();
+			LOG.debug(
+					"delta [ns] since modification FileSnapshot failed to detect");
+			for (Long d : deltas) {
+				stats.add(d);
+				LOG.debug(String.format("%,d", d));
+			}
+			LOG.error(
+					"count, failures, eff. racy threshold [ns], delta min [ns],"
+							+ " delta max [ns], delta avg [ns],"
+							+ " delta stddev [ns]");
+			LOG.error(String.format(
+					"%,d, %,d, %,d, %,.0f, %,.0f, %,.0f, %,.0f", COUNT,
+					failures, racyNanos, stats.min(), stats.max(),
+					stats.avg(), stats.stddev()));
+		}
+		assertTrue(
+				String.format(
+						"FileSnapshot: failures to detect file modifications"
+								+ " %d out of %d\n"
+								+ "timestamp resolution %d µs"
+								+ " min racy threshold %d µs"
+						, failures, COUNT,
+						fsAttrCache.getFsTimestampResolution().toNanos() / 1000,
+						fsAttrCache.getMinimalRacyInterval().toNanos() / 1000),
+				failures == 0);
+	}
+
+	private Path createFile(String string) throws IOException {
+		Files.createDirectories(trash);
+		return Files.createTempFile(trash, string, "tdat");
+	}
+
+	private static void append(Path f, byte b) throws IOException {
+		try (OutputStream os = Files.newOutputStream(f,
+				StandardOpenOption.APPEND)) {
 			os.write(b);
 		}
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java
index d16998d..eaa245b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java
@@ -147,9 +147,10 @@
 		return tip;
 	}
 
-	protected long lastModified(AnyObjectId objectId) throws IOException {
-		return repo.getFS().lastModified(
-				repo.getObjectDatabase().fileFor(objectId));
+	protected long lastModified(AnyObjectId objectId) {
+		return repo.getFS()
+				.lastModifiedInstant(repo.getObjectDatabase().fileFor(objectId))
+				.toEpochMilli();
 	}
 
 	protected static void fsTick() throws InterruptedException, IOException {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java
index a9d0dc2..97c5638 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java
@@ -65,6 +65,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
 import org.junit.Assume;
 import org.junit.Rule;
 import org.junit.Test;
@@ -157,19 +158,22 @@
 
 			// To deal with racy-git situations JGit's Filesnapshot class will
 			// report a file/folder potentially dirty if
-			// cachedLastReadTime-cachedLastModificationTime < 2500ms. This
-			// causes JGit to always rescan a file after modification. But:
-			// this was true only if the difference between current system time
-			// and cachedLastModification time was less than 2500ms. If the
-			// modification is more than 2500ms ago we may have reported a
-			// file/folder to be clean although it has not been rescanned. A
-			// Bug. To show the bug we sleep for more than 2500ms
+			// cachedLastReadTime-cachedLastModificationTime < filesystem
+			// timestamp resolution. This causes JGit to always rescan a file
+			// after modification. But: this was true only if the difference
+			// between current system time and cachedLastModification time was
+			// less than 2500ms. If the modification is more than 2500ms ago we
+			// may have reported a file/folder to be clean although it has not
+			// been rescanned. A bug. To show the bug we sleep for more than
+			// 2500ms
 			Thread.sleep(2600);
 
 			File[] ret = packsFolder.listFiles(
 					(File dir, String name) -> name.endsWith(".pack"));
 			assertTrue(ret != null && ret.length == 1);
-			Assume.assumeTrue(tmpFile.lastModified() == ret[0].lastModified());
+			FS fs = db.getFS();
+			Assume.assumeTrue(fs.lastModifiedInstant(tmpFile)
+					.equals(fs.lastModifiedInstant(ret[0])));
 
 			// all objects are in a new packfile but we will not detect it
 			assertFalse(receivingDB.getObjectDatabase().has(unknownID));
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java
index a1433e9..d5bc61a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java
@@ -59,6 +59,7 @@
 import java.nio.file.StandardOpenOption;
 //import java.nio.file.attribute.BasicFileAttributes;
 import java.text.ParseException;
+import java.time.Instant;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.Random;
@@ -80,6 +81,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.storage.pack.PackConfig;
+import org.eclipse.jgit.util.FS;
 import org.junit.Test;
 
 public class PackFileSnapshotTest extends RepositoryTestCase {
@@ -188,7 +190,8 @@
 		AnyObjectId chk1 = pf.getPackChecksum();
 		String name = pf.getPackName();
 		Long length = Long.valueOf(pf.getPackFile().length());
-		long m1 = packFilePath.toFile().lastModified();
+		FS fs = db.getFS();
+		Instant m1 = fs.lastModifiedInstant(packFilePath);
 
 		// Wait for a filesystem timer tick to enhance probability the rest of
 		// this test is done before the filesystem timer ticks again.
@@ -198,15 +201,15 @@
 		// content and checksum are different since compression level differs
 		AnyObjectId chk2 = repackAndCheck(6, name, length, chk1)
 				.getPackChecksum();
-		long m2 = packFilePath.toFile().lastModified();
-		assumeFalse(m2 == m1);
+		Instant m2 = fs.lastModifiedInstant(packFilePath);
+		assumeFalse(m2.equals(m1));
 
 		// Repack to create packfile with same name, length. Lastmodified is
 		// equal to the previous one because we are in the same filesystem timer
 		// slot. Content and its checksum are different
 		AnyObjectId chk3 = repackAndCheck(7, name, length, chk2)
 				.getPackChecksum();
-		long m3 = packFilePath.toFile().lastModified();
+		Instant m3 = fs.lastModifiedInstant(packFilePath);
 
 		// ask for an unknown git object to force jgit to rescan the list of
 		// available packs. If we would ask for a known objectid then JGit would
@@ -214,7 +217,7 @@
 		db.getObjectDatabase().has(unknownID);
 		assertEquals(chk3, getSinglePack(db.getObjectDatabase().getPacks())
 				.getPackChecksum());
-		assumeTrue(m3 == m2);
+		assumeTrue(m3.equals(m2));
 	}
 
 	// Try repacking so fast that we get two new packs which differ only in
@@ -253,7 +256,8 @@
 		// Repack to create third packfile
 		AnyObjectId chk3 = repackAndCheck(7, name, length, chk2)
 				.getPackChecksum();
-		long m3 = packFilePath.toFile().lastModified();
+		FS fs = db.getFS();
+		Instant m3 = fs.lastModifiedInstant(packFilePath);
 		db.getObjectDatabase().has(unknownID);
 		assertEquals(chk3, getSinglePack(db.getObjectDatabase().getPacks())
 				.getPackChecksum());
@@ -265,8 +269,8 @@
 		// Copy copy2 to packfile data to force modification of packfile without
 		// changing the packfile's filekey.
 		copyPack(packFileBasePath, ".copy2", "");
-		long m2 = packFilePath.toFile().lastModified();
-		assumeFalse(m3 == m2);
+		Instant m2 = fs.lastModifiedInstant(packFilePath);
+		assumeFalse(m3.equals(m2));
 
 		db.getObjectDatabase().has(unknownID);
 		assertEquals(chk2, getSinglePack(db.getObjectDatabase().getPacks())
@@ -275,8 +279,8 @@
 		// Copy copy2 to packfile data to force modification of packfile without
 		// changing the packfile's filekey.
 		copyPack(packFileBasePath, ".copy1", "");
-		long m1 = packFilePath.toFile().lastModified();
-		assumeTrue(m2 == m1);
+		Instant m1 = fs.lastModifiedInstant(packFilePath);
+		assumeTrue(m2.equals(m1));
 		db.getObjectDatabase().has(unknownID);
 		assertEquals(chk1, getSinglePack(db.getObjectDatabase().getPacks())
 				.getPackChecksum());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
index bd9572b..d8e1685 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
@@ -60,6 +60,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -71,6 +72,7 @@
 import org.eclipse.jgit.events.ListenerHandle;
 import org.eclipse.jgit.events.RefsChangedEvent;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.Repeat;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -79,6 +81,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.util.FS;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -666,6 +669,7 @@
 		assertEquals(B, all.get(HEAD).getObjectId());
 	}
 
+	@Repeat(n = 100, abortOnFailure = false)
 	@Test
 	public void testFindRef_DiscoversModifiedLoose() throws IOException {
 		Map<String, Ref> all;
@@ -1361,10 +1365,8 @@
 	private void writePackedRefs(String content) throws IOException {
 		File pr = new File(diskRepo.getDirectory(), "packed-refs");
 		write(pr, content);
-
-		final long now = System.currentTimeMillis();
-		final int oneHourAgo = 3600 * 1000;
-		pr.setLastModified(now - oneHourAgo);
+		FS fs = diskRepo.getFS();
+		fs.setLastModified(pr.toPath(), Instant.now().minusSeconds(3600));
 	}
 
 	private void deleteLooseRef(String name) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
index 825d15b..43f30d8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
@@ -59,6 +59,7 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.time.Instant;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -82,6 +83,7 @@
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
 import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.IO;
 import org.junit.Rule;
@@ -790,12 +792,14 @@
 	 *
 	 * @param name
 	 *            the file in the repository to force a time change on.
+	 * @throws IOException
 	 */
-	private void BUG_WorkAroundRacyGitIssues(String name) {
+	private void BUG_WorkAroundRacyGitIssues(String name) throws IOException {
 		File path = new File(db.getDirectory(), name);
-		long old = path.lastModified();
+		FS fs = db.getFS();
+		Instant old = fs.lastModifiedInstant(path);
 		long set = 1250379778668L; // Sat Aug 15 20:12:58 GMT-03:30 2009
-		path.setLastModified(set);
-		assertTrue("time changed", old != path.lastModified());
+		fs.setLastModified(path.toPath(), Instant.ofEpochMilli(set));
+		assertFalse("time changed", old.equals(fs.lastModifiedInstant(path)));
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
index e3985cc..e839545 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
@@ -51,8 +51,10 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MICROSECONDS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.eclipse.jgit.util.FileUtils.pathToString;
 import static org.junit.Assert.assertArrayEquals;
@@ -1224,8 +1226,18 @@
 
 	@Test
 	public void testTimeUnit() throws ConfigInvalidException {
+		assertEquals(0, parseTime("0", NANOSECONDS));
+		assertEquals(2, parseTime("2ns", NANOSECONDS));
+		assertEquals(200, parseTime("200 nanoseconds", NANOSECONDS));
+
+		assertEquals(0, parseTime("0", MICROSECONDS));
+		assertEquals(2, parseTime("2us", MICROSECONDS));
+		assertEquals(2, parseTime("2000 nanoseconds", MICROSECONDS));
+		assertEquals(200, parseTime("200 microseconds", MICROSECONDS));
+
 		assertEquals(0, parseTime("0", MILLISECONDS));
 		assertEquals(2, parseTime("2ms", MILLISECONDS));
+		assertEquals(2, parseTime("2000microseconds", MILLISECONDS));
 		assertEquals(200, parseTime("200 milliseconds", MILLISECONDS));
 
 		assertEquals(0, parseTime("0s", SECONDS));
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexModificationTimesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexModificationTimesTest.java
index b9bbbeb..a363414 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexModificationTimesTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexModificationTimesTest.java
@@ -37,8 +37,12 @@
  */
 package org.eclipse.jgit.lib;
 
+import static java.time.Instant.EPOCH;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import java.time.Instant;
+
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheEntry;
@@ -63,11 +67,11 @@
 			DirCacheEntry entry = dc.getEntry(path);
 			DirCacheEntry entry2 = dc.getEntry(path);
 
-			assertTrue("last modified shall not be zero!",
-					entry.getLastModified() != 0);
+			assertFalse("last modified shall not be the epoch!",
+					entry.getLastModifiedInstant().equals(EPOCH));
 
-			assertTrue("last modified shall not be zero!",
-					entry2.getLastModified() != 0);
+			assertFalse("last modified shall not be the epoch!",
+					entry2.getLastModifiedInstant().equals(EPOCH));
 
 			writeTrashFile(path, "new content");
 			git.add().addFilepattern(path).call();
@@ -77,11 +81,11 @@
 			entry = dc.getEntry(path);
 			entry2 = dc.getEntry(path);
 
-			assertTrue("last modified shall not be zero!",
-					entry.getLastModified() != 0);
+			assertFalse("last modified shall not be the epoch!",
+					entry.getLastModifiedInstant().equals(EPOCH));
 
-			assertTrue("last modified shall not be zero!",
-					entry2.getLastModified() != 0);
+			assertFalse("last modified shall not be the epoch!",
+					entry2.getLastModifiedInstant().equals(EPOCH));
 		}
 	}
 
@@ -97,7 +101,7 @@
 			DirCache dc = db.readDirCache();
 			DirCacheEntry entry = dc.getEntry(path);
 
-			long masterLastMod = entry.getLastModified();
+			Instant masterLastMod = entry.getLastModifiedInstant();
 
 			git.checkout().setCreateBranch(true).setName("side").call();
 
@@ -110,7 +114,7 @@
 			dc = db.readDirCache();
 			entry = dc.getEntry(path);
 
-			long sideLastMode = entry.getLastModified();
+			Instant sideLastMod = entry.getLastModifiedInstant();
 
 			Thread.sleep(2000);
 
@@ -120,9 +124,10 @@
 			dc = db.readDirCache();
 			entry = dc.getEntry(path);
 
-			assertTrue("shall have equal mod time!", masterLastMod == sideLastMode);
-			assertTrue("shall not equal master timestamp!",
-					entry.getLastModified() == masterLastMod);
+			assertTrue("shall have equal mod time!",
+					masterLastMod.equals(sideLastMod));
+			assertTrue("shall have equal master timestamp!",
+					entry.getLastModifiedInstant().equals(masterLastMod));
 		}
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java
index 11100b6..d3e4efe 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java
@@ -42,88 +42,26 @@
  */
 package org.eclipse.jgit.lib;
 
-import static java.lang.Long.valueOf;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.util.TreeSet;
+import java.time.Instant;
 
 import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.time.TimeUtil;
 import org.eclipse.jgit.treewalk.FileTreeIterator;
-import org.eclipse.jgit.treewalk.FileTreeIteratorWithTimeControl;
-import org.eclipse.jgit.treewalk.NameConflictTreeWalk;
-import org.eclipse.jgit.util.FileUtils;
+import org.eclipse.jgit.treewalk.WorkingTreeOptions;
+import org.eclipse.jgit.util.FS;
 import org.junit.Test;
 
 public class RacyGitTests extends RepositoryTestCase {
-	@Test
-	public void testIterator()
-			throws IllegalStateException, IOException, InterruptedException {
-		TreeSet<Long> modTimes = new TreeSet<>();
-		File lastFile = null;
-		for (int i = 0; i < 10; i++) {
-			lastFile = new File(db.getWorkTree(), "0." + i);
-			FileUtils.createNewFile(lastFile);
-			if (i == 5)
-				fsTick(lastFile);
-		}
-		modTimes.add(valueOf(fsTick(lastFile)));
-		for (int i = 0; i < 10; i++) {
-			lastFile = new File(db.getWorkTree(), "1." + i);
-			FileUtils.createNewFile(lastFile);
-		}
-		modTimes.add(valueOf(fsTick(lastFile)));
-		for (int i = 0; i < 10; i++) {
-			lastFile = new File(db.getWorkTree(), "2." + i);
-			FileUtils.createNewFile(lastFile);
-			if (i % 4 == 0)
-				fsTick(lastFile);
-		}
-		FileTreeIteratorWithTimeControl fileIt = new FileTreeIteratorWithTimeControl(
-				db, modTimes);
-		try (NameConflictTreeWalk tw = new NameConflictTreeWalk(db)) {
-			tw.addTree(fileIt);
-			tw.setRecursive(true);
-			FileTreeIterator t;
-			long t0 = 0;
-			for (int i = 0; i < 10; i++) {
-				assertTrue(tw.next());
-				t = tw.getTree(0, FileTreeIterator.class);
-				if (i == 0) {
-					t0 = t.getEntryLastModified();
-				} else {
-					assertEquals(t0, t.getEntryLastModified());
-				}
-			}
-			long t1 = 0;
-			for (int i = 0; i < 10; i++) {
-				assertTrue(tw.next());
-				t = tw.getTree(0, FileTreeIterator.class);
-				if (i == 0) {
-					t1 = t.getEntryLastModified();
-					assertTrue(t1 > t0);
-				} else {
-					assertEquals(t1, t.getEntryLastModified());
-				}
-			}
-			long t2 = 0;
-			for (int i = 0; i < 10; i++) {
-				assertTrue(tw.next());
-				t = tw.getTree(0, FileTreeIterator.class);
-				if (i == 0) {
-					t2 = t.getEntryLastModified();
-					assertTrue(t2 > t1);
-				} else {
-					assertEquals(t2, t.getEntryLastModified());
-				}
-			}
-		}
-	}
 
 	@Test
 	public void testRacyGitDetection() throws Exception {
@@ -137,10 +75,10 @@
 		fsTick(db.getIndexFile());
 
 		// create two files
-		File a = addToWorkDir("a", "a");
-		File b = addToWorkDir("b", "b");
-		assertTrue(a.setLastModified(b.lastModified()));
-		assertTrue(b.setLastModified(b.lastModified()));
+		File a = writeToWorkDir("a", "a");
+		File b = writeToWorkDir("b", "b");
+		TimeUtil.setLastModifiedOf(a.toPath(), b.toPath());
+		TimeUtil.setLastModifiedOf(b.toPath(), b.toPath());
 
 		// wait to ensure that file-modTimes and therefore index entry modTime
 		// doesn't match the modtime of index-file after next persistance
@@ -158,23 +96,36 @@
 		fsTick(db.getIndexFile());
 
 		// Create a racy git situation. This is a situation that the index is
-		// updated and then a file is modified within a second. By changing the
-		// index file artificially, we create a fake racy situation.
-		File updatedA = addToWorkDir("a", "a2");
-		assertTrue(updatedA.setLastModified(updatedA.lastModified() + 100));
+		// updated and then a file is modified within the same tick of the
+		// filesystem timestamp resolution. By changing the index file
+		// artificially, we create a fake racy situation.
+		File updatedA = writeToWorkDir("a", "a2");
+		Instant newLastModified = TimeUtil
+				.setLastModifiedWithOffset(updatedA.toPath(), 100L);
 		resetIndex(new FileTreeIterator(db));
-		assertTrue(db.getIndexFile()
-				.setLastModified(updatedA.lastModified() + 90));
+		FS.DETECTED.setLastModified(db.getIndexFile().toPath(),
+				newLastModified);
 
-		db.readDirCache();
-		// although racily clean a should not be reported as being dirty
+		DirCache dc = db.readDirCache();
+		// check index state: although racily clean a should not be reported as
+		// being dirty since we forcefully reset the index to match the working
+		// tree
 		assertEquals(
 				"[a, mode:100644, time:t1, smudged, length:0, content:a2]"
 						+ "[b, mode:100644, time:t0, length:1, content:b]",
 				indexState(SMUDGE | MOD_TIME | LENGTH | CONTENT));
+
+		// compare state of files in working tree with index to check that
+		// FileTreeIterator.isModified() works as expected
+		FileTreeIterator f = new FileTreeIterator(db.getWorkTree(), db.getFS(),
+				db.getConfig().get(WorkingTreeOptions.KEY));
+		assertTrue(f.findFile("a"));
+		try (ObjectReader reader = db.newObjectReader()) {
+			assertFalse(f.isModified(dc.getEntry("a"), false, reader));
+		}
 	}
 
-	private File addToWorkDir(String path, String content) throws IOException {
+	private File writeToWorkDir(String path, String content) throws IOException {
 		File f = new File(db.getWorkTree(), path);
 		try (FileOutputStream fos = new FileOutputStream(f)) {
 			fos.write(content.getBytes(UTF_8));
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
index f6fc00c..62495fb 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
@@ -43,6 +43,7 @@
 package org.eclipse.jgit.merge;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.time.Instant.EPOCH;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -54,6 +55,7 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Map;
 
@@ -840,9 +842,9 @@
 	 * Throws an exception if reading beyond limit.
 	 */
 	static class BigReadForbiddenStream extends ObjectStream.Filter {
-		int limit;
+		long limit;
 
-		BigReadForbiddenStream(ObjectStream orig, int limit) {
+		BigReadForbiddenStream(ObjectStream orig, long limit) {
 			super(orig.getType(), orig.getSize(), orig);
 			this.limit = limit;
 		}
@@ -1090,13 +1092,13 @@
 	@Theory
 	public void checkForCorrectIndex(MergeStrategy strategy) throws Exception {
 		File f;
-		long lastTs4, lastTsIndex;
+		Instant lastTs4, lastTsIndex;
 		Git git = Git.wrap(db);
 		File indexFile = db.getIndexFile();
 
 		// Create initial content and remember when the last file was written.
 		f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
-		lastTs4 = FS.DETECTED.lastModified(f);
+		lastTs4 = FS.DETECTED.lastModifiedInstant(f);
 
 		// add all files, commit and check this doesn't update any working tree
 		// files and that the index is in a new file system timer tick. Make
@@ -1109,8 +1111,9 @@
 		checkConsistentLastModified("0", "1", "2", "3", "4");
 		checkModificationTimeStampOrder("1", "2", "3", "4", "<.git/index");
 		assertEquals("Commit should not touch working tree file 4", lastTs4,
-				FS.DETECTED.lastModified(new File(db.getWorkTree(), "4")));
-		lastTsIndex = FS.DETECTED.lastModified(indexFile);
+				FS.DETECTED
+						.lastModifiedInstant(new File(db.getWorkTree(), "4")));
+		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
 
 		// Do modifications on the master branch. Then add and commit. This
 		// should touch only "0", "2 and "3"
@@ -1124,7 +1127,7 @@
 		checkConsistentLastModified("0", "1", "2", "3", "4");
 		checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
 				+ lastTsIndex, "<0", "2", "3", "<.git/index");
-		lastTsIndex = FS.DETECTED.lastModified(indexFile);
+		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
 
 		// Checkout a side branch. This should touch only "0", "2 and "3"
 		fsTick(indexFile);
@@ -1133,7 +1136,7 @@
 		checkConsistentLastModified("0", "1", "2", "3", "4");
 		checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
 				+ lastTsIndex, "<0", "2", "3", ".git/index");
-		lastTsIndex = FS.DETECTED.lastModified(indexFile);
+		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
 
 		// This checkout may have populated worktree and index so fast that we
 		// may have smudged entries now. Check that we have the right content
@@ -1146,13 +1149,13 @@
 				indexState(CONTENT));
 		fsTick(indexFile);
 		f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
-		lastTs4 = FS.DETECTED.lastModified(f);
+		lastTs4 = FS.DETECTED.lastModifiedInstant(f);
 		fsTick(f);
 		git.add().addFilepattern(".").call();
 		checkConsistentLastModified("0", "1", "2", "3", "4");
 		checkModificationTimeStampOrder("*" + lastTsIndex, "<0", "1", "2", "3",
 				"4", "<.git/index");
-		lastTsIndex = FS.DETECTED.lastModified(indexFile);
+		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
 
 		// Do modifications on the side branch. Touch only "1", "2 and "3"
 		fsTick(indexFile);
@@ -1163,7 +1166,7 @@
 		checkConsistentLastModified("0", "1", "2", "3", "4");
 		checkModificationTimeStampOrder("0", "4", "*" + lastTs4, "<*"
 				+ lastTsIndex, "<1", "2", "3", "<.git/index");
-		lastTsIndex = FS.DETECTED.lastModified(indexFile);
+		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
 
 		// merge master and side. Should only touch "0," "2" and "3"
 		fsTick(indexFile);
@@ -1330,9 +1333,10 @@
 			assertEquals(
 					"IndexEntry with path "
 							+ path
-							+ " has lastmodified with is different from the worktree file",
-					FS.DETECTED.lastModified(new File(workTree, path)), dc.getEntry(path)
-							.getLastModified());
+							+ " has lastmodified which is different from the worktree file",
+					FS.DETECTED.lastModifiedInstant(new File(workTree, path)),
+					dc.getEntry(path)
+							.getLastModifiedInstant());
 	}
 
 	// Assert that modification timestamps of working tree files are as
@@ -1341,21 +1345,22 @@
 	// then this file must be younger then file i. A path "*<modtime>"
 	// represents a file with a modification time of <modtime>
 	// E.g. ("a", "b", "<c", "f/a.txt") means: a<=b<c<=f/a.txt
-	private void checkModificationTimeStampOrder(String... pathes)
-			throws IOException {
-		long lastMod = Long.MIN_VALUE;
+	private void checkModificationTimeStampOrder(String... pathes) {
+		Instant lastMod = EPOCH;
 		for (String p : pathes) {
 			boolean strong = p.startsWith("<");
 			boolean fixed = p.charAt(strong ? 1 : 0) == '*';
 			p = p.substring((strong ? 1 : 0) + (fixed ? 1 : 0));
-			long curMod = fixed ? Long.valueOf(p).longValue()
-					: FS.DETECTED.lastModified(new File(db.getWorkTree(), p));
-			if (strong)
+			Instant curMod = fixed ? Instant.parse(p)
+					: FS.DETECTED
+							.lastModifiedInstant(new File(db.getWorkTree(), p));
+			if (strong) {
 				assertTrue("path " + p + " is not younger than predecesssor",
-						curMod > lastMod);
-			else
+						curMod.compareTo(lastMod) > 0);
+			} else {
 				assertTrue("path " + p + " is older than predecesssor",
-						curMod >= lastMod);
+						curMod.compareTo(lastMod) >= 0);
+			}
 		}
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevObjectTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevObjectTest.java
index 4969305..e397220 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevObjectTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevObjectTest.java
@@ -89,8 +89,8 @@
 		assertEquals(a1.hashCode(), a2.hashCode());
 		assertEquals(b1.hashCode(), b2.hashCode());
 
-		assertTrue(AnyObjectId.equals(a1, a2));
-		assertTrue(AnyObjectId.equals(b1, b2));
+		assertTrue(AnyObjectId.isEqual(a1, a2));
+		assertTrue(AnyObjectId.isEqual(b1, b2));
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java
index b401d2b..4364820 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java
@@ -46,12 +46,13 @@
 import static org.eclipse.jgit.util.FileUtils.pathToString;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
 import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 import java.util.StringTokenizer;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -85,42 +86,44 @@
 	private static final String CONTENT3 = "[" + USER + "]\n\t" + NAME + " = "
 			+ ALICE + "\n" + "[" + USER + "]\n\t" + EMAIL + " = " + ALICE_EMAIL;
 
-	private File trash;
+	private Path trash;
 
 	@Before
 	public void setUp() throws Exception {
-		trash = File.createTempFile("tmp_", "");
-		trash.delete();
-		assertTrue("mkdir " + trash, trash.mkdir());
+		trash = Files.createTempDirectory("tmp_");
+		FS.getFileStoreAttributes(trash.getParent());
 	}
 
 	@After
 	public void tearDown() throws Exception {
-		FileUtils.delete(trash, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
+		FileUtils.delete(trash.toFile(),
+				FileUtils.RECURSIVE | FileUtils.SKIP_MISSING | FileUtils.RETRY);
 	}
 
 	@Test
 	public void testSystemEncoding() throws IOException, ConfigInvalidException {
-		final File file = createFile(CONTENT1.getBytes(UTF_8));
-		final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED);
+		final Path file = createFile(CONTENT1.getBytes(UTF_8));
+		final FileBasedConfig config = new FileBasedConfig(file.toFile(),
+				FS.DETECTED);
 		config.load();
 		assertEquals(ALICE, config.getString(USER, null, NAME));
 
 		config.setString(USER, null, NAME, BOB);
 		config.save();
-		assertArrayEquals(CONTENT2.getBytes(UTF_8), IO.readFully(file));
+		assertArrayEquals(CONTENT2.getBytes(UTF_8), IO.readFully(file.toFile()));
 	}
 
 	@Test
 	public void testUTF8withoutBOM() throws IOException, ConfigInvalidException {
-		final File file = createFile(CONTENT1.getBytes(UTF_8));
-		final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED);
+		final Path file = createFile(CONTENT1.getBytes(UTF_8));
+		final FileBasedConfig config = new FileBasedConfig(file.toFile(),
+				FS.DETECTED);
 		config.load();
 		assertEquals(ALICE, config.getString(USER, null, NAME));
 
 		config.setString(USER, null, NAME, BOB);
 		config.save();
-		assertArrayEquals(CONTENT2.getBytes(UTF_8), IO.readFully(file));
+		assertArrayEquals(CONTENT2.getBytes(UTF_8), IO.readFully(file.toFile()));
 	}
 
 	@Test
@@ -131,8 +134,9 @@
 		bos1.write(0xBF);
 		bos1.write(CONTENT1.getBytes(UTF_8));
 
-		final File file = createFile(bos1.toByteArray());
-		final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED);
+		final Path file = createFile(bos1.toByteArray());
+		final FileBasedConfig config = new FileBasedConfig(file.toFile(),
+				FS.DETECTED);
 		config.load();
 		assertEquals(ALICE, config.getString(USER, null, NAME));
 
@@ -144,7 +148,7 @@
 		bos2.write(0xBB);
 		bos2.write(0xBF);
 		bos2.write(CONTENT2.getBytes(UTF_8));
-		assertArrayEquals(bos2.toByteArray(), IO.readFully(file));
+		assertArrayEquals(bos2.toByteArray(), IO.readFully(file.toFile()));
 	}
 
 	@Test
@@ -153,8 +157,9 @@
 		bos1.write(" \n\t".getBytes(UTF_8));
 		bos1.write(CONTENT1.getBytes(UTF_8));
 
-		final File file = createFile(bos1.toByteArray());
-		final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED);
+		final Path file = createFile(bos1.toByteArray());
+		final FileBasedConfig config = new FileBasedConfig(file.toFile(),
+				FS.DETECTED);
 		config.load();
 		assertEquals(ALICE, config.getString(USER, null, NAME));
 
@@ -164,19 +169,20 @@
 		final ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
 		bos2.write(" \n\t".getBytes(UTF_8));
 		bos2.write(CONTENT2.getBytes(UTF_8));
-		assertArrayEquals(bos2.toByteArray(), IO.readFully(file));
+		assertArrayEquals(bos2.toByteArray(), IO.readFully(file.toFile()));
 	}
 
 	@Test
 	public void testIncludeAbsolute()
 			throws IOException, ConfigInvalidException {
-		final File includedFile = createFile(CONTENT1.getBytes(UTF_8));
+		final Path includedFile = createFile(CONTENT1.getBytes(UTF_8));
 		final ByteArrayOutputStream bos = new ByteArrayOutputStream();
 		bos.write("[include]\npath=".getBytes(UTF_8));
-		bos.write(pathToString(includedFile).getBytes(UTF_8));
+		bos.write(pathToString(includedFile.toFile()).getBytes(UTF_8));
 
-		final File file = createFile(bos.toByteArray());
-		final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED);
+		final Path file = createFile(bos.toByteArray());
+		final FileBasedConfig config = new FileBasedConfig(file.toFile(),
+				FS.DETECTED);
 		config.load();
 		assertEquals(ALICE, config.getString(USER, null, NAME));
 	}
@@ -184,13 +190,14 @@
 	@Test
 	public void testIncludeRelativeDot()
 			throws IOException, ConfigInvalidException {
-		final File includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1");
+		final Path includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1");
 		final ByteArrayOutputStream bos = new ByteArrayOutputStream();
 		bos.write("[include]\npath=".getBytes(UTF_8));
-		bos.write(("./" + includedFile.getName()).getBytes(UTF_8));
+		bos.write(("./" + includedFile.getFileName()).getBytes(UTF_8));
 
-		final File file = createFile(bos.toByteArray(), "dir1");
-		final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED);
+		final Path file = createFile(bos.toByteArray(), "dir1");
+		final FileBasedConfig config = new FileBasedConfig(file.toFile(),
+				FS.DETECTED);
 		config.load();
 		assertEquals(ALICE, config.getString(USER, null, NAME));
 	}
@@ -198,14 +205,15 @@
 	@Test
 	public void testIncludeRelativeDotDot()
 			throws IOException, ConfigInvalidException {
-		final File includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1");
+		final Path includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1");
 		final ByteArrayOutputStream bos = new ByteArrayOutputStream();
 		bos.write("[include]\npath=".getBytes(UTF_8));
-		bos.write(("../" + includedFile.getParentFile().getName() + "/"
-				+ includedFile.getName()).getBytes(UTF_8));
+		bos.write(("../" + includedFile.getParent().getFileName() + "/"
+				+ includedFile.getFileName()).getBytes(UTF_8));
 
-		final File file = createFile(bos.toByteArray(), "dir2");
-		final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED);
+		final Path file = createFile(bos.toByteArray(), "dir2");
+		final FileBasedConfig config = new FileBasedConfig(file.toFile(),
+				FS.DETECTED);
 		config.load();
 		assertEquals(ALICE, config.getString(USER, null, NAME));
 	}
@@ -213,13 +221,14 @@
 	@Test
 	public void testIncludeRelativeDotDotNotFound()
 			throws IOException, ConfigInvalidException {
-		final File includedFile = createFile(CONTENT1.getBytes(UTF_8));
+		final Path includedFile = createFile(CONTENT1.getBytes(UTF_8));
 		final ByteArrayOutputStream bos = new ByteArrayOutputStream();
 		bos.write("[include]\npath=".getBytes(UTF_8));
-		bos.write(("../" + includedFile.getName()).getBytes(UTF_8));
+		bos.write(("../" + includedFile.getFileName()).getBytes(UTF_8));
 
-		final File file = createFile(bos.toByteArray());
-		final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED);
+		final Path file = createFile(bos.toByteArray());
+		final FileBasedConfig config = new FileBasedConfig(file.toFile(),
+				FS.DETECTED);
 		config.load();
 		assertEquals(null, config.getString(USER, null, NAME));
 	}
@@ -227,16 +236,16 @@
 	@Test
 	public void testIncludeWithTilde()
 			throws IOException, ConfigInvalidException {
-		final File includedFile = createFile(CONTENT1.getBytes(UTF_8), "home");
+		final Path includedFile = createFile(CONTENT1.getBytes(UTF_8), "home");
 		final ByteArrayOutputStream bos = new ByteArrayOutputStream();
 		bos.write("[include]\npath=".getBytes(UTF_8));
-		bos.write(("~/" + includedFile.getName()).getBytes(UTF_8));
+		bos.write(("~/" + includedFile.getFileName()).getBytes(UTF_8));
 
-		final File file = createFile(bos.toByteArray(), "repo");
+		final Path file = createFile(bos.toByteArray(), "repo");
 		final FS fs = FS.DETECTED.newInstance();
-		fs.setUserHome(includedFile.getParentFile());
+		fs.setUserHome(includedFile.getParent().toFile());
 
-		final FileBasedConfig config = new FileBasedConfig(file, fs);
+		final FileBasedConfig config = new FileBasedConfig(file.toFile(), fs);
 		config.load();
 		assertEquals(ALICE, config.getString(USER, null, NAME));
 	}
@@ -246,13 +255,14 @@
 			throws IOException, ConfigInvalidException {
 		// use a content with multiple sections and multiple key/value pairs
 		// because code for first line works different than for subsequent lines
-		final File includedFile = createFile(CONTENT3.getBytes(UTF_8), "dir1");
+		final Path includedFile = createFile(CONTENT3.getBytes(UTF_8), "dir1");
 
-		final File file = createFile(new byte[0], "dir2");
-		FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED);
+		final Path file = createFile(new byte[0], "dir2");
+		FileBasedConfig config = new FileBasedConfig(file.toFile(),
+				FS.DETECTED);
 		config.setString("include", null, "path",
-				("../" + includedFile.getParentFile().getName() + "/"
-						+ includedFile.getName()));
+				("../" + includedFile.getParent().getFileName() + "/"
+						+ includedFile.getFileName()));
 
 		// just by setting the include.path, it won't be included
 		assertEquals(null, config.getString(USER, null, NAME));
@@ -267,7 +277,7 @@
 		assertEquals(2,
 				new StringTokenizer(expectedText, "\n", false).countTokens());
 
-		config = new FileBasedConfig(file, FS.DETECTED);
+		config = new FileBasedConfig(file.toFile(), FS.DETECTED);
 		config.load();
 
 		String actualText = config.toText();
@@ -285,16 +295,17 @@
 		assertEquals(ALICE_EMAIL, config.getString(USER, null, EMAIL));
 	}
 
-	private File createFile(byte[] content) throws IOException {
+	private Path createFile(byte[] content) throws IOException {
 		return createFile(content, null);
 	}
 
-	private File createFile(byte[] content, String subdir) throws IOException {
-		File dir = subdir != null ? new File(trash, subdir) : trash;
-		dir.mkdirs();
+	private Path createFile(byte[] content, String subdir) throws IOException {
+		Path dir = subdir != null ? trash.resolve(subdir) : trash;
+		Files.createDirectories(dir);
 
-		File f = File.createTempFile(getClass().getName(), null, dir);
-		try (FileOutputStream os = new FileOutputStream(f, true)) {
+		Path f = Files.createTempFile(dir, getClass().getName(), null);
+		try (OutputStream os = Files.newOutputStream(f,
+				StandardOpenOption.APPEND)) {
 			os.write(content);
 		}
 		return f;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java
index 1a22e10..2e5027f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java
@@ -56,10 +56,13 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.transport.OpenSshConfig.Host;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.SystemReader;
 import org.junit.Before;
@@ -91,13 +94,19 @@
 	}
 
 	private void config(String data) throws IOException {
-		long lastMtime = configFile.lastModified();
+		FS fs = FS.DETECTED;
+		long resolution = FS.getFileStoreAttributes(configFile.toPath())
+				.getFsTimestampResolution().toNanos();
+		Instant lastMtime = fs.lastModifiedInstant(configFile);
 		do {
 			try (final OutputStreamWriter fw = new OutputStreamWriter(
 					new FileOutputStream(configFile), UTF_8)) {
 				fw.write(data);
+				TimeUnit.NANOSECONDS.sleep(resolution);
+			} catch (InterruptedException e) {
+				Thread.interrupted();
 			}
-		} while (lastMtime == configFile.lastModified());
+		} while (lastMtime.equals(fs.lastModifiedInstant(configFile)));
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java
index 0303ea2..a3ce4ae 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java
@@ -54,6 +54,7 @@
 import java.io.IOException;
 import java.nio.file.InvalidPathException;
 import java.security.MessageDigest;
+import java.time.Instant;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.ResetCommand.ResetType;
@@ -88,7 +89,7 @@
 public class FileTreeIteratorTest extends RepositoryTestCase {
 	private final String[] paths = { "a,", "a,b", "a/b", "a0b" };
 
-	private long[] mtime;
+	private Instant[] mtime;
 
 	@Override
 	@Before
@@ -101,11 +102,11 @@
 		// This should stress the sorting code better than doing it in
 		// the correct order.
 		//
-		mtime = new long[paths.length];
+		mtime = new Instant[paths.length];
 		for (int i = paths.length - 1; i >= 0; i--) {
 			final String s = paths[i];
 			writeTrashFile(s, s);
-			mtime[i] = FS.DETECTED.lastModified(new File(trash, s));
+			mtime[i] = db.getFS().lastModifiedInstant(new File(trash, s));
 		}
 	}
 
@@ -201,7 +202,7 @@
 		assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode);
 		assertEquals(paths[0], nameOf(top));
 		assertEquals(paths[0].length(), top.getEntryLength());
-		assertEquals(mtime[0], top.getEntryLastModified());
+		assertEquals(mtime[0], top.getEntryLastModifiedInstant());
 
 		top.next(1);
 		assertFalse(top.first());
@@ -209,7 +210,7 @@
 		assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode);
 		assertEquals(paths[1], nameOf(top));
 		assertEquals(paths[1].length(), top.getEntryLength());
-		assertEquals(mtime[1], top.getEntryLastModified());
+		assertEquals(mtime[1], top.getEntryLastModifiedInstant());
 
 		top.next(1);
 		assertFalse(top.first());
@@ -224,7 +225,7 @@
 		assertFalse(sub.eof());
 		assertEquals(paths[2], nameOf(sub));
 		assertEquals(paths[2].length(), subfti.getEntryLength());
-		assertEquals(mtime[2], subfti.getEntryLastModified());
+		assertEquals(mtime[2], subfti.getEntryLastModifiedInstant());
 
 		sub.next(1);
 		assertTrue(sub.eof());
@@ -235,7 +236,7 @@
 		assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode);
 		assertEquals(paths[3], nameOf(top));
 		assertEquals(paths[3].length(), top.getEntryLength());
-		assertEquals(mtime[3], top.getEntryLastModified());
+		assertEquals(mtime[3], top.getEntryLastModifiedInstant());
 
 		top.next(1);
 		assertTrue(top.eof());
@@ -347,20 +348,21 @@
 	@Test
 	public void testIsModifiedFileSmudged() throws Exception {
 		File f = writeTrashFile("file", "content");
+		FS fs = db.getFS();
 		try (Git git = new Git(db)) {
 			// The idea of this test is to check the smudged handling
 			// Hopefully fsTick will make sure our entry gets smudged
 			fsTick(f);
 			writeTrashFile("file", "content");
-			long lastModified = f.lastModified();
+			Instant lastModified = fs.lastModifiedInstant(f);
 			git.add().addFilepattern("file").call();
 			writeTrashFile("file", "conten2");
-			f.setLastModified(lastModified);
+			fs.setLastModified(f.toPath(), lastModified);
 			// We cannot trust this to go fast enough on
 			// a system with less than one-second lastModified
 			// resolution, so we force the index to have the
 			// same timestamp as the file we look at.
-			db.getIndexFile().setLastModified(lastModified);
+			fs.setLastModified(db.getIndexFile().toPath(), lastModified);
 		}
 		DirCacheEntry dce = db.readDirCache().getEntry("file");
 		FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(), db
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorWithTimeControl.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorWithTimeControl.java
deleted file mode 100644
index fc79d45..0000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorWithTimeControl.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
- * 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.treewalk;
-
-import java.io.File;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.util.FS;
-
-/**
- * A {@link FileTreeIterator} used in tests which allows to specify explicitly
- * what will be returned by {@link #getEntryLastModified()}. This allows to
- * write tests where certain files have to have the same modification time.
- * <p>
- * This iterator is configured by a list of strictly increasing long values
- * t(0), t(1), ..., t(n). For each file with a modification between t(x) and
- * t(x+1) [ t(x) &lt;= time &lt; t(x+1) ] this iterator will report t(x). For
- * files with a modification time smaller t(0) a modification time of 0 is
- * returned. For files with a modification time greater or equal t(n) t(n) will
- * be returned.
- * <p>
- * This class was written especially to test racy-git problems
- */
-public class FileTreeIteratorWithTimeControl extends FileTreeIterator {
-	private TreeSet<Long> modTimes;
-
-	public FileTreeIteratorWithTimeControl(FileTreeIterator p, Repository repo,
-			TreeSet<Long> modTimes) {
-		super(p, repo.getWorkTree(), repo.getFS());
-		this.modTimes = modTimes;
-	}
-
-	public FileTreeIteratorWithTimeControl(FileTreeIterator p, File f, FS fs,
-			TreeSet<Long> modTimes) {
-		super(p, f, fs);
-		this.modTimes = modTimes;
-	}
-
-	public FileTreeIteratorWithTimeControl(Repository repo,
-			TreeSet<Long> modTimes) {
-		super(repo);
-		this.modTimes = modTimes;
-	}
-
-	public FileTreeIteratorWithTimeControl(File f, FS fs,
-			TreeSet<Long> modTimes) {
-		super(f, fs, new Config().get(WorkingTreeOptions.KEY));
-		this.modTimes = modTimes;
-	}
-
-	@Override
-	public AbstractTreeIterator createSubtreeIterator(ObjectReader reader) {
-		return new FileTreeIteratorWithTimeControl(this,
-				((FileEntry) current()).getFile(), fs, modTimes);
-	}
-
-	@Override
-	public long getEntryLastModified() {
-		if (modTimes == null)
-			return 0;
-		Long cutOff = Long.valueOf(super.getEntryLastModified() + 1);
-		SortedSet<Long> head = modTimes.headSet(cutOff);
-		return head.isEmpty() ? 0 : head.last().longValue();
-	}
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java
index 6dfa6ef..99d4351 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java
@@ -43,6 +43,7 @@
 
 package org.eclipse.jgit.util;
 
+import static java.time.Instant.EPOCH;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -67,6 +68,7 @@
 
 import org.eclipse.jgit.errors.CommandFailedException;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.RepositoryCache;
 import org.junit.After;
 import org.junit.Assume;
 import org.junit.Before;
@@ -107,7 +109,7 @@
 		assertTrue(fs.exists(link));
 		String targetName = fs.readSymLink(link);
 		assertEquals("b", targetName);
-		assertTrue(fs.lastModified(link) > 0);
+		assertTrue(fs.lastModifiedInstant(link).compareTo(EPOCH) > 0);
 		assertTrue(fs.exists(link));
 		assertFalse(fs.canExecute(link));
 		// The length of a symbolic link is a length of the target file path.
@@ -121,8 +123,9 @@
 		// Now create the link target
 		FileUtils.createNewFile(target);
 		assertTrue(fs.exists(link));
-		assertTrue(fs.lastModified(link) > 0);
-		assertTrue(fs.lastModified(target) > fs.lastModified(link));
+		assertTrue(fs.lastModifiedInstant(link).compareTo(EPOCH) > 0);
+		assertTrue(fs.lastModifiedInstant(target)
+				.compareTo(fs.lastModifiedInstant(link)) > 0);
 		assertFalse(fs.canExecute(link));
 		fs.setExecute(target, true);
 		assertFalse(fs.canExecute(link));
@@ -229,7 +232,8 @@
 				.ofPattern("uuuu-MMM-dd HH:mm:ss.nnnnnnnnn", Locale.ENGLISH)
 				.withZone(ZoneId.systemDefault());
 		Path dir = Files.createTempDirectory("probe-filesystem");
-		Duration resolution = FS.getFsTimerResolution(dir);
+		Duration resolution = FS.getFileStoreAttributes(dir)
+				.getFsTimestampResolution();
 		long resolutionNs = resolution.toNanos();
 		assertTrue(resolutionNs > 0);
 		for (int i = 0; i < 10; i++) {
@@ -252,4 +256,11 @@
 			}
 		}
 	}
+
+	// bug 548682
+	@Test
+	public void testRepoCacheRelativePathUnbornRepo() {
+		assertFalse(RepositoryCache.FileKey
+				.isGitRepository(new File("repo.git"), FS.DETECTED));
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/SimpleLruCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/SimpleLruCacheTest.java
new file mode 100644
index 0000000..5894f7d
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/SimpleLruCacheTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com>
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SimpleLruCacheTest {
+
+	private Path trash;
+
+	private SimpleLruCache<String, String> cache;
+
+
+	@Before
+	public void setup() throws IOException {
+		trash = Files.createTempDirectory("tmp_");
+		cache = new SimpleLruCache<>(100, 0.2f);
+	}
+
+	@Before
+	@After
+	public void tearDown() throws Exception {
+		FileUtils.delete(trash.toFile(),
+				FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
+	}
+
+	@Test
+	public void testPutGet() {
+		cache.put("a", "A");
+		cache.put("z", "Z");
+		assertEquals("A", cache.get("a"));
+		assertEquals("Z", cache.get("z"));
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testPurgeFactorTooLarge() {
+		cache.configure(5, 1.01f);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testPurgeFactorTooLarge2() {
+		cache.configure(5, 100);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testPurgeFactorTooSmall() {
+		cache.configure(5, 0);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testPurgeFactorTooSmall2() {
+		cache.configure(5, -100);
+	}
+
+	@Test
+	public void testGetMissing() {
+		assertEquals(null, cache.get("a"));
+	}
+
+	@Test
+	public void testPurge() {
+		for (int i = 0; i < 101; i++) {
+			cache.put("a" + i, "a" + i);
+		}
+		assertEquals(80, cache.size());
+		assertNull(cache.get("a0"));
+		assertNull(cache.get("a20"));
+		assertNotNull(cache.get("a21"));
+		assertNotNull(cache.get("a99"));
+	}
+
+	@Test
+	public void testConfigure() {
+		for (int i = 0; i < 100; i++) {
+			cache.put("a" + i, "a" + i);
+		}
+		assertEquals(100, cache.size());
+		cache.configure(10, 0.3f);
+		assertEquals(7, cache.size());
+		assertNull(cache.get("a0"));
+		assertNull(cache.get("a92"));
+		assertNotNull(cache.get("a93"));
+		assertNotNull(cache.get("a99"));
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StatsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StatsTest.java
new file mode 100644
index 0000000..8b25382
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StatsTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com>
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.eclipse.jgit.util.Stats;
+import org.junit.Test;
+
+public class StatsTest {
+	@Test
+	public void testStatsTrivial() {
+		Stats s = new Stats();
+		s.add(1);
+		s.add(1);
+		s.add(1);
+		assertEquals(3, s.count());
+		assertEquals(1.0, s.min(), 1E-6);
+		assertEquals(1.0, s.max(), 1E-6);
+		assertEquals(1.0, s.avg(), 1E-6);
+		assertEquals(0.0, s.var(), 1E-6);
+		assertEquals(0.0, s.stddev(), 1E-6);
+	}
+
+	@Test
+	public void testStats() {
+		Stats s = new Stats();
+		s.add(1);
+		s.add(2);
+		s.add(3);
+		s.add(4);
+		assertEquals(4, s.count());
+		assertEquals(1.0, s.min(), 1E-6);
+		assertEquals(4.0, s.max(), 1E-6);
+		assertEquals(2.5, s.avg(), 1E-6);
+		assertEquals(1.666667, s.var(), 1E-6);
+		assertEquals(1.290994, s.stddev(), 1E-6);
+	}
+
+	@Test
+	/**
+	 * see
+	 * https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Example
+	 */
+	public void testStatsCancellationExample1() {
+		Stats s = new Stats();
+		s.add(1E8 + 4);
+		s.add(1E8 + 7);
+		s.add(1E8 + 13);
+		s.add(1E8 + 16);
+		assertEquals(4, s.count());
+		assertEquals(1E8 + 4, s.min(), 1E-6);
+		assertEquals(1E8 + 16, s.max(), 1E-6);
+		assertEquals(1E8 + 10, s.avg(), 1E-6);
+		assertEquals(30, s.var(), 1E-6);
+		assertEquals(5.477226, s.stddev(), 1E-6);
+	}
+
+	@Test
+	/**
+	 * see
+	 * https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Example
+	 */
+	public void testStatsCancellationExample2() {
+		Stats s = new Stats();
+		s.add(1E9 + 4);
+		s.add(1E9 + 7);
+		s.add(1E9 + 13);
+		s.add(1E9 + 16);
+		assertEquals(4, s.count());
+		assertEquals(1E9 + 4, s.min(), 1E-6);
+		assertEquals(1E9 + 16, s.max(), 1E-6);
+		assertEquals(1E9 + 10, s.avg(), 1E-6);
+		assertEquals(30, s.var(), 1E-6);
+		assertEquals(5.477226, s.stddev(), 1E-6);
+	}
+
+	@Test
+	public void testNoValues() {
+		Stats s = new Stats();
+		assertTrue(Double.isNaN(s.var()));
+		assertTrue(Double.isNaN(s.stddev()));
+		assertTrue(Double.isNaN(s.avg()));
+		assertTrue(Double.isNaN(s.min()));
+		assertTrue(Double.isNaN(s.max()));
+		s.add(42.3);
+		assertTrue(Double.isNaN(s.var()));
+		assertTrue(Double.isNaN(s.stddev()));
+		assertEquals(42.3, s.avg(), 1E-6);
+		assertEquals(42.3, s.max(), 1E-6);
+		assertEquals(42.3, s.min(), 1E-6);
+		s.add(42.3);
+		assertEquals(0, s.var(), 1E-6);
+		assertEquals(0, s.stddev(), 1E-6);
+	}
+}
diff --git a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/CommitGraphPane.java b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/CommitGraphPane.java
index 943a325..b898caf 100644
--- a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/CommitGraphPane.java
+++ b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/CommitGraphPane.java
@@ -64,6 +64,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revplot.PlotCommit;
 import org.eclipse.jgit.revplot.PlotCommitList;
+import org.eclipse.jgit.util.References;
 
 /**
  * Draws a commit graph in a JTable.
@@ -176,7 +177,7 @@
 		}
 
 		PersonIdent authorFor(PlotCommit<SwingLane> c) {
-			if (c != lastCommit) {
+			if (!References.isSameObject(c, lastCommit)) {
 				lastCommit = c;
 				lastAuthor = c.getAuthorIdent();
 			}
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
new file mode 100644
index 0000000..3a5dd11
--- /dev/null
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<component id="org.eclipse.jgit" version="2">
+    <resource path="src/org/eclipse/jgit/dircache/DirCacheEntry.java" type="org.eclipse.jgit.dircache.DirCacheEntry">
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="getLastModifiedInstant()"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="mightBeRacilyClean(Instant)"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="setLastModified(Instant)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/lib/AnyObjectId.java" type="org.eclipse.jgit.lib.AnyObjectId">
+        <filter id="1141899266">
+            <message_arguments>
+                <message_argument value="5.4"/>
+                <message_argument value="5.5"/>
+                <message_argument value="isEqual(AnyObjectId, AnyObjectId)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/lib/ConfigConstants.java" type="org.eclipse.jgit.lib.ConfigConstants">
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="CONFIG_FILESYSTEM_SECTION"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="CONFIG_KEY_MIN_RACY_THRESHOLD"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="CONFIG_KEY_TIMESTAMP_RESOLUTION"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/storage/file/FileBasedConfig.java" type="org.eclipse.jgit.storage.file.FileBasedConfig">
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="load(boolean)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java" type="org.eclipse.jgit.treewalk.WorkingTreeIterator">
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="getEntryLastModifiedInstant()"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java" type="org.eclipse.jgit.treewalk.WorkingTreeIterator$Entry">
+        <filter id="336695337">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.treewalk.WorkingTreeIterator.Entry"/>
+                <message_argument value="getLastModifiedInstant()"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="getLastModifiedInstant()"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/util/FS.java" type="org.eclipse.jgit.util.FS">
+        <filter id="338792546">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.util.FS"/>
+                <message_argument value="getFsTimerResolution(Path)"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="getFileStoreAttributes(Path)"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="lastModifiedInstant(File)"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="lastModifiedInstant(Path)"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="setAsyncFileStoreAttributes(boolean)"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="setLastModified(Path, Instant)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/util/FS.java" type="org.eclipse.jgit.util.FS$Attributes">
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="getLastModifiedInstant()"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/util/FS.java" type="org.eclipse.jgit.util.FS$FileStoreAttributes">
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="FileStoreAttributes"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/util/References.java" type="org.eclipse.jgit.util.References">
+        <filter id="1108344834">
+            <message_arguments>
+                <message_argument value="5.4"/>
+                <message_argument value="5.5"/>
+                <message_argument value="org.eclipse.jgit.util.References"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/util/SimpleLruCache.java" type="org.eclipse.jgit.util.SimpleLruCache">
+        <filter id="1109393411">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="org.eclipse.jgit.util.SimpleLruCache"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/util/Stats.java" type="org.eclipse.jgit.util.Stats">
+        <filter id="1109393411">
+            <message_arguments>
+                <message_argument value="5.1.9"/>
+                <message_argument value="org.eclipse.jgit.util.Stats"/>
+            </message_arguments>
+        </filter>
+    </resource>
+</component>
diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml
index 3ea730b..cd187bf 100644
--- a/org.eclipse.jgit/pom.xml
+++ b/org.eclipse.jgit/pom.xml
@@ -223,6 +223,7 @@
         <plugin>
           <groupId>com.github.spotbugs</groupId>
           <artifactId>spotbugs-maven-plugin</artifactId>
+          <version>${spotbugs-maven-plugin-version}</version>
           <configuration>
             <excludeFilterFile>findBugs/FindBugsExcludeFilter.xml</excludeFilterFile>
           </configuration>
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index b610deb..e26163e 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -105,6 +105,7 @@
 cannotReadTree=Cannot read tree {0}
 cannotRebaseWithoutCurrentHead=Can not rebase without a current HEAD
 cannotResolveLocalTrackingRefForUpdating=Cannot resolve local tracking ref {0} for updating.
+cannotSaveConfig=Cannot save config file ''{0}''
 cannotSquashFixupWithoutPreviousCommit=Cannot {0} without previous commit.
 cannotStoreObjects=cannot store objects
 cannotResolveUniquelyAbbrevObjectId=Could not resolve uniquely the abbreviated object ID
@@ -406,6 +407,7 @@
 invalidPathPeriodAtEndWindows=Invalid path (period at end is ignored by Windows): {0}
 invalidPathSpaceAtEndWindows=Invalid path (space at end is ignored by Windows): {0}
 invalidPathReservedOnWindows=Invalid path (''{0}'' is reserved on Windows): {1}
+invalidPurgeFactor=Invalid purgeFactor {0}, values have to be in range between 0 and 1
 invalidRedirectLocation=Invalid redirect location {0} -> {1}
 invalidRefAdvertisementLine=Invalid ref advertisement line: ''{1}''
 invalidReflogRevision=Invalid reflog revision: {0}
@@ -577,8 +579,10 @@
 pushNotPermitted=push not permitted
 pushOptionsNotSupported=Push options not supported; received {0}
 rawLogMessageDoesNotParseAsLogEntry=Raw log message does not parse as log entry
+readConfigFailed=Reading config file ''{0}'' failed
 readerIsRequired=Reader is required
 readingObjectsFromLocalRepositoryFailed=reading objects from local repository failed: {0}
+readLastModifiedFailed=Reading lastModified of {0} failed
 readTimedOut=Read timed out after {0} ms
 receivePackObjectTooLarge1=Object too large, rejecting the pack. Max object size limit is {0} bytes.
 receivePackObjectTooLarge2=Object too large ({0} bytes), rejecting the pack. Max object size limit is {1} bytes.
@@ -701,6 +705,7 @@
 threadInterruptedWhileRunning="Current thread interrupted while running {0}"
 timeIsUncertain=Time is uncertain
 timerAlreadyTerminated=Timer already terminated
+timeoutMeasureFsTimestampResolution=measuring filesystem timestamp resolution for ''{0}'' timed out, fall back to resolution of 2 seconds
 tooManyCommands=Too many commands
 tooManyFilters=Too many "filter" lines in request
 tooManyIncludeRecursions=Too many recursions; circular includes in config file(s)?
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
index f0408ab..a2cd4ae 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
@@ -50,6 +50,7 @@
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.time.Instant;
 import java.util.Collection;
 import java.util.LinkedList;
 
@@ -228,7 +229,7 @@
 
 				if (GITLINK != mode) {
 					entry.setLength(f.getEntryLength());
-					entry.setLastModified(f.getEntryLastModified());
+					entry.setLastModified(f.getEntryLastModifiedInstant());
 					long len = f.getEntryContentLength();
 					// We read and filter the content multiple times.
 					// f.getEntryContentLength() reads and filters the input and
@@ -241,7 +242,7 @@
 					}
 				} else {
 					entry.setLength(0);
-					entry.setLastModified(0);
+					entry.setLastModified(Instant.ofEpochSecond(0));
 					entry.setObjectId(f.getEntryObjectId());
 				}
 				builder.add(entry);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java
index 27bb5a9..3f7306b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java
@@ -56,13 +56,18 @@
 
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.MutableObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
@@ -375,13 +380,15 @@
 				MutableObjectId idBuf = new MutableObjectId();
 				ObjectReader reader = walk.getObjectReader();
 
-				walk.reset(rw.parseTree(tree));
-				if (!paths.isEmpty())
+				RevObject o = rw.peel(rw.parseAny(tree));
+				walk.reset(getTree(o));
+				if (!paths.isEmpty()) {
 					walk.setFilter(PathFilterGroup.createFromStrings(paths));
+				}
 
 				// Put base directory into archive
 				if (pfx.endsWith("/")) { //$NON-NLS-1$
-					fmt.putEntry(outa, tree, pfx.replaceAll("[/]+$", "/"), //$NON-NLS-1$ //$NON-NLS-2$
+					fmt.putEntry(outa, o, pfx.replaceAll("[/]+$", "/"), //$NON-NLS-1$ //$NON-NLS-2$
 							FileMode.TREE, null);
 				}
 
@@ -392,17 +399,18 @@
 					if (walk.isSubtree())
 						walk.enterSubtree();
 
-					if (mode == FileMode.GITLINK)
+					if (mode == FileMode.GITLINK) {
 						// TODO(jrn): Take a callback to recurse
 						// into submodules.
 						mode = FileMode.TREE;
+					}
 
 					if (mode == FileMode.TREE) {
-						fmt.putEntry(outa, tree, name + "/", mode, null); //$NON-NLS-1$
+						fmt.putEntry(outa, o, name + "/", mode, null); //$NON-NLS-1$
 						continue;
 					}
 					walk.getObjectId(idBuf, 0);
-					fmt.putEntry(outa, tree, name, mode, reader.open(idBuf));
+					fmt.putEntry(outa, o, name, mode, reader.open(idBuf));
 				}
 				return out;
 			} finally {
@@ -534,4 +542,19 @@
 		this.paths = Arrays.asList(paths);
 		return this;
 	}
+
+	private RevTree getTree(RevObject o)
+			throws IncorrectObjectTypeException {
+		final RevTree t;
+		if (o instanceof RevCommit) {
+			t = ((RevCommit) o).getTree();
+		} else if (!(o instanceof RevTree)) {
+			throw new IncorrectObjectTypeException(tree.toObjectId(),
+					Constants.TYPE_TREE);
+		} else {
+			t = (RevTree) o;
+		}
+		return t;
+	}
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
index 65b72f7..c9dd547 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
@@ -157,8 +157,8 @@
 				merger.setCommitNames(new String[] { "BASE", ourName, //$NON-NLS-1$
 						cherryPickName });
 				if (merger.merge(newHead, srcCommit)) {
-					if (AnyObjectId.equals(newHead.getTree().getId(), merger
-							.getResultTreeId()))
+					if (AnyObjectId.isEqual(newHead.getTree().getId(),
+							merger.getResultTreeId()))
 						continue;
 					DirCacheCheckout dco = new DirCacheCheckout(repo,
 							newHead.getTree(), repo.lockDirCache(),
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
index 8671800..b55987e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
@@ -413,7 +413,7 @@
 						final DirCacheEntry dcEntry = new DirCacheEntry(path);
 						long entryLength = fTree.getEntryLength();
 						dcEntry.setLength(entryLength);
-						dcEntry.setLastModified(fTree.getEntryLastModified());
+						dcEntry.setLastModified(fTree.getEntryLastModifiedInstant());
 						dcEntry.setFileMode(fTree.getIndexFileMode(dcTree));
 
 						boolean objectExists = (dcTree != null
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
index 593874c..0dacd4d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -575,7 +575,7 @@
 			ObjectId headId = getHead().getObjectId();
 			// getHead() checks for null
 			assert headId != null;
-			if (!AnyObjectId.equals(headId, newParents.get(0)))
+			if (!AnyObjectId.isEqual(headId, newParents.get(0)))
 				checkoutCommit(headId.getName(), newParents.get(0));
 
 			// Use the cherry-pick strategy if all non-first parents did not
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
index 13ce4e7..d7c9ad5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
@@ -422,7 +422,7 @@
 						DirCacheIterator.class);
 				if (dcIter != null && dcIter.idEqual(cIter)) {
 					DirCacheEntry indexEntry = dcIter.getDirCacheEntry();
-					entry.setLastModified(indexEntry.getLastModified());
+					entry.setLastModified(indexEntry.getLastModifiedInstant());
 					entry.setLength(indexEntry.getLength());
 				}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java
index 46e0df7..ddd60b6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java
@@ -175,8 +175,8 @@
 						+ "This reverts commit " + srcCommit.getId().getName() //$NON-NLS-1$
 						+ ".\n"; //$NON-NLS-1$
 				if (merger.merge(headCommit, srcParent)) {
-					if (AnyObjectId.equals(headCommit.getTree().getId(), merger
-							.getResultTreeId()))
+					if (AnyObjectId.isEqual(headCommit.getTree().getId(),
+							merger.getResultTreeId()))
 						continue;
 					DirCacheCheckout dco = new DirCacheCheckout(repo,
 							headCommit.getTree(), repo.lockDirCache(),
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
index 2136e51..aeb9395 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
@@ -362,7 +362,7 @@
 						DirCacheIterator.class);
 				if (dcIter != null && dcIter.idEqual(cIter)) {
 					DirCacheEntry indexEntry = dcIter.getDirCacheEntry();
-					entry.setLastModified(indexEntry.getLastModified());
+					entry.setLastModified(indexEntry.getLastModifiedInstant());
 					entry.setLength(indexEntry.getLength());
 				}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java
index c32890d..0d010ad 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java
@@ -300,7 +300,8 @@
 						final DirCacheEntry entry = new DirCacheEntry(
 								treeWalk.getRawPath());
 						entry.setLength(wtIter.getEntryLength());
-						entry.setLastModified(wtIter.getEntryLastModified());
+						entry.setLastModified(
+								wtIter.getEntryLastModifiedInstant());
 						entry.setFileMode(wtIter.getEntryFileMode());
 						long contentLength = wtIter.getEntryContentLength();
 						try (InputStream in = wtIter.openEntryStream()) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
index eeab03d..95e1d21 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
@@ -58,6 +58,7 @@
 import java.security.DigestOutputStream;
 import java.security.MessageDigest;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
@@ -489,8 +490,7 @@
 			throw new CorruptObjectException(JGitText.get().DIRCHasTooManyEntries);
 
 		snapshot = FileSnapshot.save(liveFile);
-		int smudge_s = (int) (snapshot.lastModified() / 1000);
-		int smudge_ns = ((int) (snapshot.lastModified() % 1000)) * 1000000;
+		Instant smudge = snapshot.lastModifiedInstant();
 
 		// Load the individual file entries.
 		//
@@ -499,8 +499,9 @@
 		sortedEntries = new DirCacheEntry[entryCnt];
 
 		final MutableInteger infoAt = new MutableInteger();
-		for (int i = 0; i < entryCnt; i++)
-			sortedEntries[i] = new DirCacheEntry(infos, infoAt, in, md, smudge_s, smudge_ns);
+		for (int i = 0; i < entryCnt; i++) {
+			sortedEntries[i] = new DirCacheEntry(infos, infoAt, in, md, smudge);
+		}
 
 		// After the file entries are index extensions, and then a footer.
 		//
@@ -665,8 +666,8 @@
 			// so we use the current timestamp as a approximation.
 			myLock.createCommitSnapshot();
 			snapshot = myLock.getCommitSnapshot();
-			smudge_s = (int) (snapshot.lastModified() / 1000);
-			smudge_ns = ((int) (snapshot.lastModified() % 1000)) * 1000000;
+			smudge_s = (int) (snapshot.lastModifiedInstant().getEpochSecond());
+			smudge_ns = snapshot.lastModifiedInstant().getNano();
 		} else {
 			// Used in unit tests only
 			smudge_ns = 0;
@@ -1011,7 +1012,7 @@
 				DirCacheEntry entry = iIter.getDirCacheEntry();
 				if (entry.isSmudged() && iIter.idEqual(fIter)) {
 					entry.setLength(fIter.getEntryLength());
-					entry.setLastModified(fIter.getEntryLastModified());
+					entry.setLastModified(fIter.getEntryLastModifiedInstant());
 				}
 			}
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
index 3b2e74e..6bc2946 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -50,6 +50,7 @@
 import java.io.OutputStream;
 import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -427,8 +428,10 @@
 					// update the timestamp of the index with the one from the
 					// file if not set, as we are sure to be in sync here.
 					DirCacheEntry entry = i.getDirCacheEntry();
-					if (entry.getLastModified() == 0)
-						entry.setLastModified(f.getEntryLastModified());
+					Instant mtime = entry.getLastModifiedInstant();
+					if (mtime == null || mtime.equals(Instant.EPOCH)) {
+						entry.setLastModified(f.getEntryLastModifiedInstant());
+					}
 					keep(entry, f);
 				}
 			} else
@@ -640,7 +643,7 @@
 		File gitlinkDir = new File(repo.getWorkTree(), path);
 		FileUtils.mkdirs(gitlinkDir, true);
 		FS fs = repo.getFS();
-		entry.setLastModified(fs.lastModified(gitlinkDir));
+		entry.setLastModified(fs.lastModifiedInstant(gitlinkDir));
 	}
 
 	private static ArrayList<String> filterOut(ArrayList<String> strings,
@@ -1477,7 +1480,7 @@
 			}
 			fs.createSymLink(f, target);
 			entry.setLength(bytes.length);
-			entry.setLastModified(fs.lastModified(f));
+			entry.setLastModified(fs.lastModifiedInstant(f));
 			return;
 		}
 
@@ -1546,7 +1549,7 @@
 				FileUtils.delete(tmpFile);
 			}
 		}
-		entry.setLastModified(fs.lastModified(f));
+		entry.setLastModified(fs.lastModifiedInstant(f));
 	}
 
 	// Run an external filter command
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
index 6b1d4f4..d4db15c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
@@ -56,6 +56,7 @@
 import java.nio.ByteBuffer;
 import java.security.MessageDigest;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.Arrays;
 
 import org.eclipse.jgit.errors.CorruptObjectException;
@@ -145,8 +146,8 @@
 	private byte inCoreFlags;
 
 	DirCacheEntry(final byte[] sharedInfo, final MutableInteger infoAt,
-			final InputStream in, final MessageDigest md, final int smudge_s,
-			final int smudge_ns) throws IOException {
+			final InputStream in, final MessageDigest md, final Instant smudge)
+			throws IOException {
 		info = sharedInfo;
 		infoOffset = infoAt.value;
 
@@ -215,8 +216,9 @@
 			md.update(nullpad, 0, padLen);
 		}
 
-		if (mightBeRacilyClean(smudge_s, smudge_ns))
+		if (mightBeRacilyClean(smudge)) {
 			smudgeRacilyClean();
+		}
 	}
 
 	/**
@@ -344,8 +346,29 @@
 	 * @param smudge_ns
 	 *            nanoseconds component of the index's last modified time.
 	 * @return true if extra careful checks should be used.
+	 * @deprecated use {@link #mightBeRacilyClean(Instant)} instead
 	 */
+	@Deprecated
 	public final boolean mightBeRacilyClean(int smudge_s, int smudge_ns) {
+		return mightBeRacilyClean(Instant.ofEpochSecond(smudge_s, smudge_ns));
+	}
+
+	/**
+	 * Is it possible for this entry to be accidentally assumed clean?
+	 * <p>
+	 * The "racy git" problem happens when a work file can be updated faster
+	 * than the filesystem records file modification timestamps. It is possible
+	 * for an application to edit a work file, update the index, then edit it
+	 * again before the filesystem will give the work file a new modification
+	 * timestamp. This method tests to see if file was written out at the same
+	 * time as the index.
+	 *
+	 * @param smudge
+	 *            index's last modified time.
+	 * @return true if extra careful checks should be used.
+	 * @since 5.1.9
+	 */
+	public final boolean mightBeRacilyClean(Instant smudge) {
 		// If the index has a modification time then it came from disk
 		// and was not generated from scratch in memory. In such cases
 		// the entry is 'racily clean' if the entry's cached modification
@@ -355,8 +378,9 @@
 		//
 		final int base = infoOffset + P_MTIME;
 		final int mtime = NB.decodeInt32(info, base);
-		if (smudge_s == mtime)
-			return smudge_ns <= NB.decodeInt32(info, base + 4);
+		if (smudge.getEpochSecond() == mtime) {
+			return smudge.getNano() <= NB.decodeInt32(info, base + 4);
+		}
 		return false;
 	}
 
@@ -424,9 +448,9 @@
 	 */
 	public void setAssumeValid(boolean assume) {
 		if (assume)
-			info[infoOffset + P_FLAGS] |= ASSUME_VALID;
+			info[infoOffset + P_FLAGS] |= (byte) ASSUME_VALID;
 		else
-			info[infoOffset + P_FLAGS] &= ~ASSUME_VALID;
+			info[infoOffset + P_FLAGS] &= (byte) ~ASSUME_VALID;
 	}
 
 	/**
@@ -446,9 +470,9 @@
 	 */
 	public void setUpdateNeeded(boolean updateNeeded) {
 		if (updateNeeded)
-			inCoreFlags |= UPDATE_NEEDED;
+			inCoreFlags |= (byte) UPDATE_NEEDED;
 		else
-			inCoreFlags &= ~UPDATE_NEEDED;
+			inCoreFlags &= (byte) ~UPDATE_NEEDED;
 	}
 
 	/**
@@ -563,22 +587,51 @@
 	 *
 	 * @return last modification time of this file, in milliseconds since the
 	 *         Java epoch (midnight Jan 1, 1970 UTC).
+	 * @deprecated use {@link #getLastModifiedInstant()} instead
 	 */
+	@Deprecated
 	public long getLastModified() {
 		return decodeTS(P_MTIME);
 	}
 
 	/**
+	 * Get the cached last modification date of this file.
+	 * <p>
+	 * One of the indicators that the file has been modified by an application
+	 * changing the working tree is if the last modification time for the file
+	 * differs from the time stored in this entry.
+	 *
+	 * @return last modification time of this file.
+	 * @since 5.1.9
+	 */
+	public Instant getLastModifiedInstant() {
+		return decodeTSInstant(P_MTIME);
+	}
+
+	/**
 	 * Set the cached last modification date of this file, using milliseconds.
 	 *
 	 * @param when
 	 *            new cached modification date of the file, in milliseconds.
+	 * @deprecated use {@link #setLastModified(Instant)} instead
 	 */
+	@Deprecated
 	public void setLastModified(long when) {
 		encodeTS(P_MTIME, when);
 	}
 
 	/**
+	 * Set the cached last modification date of this file.
+	 *
+	 * @param when
+	 *            new cached modification date of the file.
+	 * @since 5.1.9
+	 */
+	public void setLastModified(Instant when) {
+		encodeTS(P_MTIME, when);
+	}
+
+	/**
 	 * Get the cached size (mod 4 GB) (in bytes) of this file.
 	 * <p>
 	 * One of the indicators that the file has been modified by an application
@@ -692,7 +745,8 @@
 	@SuppressWarnings("nls")
 	@Override
 	public String toString() {
-		return getFileMode() + " " + getLength() + " " + getLastModified()
+		return getFileMode() + " " + getLength() + " "
+				+ getLastModifiedInstant()
 				+ " " + getObjectId() + " " + getStage() + " "
 				+ getPathString() + "\n";
 	}
@@ -750,12 +804,25 @@
 		return 1000L * sec + ms;
 	}
 
+	private Instant decodeTSInstant(int pIdx) {
+		final int base = infoOffset + pIdx;
+		final int sec = NB.decodeInt32(info, base);
+		final int nano = NB.decodeInt32(info, base + 4);
+		return Instant.ofEpochSecond(sec, nano);
+	}
+
 	private void encodeTS(int pIdx, long when) {
 		final int base = infoOffset + pIdx;
 		NB.encodeInt32(info, base, (int) (when / 1000));
 		NB.encodeInt32(info, base + 4, ((int) (when % 1000)) * 1000000);
 	}
 
+	private void encodeTS(int pIdx, Instant when) {
+		final int base = infoOffset + pIdx;
+		NB.encodeInt32(info, base, (int) when.getEpochSecond());
+		NB.encodeInt32(info, base + 4, when.getNano());
+	}
+
 	private int getExtendedFlags() {
 		if (isExtended())
 			return NB.decodeUInt16(info, infoOffset + P_FLAGS2) << 16;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/IncorrectObjectTypeException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/IncorrectObjectTypeException.java
index 5abd0c3..15fd803 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/IncorrectObjectTypeException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/IncorrectObjectTypeException.java
@@ -63,7 +63,7 @@
 	private static final long serialVersionUID = 1L;
 
 	/**
-	 * Construct and IncorrectObjectTypeException for the specified object id.
+	 * Construct an IncorrectObjectTypeException for the specified object id.
 	 *
 	 * Provide the type to make it easier to track down the problem.
 	 *
@@ -75,7 +75,7 @@
 	}
 
 	/**
-	 * Construct and IncorrectObjectTypeException for the specified object id.
+	 * Construct an IncorrectObjectTypeException for the specified object id.
 	 *
 	 * Provide the type to make it easier to track down the problem.
 	 *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index 572f963..320036c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -166,6 +166,7 @@
 	/***/ public String cannotReadTree;
 	/***/ public String cannotRebaseWithoutCurrentHead;
 	/***/ public String cannotResolveLocalTrackingRefForUpdating;
+	/***/ public String cannotSaveConfig;
 	/***/ public String cannotSquashFixupWithoutPreviousCommit;
 	/***/ public String cannotStoreObjects;
 	/***/ public String cannotResolveUniquelyAbbrevObjectId;
@@ -467,6 +468,7 @@
 	/***/ public String invalidPathPeriodAtEndWindows;
 	/***/ public String invalidPathSpaceAtEndWindows;
 	/***/ public String invalidPathReservedOnWindows;
+	/***/ public String invalidPurgeFactor;
 	/***/ public String invalidRedirectLocation;
 	/***/ public String invalidRefAdvertisementLine;
 	/***/ public String invalidReflogRevision;
@@ -638,8 +640,10 @@
 	/***/ public String pushNotPermitted;
 	/***/ public String pushOptionsNotSupported;
 	/***/ public String rawLogMessageDoesNotParseAsLogEntry;
+	/***/ public String readConfigFailed;
 	/***/ public String readerIsRequired;
 	/***/ public String readingObjectsFromLocalRepositoryFailed;
+	/***/ public String readLastModifiedFailed;
 	/***/ public String readTimedOut;
 	/***/ public String receivePackObjectTooLarge1;
 	/***/ public String receivePackObjectTooLarge2;
@@ -757,6 +761,7 @@
 	/***/ public String tagAlreadyExists;
 	/***/ public String tagNameInvalid;
 	/***/ public String tagOnRepoWithoutHEADCurrentlyNotSupported;
+	/***/ public String timeoutMeasureFsTimestampResolution;
 	/***/ public String transactionAborted;
 	/***/ public String theFactoryMustNotBeNull;
 	/***/ public String threadInterruptedWhileRunning;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java
index a0176d7..0e8377d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java
@@ -345,7 +345,7 @@
 	}
 
 	private static boolean equals(@Nullable ObjectId a, LogIndex b) {
-		return a != null && b != null && AnyObjectId.equals(a, b);
+		return a != null && b != null && AnyObjectId.isEqual(a, b);
 	}
 
 	/**
@@ -749,7 +749,7 @@
 				Ref oldRef = remote.remove(name);
 				ObjectId oldId = getId(oldRef);
 				ObjectId newId = tw.getObjectId(0);
-				if (!AnyObjectId.equals(oldId, newId)) {
+				if (!AnyObjectId.isEqual(oldId, newId)) {
 					delta.add(new ReceiveCommand(oldId, newId, name));
 				}
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java
index c09d872..53fd198 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java
@@ -106,7 +106,7 @@
 			return UNKNOWN;
 		}
 
-		if (AnyObjectId.equals(remoteId, ObjectId.zeroId())) {
+		if (AnyObjectId.isEqual(remoteId, ObjectId.zeroId())) {
 			// Replica does not have the txnAccepted reference.
 			return LAGGING;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java
index 3c9b187..4bed575 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java
@@ -200,7 +200,7 @@
 	private static boolean isExpectedValue(Map<String, Ref> adv,
 			RemoteRefUpdate u) {
 		Ref r = adv.get(u.getRemoteName());
-		if (!AnyObjectId.equals(getId(r), u.getExpectedOldObjectId())) {
+		if (!AnyObjectId.isEqual(getId(r), u.getExpectedOldObjectId())) {
 			((RemoteCommand) u).cmd.setResult(LOCK_FAILURE);
 			return false;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java
index ae82dce..815984d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java
@@ -138,7 +138,7 @@
 		try (RevWalk rw = new RevWalk(git);
 				TreeWalk tw = new TreeWalk(rw.getObjectReader());
 				ObjectInserter ins = git.newObjectInserter()) {
-			if (AnyObjectId.equals(oldTree, ObjectId.zeroId())) {
+			if (AnyObjectId.isEqual(oldTree, ObjectId.zeroId())) {
 				tw.addTree(new EmptyTreeIterator());
 			} else {
 				tw.addTree(rw.parseTree(oldTree));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
index ca11fb9..f10a1d8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
@@ -661,7 +661,7 @@
 	private int objectsBefore() {
 		int cnt = 0;
 		for (DfsPackFile p : packsBefore)
-			cnt += p.getPackDescription().getObjectCount();
+			cnt += (int) p.getPackDescription().getObjectCount();
 		return cnt;
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackParser.java
index 45e3b19..e4c37cb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackParser.java
@@ -432,7 +432,7 @@
 		buf[len++] = (byte) ((typeCode << 4) | (sz & 15));
 		sz >>>= 4;
 		while (sz > 0) {
-			buf[len - 1] |= 0x80;
+			buf[len - 1] |= (byte) 0x80;
 			buf[len++] = (byte) (sz & 0x7f);
 			sz >>>= 7;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReftableBatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReftableBatchRefUpdate.java
index 47ac4ec..07fd00f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReftableBatchRefUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReftableBatchRefUpdate.java
@@ -252,7 +252,7 @@
 
 	private static boolean matchOld(ReceiveCommand cmd, @Nullable Ref ref) {
 		if (ref == null) {
-			return AnyObjectId.equals(ObjectId.zeroId(), cmd.getOldId())
+			return AnyObjectId.isEqual(ObjectId.zeroId(), cmd.getOldId())
 					&& cmd.getOldSymref() == null;
 		} else if (ref.isSymbolic()) {
 			return ref.getTarget().getName().equals(cmd.getOldSymref());
@@ -368,7 +368,7 @@
 			String name = cmd.getRefName();
 			ObjectId newId = cmd.getNewId();
 			String newSymref = cmd.getNewSymref();
-			if (AnyObjectId.equals(ObjectId.zeroId(), newId)
+			if (AnyObjectId.isEqual(ObjectId.zeroId(), newId)
 					&& newSymref == null) {
 				refs.add(new ObjectIdRef.Unpeeled(NEW, name, null));
 				continue;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java
index 1de3135..8650ebf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java
@@ -43,19 +43,24 @@
 
 package org.eclipse.jgit.internal.storage.file;
 
+import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_FILESTORE_ATTRIBUTES;
+import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_TIMESTAMP_RESOLUTION;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.attribute.BasicFileAttributes;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
 import java.time.Duration;
-import java.util.Date;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.FileStoreAttributes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Caches when a file was last read, making it possible to detect future edits.
@@ -74,6 +79,8 @@
  * file is less than 3 seconds ago.
  */
 public class FileSnapshot {
+	private static final Logger LOG = LoggerFactory
+			.getLogger(FileSnapshot.class);
 	/**
 	 * An unknown file size.
 	 *
@@ -81,8 +88,14 @@
 	 */
 	public static final long UNKNOWN_SIZE = -1;
 
+	private static final Instant UNKNOWN_TIME = Instant.ofEpochMilli(-1);
+
 	private static final Object MISSING_FILEKEY = new Object();
 
+	private static final DateTimeFormatter dateFmt = DateTimeFormatter
+			.ofPattern("yyyy-MM-dd HH:mm:ss.nnnnnnnnn") //$NON-NLS-1$
+			.withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault());
+
 	/**
 	 * A FileSnapshot that is considered to always be modified.
 	 * <p>
@@ -90,8 +103,8 @@
 	 * file, but only after {@link #isModified(File)} gets invoked. The returned
 	 * snapshot contains only invalid status information.
 	 */
-	public static final FileSnapshot DIRTY = new FileSnapshot(-1, -1,
-			UNKNOWN_SIZE, Duration.ZERO, MISSING_FILEKEY);
+	public static final FileSnapshot DIRTY = new FileSnapshot(UNKNOWN_TIME,
+			UNKNOWN_TIME, UNKNOWN_SIZE, Duration.ZERO, MISSING_FILEKEY);
 
 	/**
 	 * A FileSnapshot that is clean if the file does not exist.
@@ -100,8 +113,8 @@
 	 * file to be clean. {@link #isModified(File)} will return false if the file
 	 * path does not exist.
 	 */
-	public static final FileSnapshot MISSING_FILE = new FileSnapshot(0, 0, 0,
-			Duration.ZERO, MISSING_FILEKEY) {
+	public static final FileSnapshot MISSING_FILE = new FileSnapshot(
+			Instant.EPOCH, Instant.EPOCH, 0, Duration.ZERO, MISSING_FILEKEY) {
 		@Override
 		public boolean isModified(File path) {
 			return FS.DETECTED.exists(path);
@@ -122,6 +135,22 @@
 		return new FileSnapshot(path);
 	}
 
+	/**
+	 * Record a snapshot for a specific file path without using config file to
+	 * get filesystem timestamp resolution.
+	 * <p>
+	 * This method should be invoked before the file is accessed. It is used by
+	 * FileBasedConfig to avoid endless recursion.
+	 *
+	 * @param path
+	 *            the path to later remember. The path's current status
+	 *            information is saved.
+	 * @return the snapshot.
+	 */
+	public static FileSnapshot saveNoConfig(File path) {
+		return new FileSnapshot(path, false);
+	}
+
 	private static Object getFileKey(BasicFileAttributes fileAttributes) {
 		Object fileKey = fileAttributes.fileKey();
 		return fileKey == null ? MISSING_FILEKEY : fileKey;
@@ -141,18 +170,41 @@
 	 * @param modified
 	 *            the last modification time of the file
 	 * @return the snapshot.
+	 * @deprecated use {@link #save(Instant)} instead.
 	 */
+	@Deprecated
 	public static FileSnapshot save(long modified) {
-		final long read = System.currentTimeMillis();
-		return new FileSnapshot(read, modified, -1, Duration.ZERO,
-				MISSING_FILEKEY);
+		final Instant read = Instant.now();
+		return new FileSnapshot(read, Instant.ofEpochMilli(modified),
+				UNKNOWN_SIZE, FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY);
+	}
+
+	/**
+	 * Record a snapshot for a file for which the last modification time is
+	 * already known.
+	 * <p>
+	 * This method should be invoked before the file is accessed.
+	 * <p>
+	 * Note that this method cannot rely on measuring file timestamp resolution
+	 * to avoid racy git issues caused by finite file timestamp resolution since
+	 * it's unknown in which filesystem the file is located. Hence the worst
+	 * case fallback for timestamp resolution is used.
+	 *
+	 * @param modified
+	 *            the last modification time of the file
+	 * @return the snapshot.
+	 */
+	public static FileSnapshot save(Instant modified) {
+		final Instant read = Instant.now();
+		return new FileSnapshot(read, modified, UNKNOWN_SIZE,
+				FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY);
 	}
 
 	/** Last observed modification time of the path. */
-	private final long lastModified;
+	private final Instant lastModified;
 
 	/** Last wall-clock time the path was read. */
-	private volatile long lastRead;
+	private volatile Instant lastRead;
 
 	/** True once {@link #lastRead} is far later than {@link #lastModified}. */
 	private boolean cannotBeRacilyClean;
@@ -162,8 +214,8 @@
 	 * When set to {@link #UNKNOWN_SIZE} the size is not considered for modification checks. */
 	private final long size;
 
-	/** measured filesystem timestamp resolution */
-	private Duration fsTimestampResolution;
+	/** measured FileStore attributes */
+	private FileStoreAttributes fileStoreAttributeCache;
 
 	/**
 	 * Object that uniquely identifies the given file, or {@code
@@ -171,31 +223,57 @@
 	 */
 	private final Object fileKey;
 
+	private final File file;
+
 	/**
 	 * Record a snapshot for a specific file path.
 	 * <p>
 	 * This method should be invoked before the file is accessed.
 	 *
-	 * @param path
-	 *            the path to later remember. The path's current status
+	 * @param file
+	 *            the path to remember meta data for. The path's current status
 	 *            information is saved.
 	 */
-	protected FileSnapshot(File path) {
-		this.lastRead = System.currentTimeMillis();
-		this.fsTimestampResolution = FS
-				.getFsTimerResolution(path.toPath().getParent());
+	protected FileSnapshot(File file) {
+		this(file, true);
+	}
+
+	/**
+	 * Record a snapshot for a specific file path.
+	 * <p>
+	 * This method should be invoked before the file is accessed.
+	 *
+	 * @param file
+	 *            the path to remember meta data for. The path's current status
+	 *            information is saved.
+	 * @param useConfig
+	 *            if {@code true} read filesystem time resolution from
+	 *            configuration file otherwise use fallback resolution
+	 */
+	protected FileSnapshot(File file, boolean useConfig) {
+		this.file = file;
+		this.lastRead = Instant.now();
+		this.fileStoreAttributeCache = useConfig
+				? FS.getFileStoreAttributes(file.toPath().getParent())
+				: FALLBACK_FILESTORE_ATTRIBUTES;
 		BasicFileAttributes fileAttributes = null;
 		try {
-			fileAttributes = FS.DETECTED.fileAttributes(path);
+			fileAttributes = FS.DETECTED.fileAttributes(file);
 		} catch (IOException e) {
-			this.lastModified = path.lastModified();
-			this.size = path.length();
+			this.lastModified = Instant.ofEpochMilli(file.lastModified());
+			this.size = file.length();
 			this.fileKey = MISSING_FILEKEY;
 			return;
 		}
-		this.lastModified = fileAttributes.lastModifiedTime().toMillis();
+		this.lastModified = fileAttributes.lastModifiedTime().toInstant();
 		this.size = fileAttributes.size();
 		this.fileKey = getFileKey(fileAttributes);
+		if (LOG.isDebugEnabled()) {
+			LOG.debug("file={}, create new FileSnapshot: lastRead={}, lastModified={}, size={}, fileKey={}", //$NON-NLS-1$
+					file, dateFmt.format(lastRead),
+					dateFmt.format(lastModified), Long.valueOf(size),
+					fileKey.toString());
+		}
 	}
 
 	private boolean sizeChanged;
@@ -206,11 +284,17 @@
 
 	private boolean wasRacyClean;
 
-	private FileSnapshot(long read, long modified, long size,
+	private long delta;
+
+	private long racyThreshold;
+
+	private FileSnapshot(Instant read, Instant modified, long size,
 			@NonNull Duration fsTimestampResolution, @NonNull Object fileKey) {
+		this.file = null;
 		this.lastRead = read;
 		this.lastModified = modified;
-		this.fsTimestampResolution = fsTimestampResolution;
+		this.fileStoreAttributeCache = new FileStoreAttributes(
+				fsTimestampResolution);
 		this.size = size;
 		this.fileKey = fileKey;
 	}
@@ -219,8 +303,19 @@
 	 * Get time of last snapshot update
 	 *
 	 * @return time of last snapshot update
+	 * @deprecated use {@link #lastModifiedInstant()} instead
 	 */
+	@Deprecated
 	public long lastModified() {
+		return lastModified.toEpochMilli();
+	}
+
+	/**
+	 * Get time of last snapshot update
+	 *
+	 * @return time of last snapshot update
+	 */
+	public Instant lastModifiedInstant() {
 		return lastModified;
 	}
 
@@ -239,16 +334,16 @@
 	 * @return true if the path needs to be read again.
 	 */
 	public boolean isModified(File path) {
-		long currLastModified;
+		Instant currLastModified;
 		long currSize;
 		Object currFileKey;
 		try {
 			BasicFileAttributes fileAttributes = FS.DETECTED.fileAttributes(path);
-			currLastModified = fileAttributes.lastModifiedTime().toMillis();
+			currLastModified = fileAttributes.lastModifiedTime().toInstant();
 			currSize = fileAttributes.size();
 			currFileKey = getFileKey(fileAttributes);
 		} catch (IOException e) {
-			currLastModified = path.lastModified();
+			currLastModified = Instant.ofEpochMilli(path.lastModified());
 			currSize = path.length();
 			currFileKey = MISSING_FILEKEY;
 		}
@@ -290,7 +385,7 @@
 	 *            the other snapshot.
 	 */
 	public void setClean(FileSnapshot other) {
-		final long now = other.lastRead;
+		final Instant now = other.lastRead;
 		if (!isRacyClean(now)) {
 			cannotBeRacilyClean = true;
 		}
@@ -304,9 +399,10 @@
 	 *             if sleep was interrupted
 	 */
 	public void waitUntilNotRacy() throws InterruptedException {
-		while (isRacyClean(System.currentTimeMillis())) {
-			TimeUnit.NANOSECONDS
-					.sleep((fsTimestampResolution.toNanos() + 1) * 11 / 10);
+		long timestampResolution = fileStoreAttributeCache
+				.getFsTimestampResolution().toNanos();
+		while (isRacyClean(Instant.now())) {
+			TimeUnit.NANOSECONDS.sleep(timestampResolution);
 		}
 	}
 
@@ -317,8 +413,10 @@
 	 *            the other snapshot.
 	 * @return true if the two snapshots share the same information.
 	 */
+	@SuppressWarnings("NonOverridingEquals")
 	public boolean equals(FileSnapshot other) {
-		return lastModified == other.lastModified && size == other.size
+		boolean sizeEq = size == UNKNOWN_SIZE || other.size == UNKNOWN_SIZE || size == other.size;
+		return lastModified.equals(other.lastModified) && sizeEq
 				&& Objects.equals(fileKey, other.fileKey);
 	}
 
@@ -341,8 +439,7 @@
 	/** {@inheritDoc} */
 	@Override
 	public int hashCode() {
-		return Objects.hash(Long.valueOf(lastModified), Long.valueOf(size),
-				fileKey);
+		return Objects.hash(lastModified, Long.valueOf(size), fileKey);
 	}
 
 	/**
@@ -377,8 +474,24 @@
 		return wasRacyClean;
 	}
 
+	/**
+	 * @return the delta in nanoseconds between lastModified and lastRead during
+	 *         last racy check
+	 */
+	public long lastDelta() {
+		return delta;
+	}
+
+	/**
+	 * @return the racyLimitNanos threshold in nanoseconds during last racy
+	 *         check
+	 */
+	public long lastRacyThreshold() {
+		return racyThreshold;
+	}
+
 	/** {@inheritDoc} */
-	@SuppressWarnings("nls")
+	@SuppressWarnings({ "nls", "ReferenceEquality" })
 	@Override
 	public String toString() {
 		if (this == DIRTY) {
@@ -387,24 +500,46 @@
 		if (this == MISSING_FILE) {
 			return "MISSING_FILE";
 		}
-		DateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS",
-				Locale.US);
-		return "FileSnapshot[modified: " + f.format(new Date(lastModified))
-				+ ", read: " + f.format(new Date(lastRead)) + ", size:" + size
+		return "FileSnapshot[modified: " + dateFmt.format(lastModified)
+				+ ", read: " + dateFmt.format(lastRead) + ", size:" + size
 				+ ", fileKey: " + fileKey + "]";
 	}
 
-	private boolean isRacyClean(long read) {
-		// add a 10% safety margin
-		long racyNanos = (fsTimestampResolution.toNanos() + 1) * 11 / 10;
-		return wasRacyClean = (read - lastModified) * 1_000_000 <= racyNanos;
+	private boolean isRacyClean(Instant read) {
+		racyThreshold = getEffectiveRacyThreshold();
+		delta = Duration.between(lastModified, read).toNanos();
+		wasRacyClean = delta <= racyThreshold;
+		if (LOG.isDebugEnabled()) {
+			LOG.debug(
+					"file={}, isRacyClean={}, read={}, lastModified={}, delta={} ns, racy<={} ns", //$NON-NLS-1$
+					file, Boolean.valueOf(wasRacyClean), dateFmt.format(read),
+					dateFmt.format(lastModified), Long.valueOf(delta),
+					Long.valueOf(racyThreshold));
+		}
+		return wasRacyClean;
 	}
 
-	private boolean isModified(long currLastModified) {
+	private long getEffectiveRacyThreshold() {
+		long timestampResolution = fileStoreAttributeCache
+				.getFsTimestampResolution().toNanos();
+		long minRacyInterval = fileStoreAttributeCache.getMinimalRacyInterval()
+				.toNanos();
+		long max = Math.max(timestampResolution, minRacyInterval);
+		// safety margin: factor 2.5 below 100ms otherwise 1.25
+		return max < 100_000_000L ? max * 5 / 2 : max * 5 / 4;
+	}
+
+	private boolean isModified(Instant currLastModified) {
 		// Any difference indicates the path was modified.
 
-		lastModifiedChanged = lastModified != currLastModified;
+		lastModifiedChanged = !lastModified.equals(currLastModified);
 		if (lastModifiedChanged) {
+			if (LOG.isDebugEnabled()) {
+				LOG.debug(
+						"file={}, lastModified changed from {} to {}", //$NON-NLS-1$
+						file, dateFmt.format(lastModified),
+						dateFmt.format(currLastModified));
+			}
 			return true;
 		}
 
@@ -412,26 +547,40 @@
 		// after the last modification that any new modifications
 		// are certain to change the last modified time.
 		if (cannotBeRacilyClean) {
+			LOG.debug("file={}, cannot be racily clean", file); //$NON-NLS-1$
 			return false;
 		}
 		if (!isRacyClean(lastRead)) {
 			// Our last read should have marked cannotBeRacilyClean,
 			// but this thread may not have seen the change. The read
 			// of the volatile field lastRead should have fixed that.
+			LOG.debug("file={}, is unmodified", file); //$NON-NLS-1$
 			return false;
 		}
 
 		// We last read this path too close to its last observed
 		// modification time. We may have missed a modification.
 		// Scan again, to ensure we still see the same state.
+		LOG.debug("file={}, is racily clean", file); //$NON-NLS-1$
 		return true;
 	}
 
 	private boolean isFileKeyChanged(Object currFileKey) {
-		return currFileKey != MISSING_FILEKEY && !currFileKey.equals(fileKey);
+		boolean changed = currFileKey != MISSING_FILEKEY
+				&& !currFileKey.equals(fileKey);
+		if (changed) {
+			LOG.debug("file={}, FileKey changed from {} to {}", //$NON-NLS-1$
+					file, fileKey, currFileKey);
+		}
+		return changed;
 	}
 
 	private boolean isSizeChanged(long currSize) {
-		return currSize != UNKNOWN_SIZE && currSize != size;
+		boolean changed = (currSize != UNKNOWN_SIZE) && (currSize != size);
+		if (changed) {
+			LOG.debug("file={}, size changed from {} to {} bytes", //$NON-NLS-1$
+					file, Long.valueOf(size), Long.valueOf(currSize));
+		}
+		return changed;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
index 4540860..08bb6cb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
@@ -371,8 +371,9 @@
 					continue oldPackLoop;
 
 			if (!oldPack.shouldBeKept()
-					&& repo.getFS().lastModified(
-							oldPack.getPackFile()) < packExpireDate) {
+					&& repo.getFS()
+							.lastModifiedInstant(oldPack.getPackFile())
+							.toEpochMilli() < packExpireDate) {
 				oldPack.close();
 				if (shouldLoosen) {
 					loosen(inserter, reader, oldPack, ids);
@@ -560,8 +561,10 @@
 					String fName = f.getName();
 					if (fName.length() != Constants.OBJECT_ID_STRING_LENGTH - 2)
 						continue;
-					if (repo.getFS().lastModified(f) >= expireDate)
+					if (repo.getFS().lastModifiedInstant(f)
+							.toEpochMilli() >= expireDate) {
 						continue;
+					}
 					try {
 						ObjectId id = ObjectId.fromString(d + fName);
 						if (objectsToKeep.contains(id))
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
index a4fc1a2..fa8732d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
@@ -56,8 +56,12 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.Channels;
 import java.nio.channels.FileChannel;
+import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.FileTime;
 import java.text.MessageFormat;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
@@ -400,9 +404,16 @@
 	public void waitForStatChange() throws InterruptedException {
 		FileSnapshot o = FileSnapshot.save(ref);
 		FileSnapshot n = FileSnapshot.save(lck);
+		long fsTimeResolution = FS.getFileStoreAttributes(lck.toPath())
+				.getFsTimestampResolution().toNanos();
 		while (o.equals(n)) {
-			Thread.sleep(25 /* milliseconds */);
-			lck.setLastModified(System.currentTimeMillis());
+			TimeUnit.NANOSECONDS.sleep(fsTimeResolution);
+			try {
+				Files.setLastModifiedTime(lck.toPath(),
+						FileTime.from(Instant.now()));
+			} catch (IOException e) {
+				n.waitUntilNotRacy();
+			}
 			n = FileSnapshot.save(lck);
 		}
 	}
@@ -452,12 +463,23 @@
 	 * Get the modification time of the output file when it was committed.
 	 *
 	 * @return modification time of the lock file right before we committed it.
+	 * @deprecated use {@link #getCommitLastModifiedInstant()} instead
 	 */
+	@Deprecated
 	public long getCommitLastModified() {
 		return commitSnapshot.lastModified();
 	}
 
 	/**
+	 * Get the modification time of the output file when it was committed.
+	 *
+	 * @return modification time of the lock file right before we committed it.
+	 */
+	public Instant getCommitLastModifiedInstant() {
+		return commitSnapshot.lastModifiedInstant();
+	}
+
+	/**
 	 * Get the {@link FileSnapshot} just before commit.
 	 *
 	 * @return get the {@link FileSnapshot} just before commit.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
index ade7a8e..7d31673 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
@@ -356,7 +356,7 @@
 		buf[len++] = (byte) ((typeCode << 4) | (sz & 15));
 		sz >>>= 4;
 		while (sz > 0) {
-			buf[len - 1] |= 0x80;
+			buf[len - 1] |= (byte) 0x80;
 			buf[len++] = (byte) (sz & 0x7f);
 			sz >>>= 7;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
index a89e2ec..88e05af 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
@@ -60,6 +60,7 @@
 import java.nio.file.AccessDeniedException;
 import java.nio.file.NoSuchFileException;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
@@ -104,8 +105,12 @@
 public class PackFile implements Iterable<PackIndex.MutableEntry> {
 	private final static Logger LOG = LoggerFactory.getLogger(PackFile.class);
 	/** Sorts PackFiles to be most recently created to least recently created. */
-	public static final Comparator<PackFile> SORT = (PackFile a,
-			PackFile b) -> b.packLastModified - a.packLastModified;
+	public static final Comparator<PackFile> SORT = new Comparator<PackFile>() {
+		@Override
+		public int compare(PackFile a, PackFile b) {
+			return b.packLastModified.compareTo(a.packLastModified);
+		}
+	};
 
 	private final File packFile;
 
@@ -128,7 +133,7 @@
 
 	private int activeCopyRawData;
 
-	int packLastModified;
+	Instant packLastModified;
 
 	private PackFileSnapshot fileSnapshot;
 
@@ -168,7 +173,7 @@
 	public PackFile(File packFile, int extensions) {
 		this.packFile = packFile;
 		this.fileSnapshot = PackFileSnapshot.save(packFile);
-		this.packLastModified = (int) (fileSnapshot.lastModified() >> 10);
+		this.packLastModified = fileSnapshot.lastModifiedInstant();
 		this.extensions = extensions;
 
 		// Multiply by 31 here so we can more directly combine with another
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/UnpackedObjectCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/UnpackedObjectCache.java
index 967754a..ea0d269 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/UnpackedObjectCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/UnpackedObjectCache.java
@@ -112,7 +112,7 @@
 				if (obj == null)
 					break;
 
-				if (AnyObjectId.equals(obj, toFind))
+				if (AnyObjectId.isEqual(obj, toFind))
 					return true;
 
 				if (++i == ids.length())
@@ -132,7 +132,7 @@
 						continue;
 				}
 
-				if (AnyObjectId.equals(obj, toAdd))
+				if (AnyObjectId.isEqual(obj, toAdd))
 					return true;
 
 				if (++i == ids.length())
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BinaryDelta.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BinaryDelta.java
index c7e5ad6..5f69d0a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BinaryDelta.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BinaryDelta.java
@@ -142,7 +142,7 @@
 		int c, shift = 0;
 		do {
 			c = delta[deltaPtr++] & 0xff;
-			baseLen |= ((long) (c & 0x7f)) << shift;
+			baseLen |= (c & 0x7f) << shift;
 			shift += 7;
 		} while ((c & 0x80) != 0);
 		if (base.length != baseLen)
@@ -155,7 +155,7 @@
 		shift = 0;
 		do {
 			c = delta[deltaPtr++] & 0xff;
-			resLen |= ((long) (c & 0x7f)) << shift;
+			resLen |= (c & 0x7f) << shift;
 			shift += 7;
 		} while ((c & 0x80) != 0);
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
index 714e830..6506789 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
@@ -2198,7 +2198,7 @@
 		if (!cachedPacks.isEmpty()) {
 			if (otp.isEdge())
 				return;
-			if ((nFmt == PACK_WHOLE) | (nFmt == PACK_DELTA)) {
+			if (nFmt == PACK_WHOLE || nFmt == PACK_DELTA) {
 				for (CachedPack pack : cachedPacks) {
 					if (pack.hasObject(otp, next)) {
 						otp.setEdge();
@@ -2241,7 +2241,7 @@
 			otp.clearReuseAsIs();
 		}
 
-		otp.setDeltaAttempted(reuseDeltas & next.wasDeltaAttempted());
+		otp.setDeltaAttempted(reuseDeltas && next.wasDeltaAttempted());
 		otp.select(next);
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java
index 075f55c..e16adb9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java
@@ -78,7 +78,7 @@
  * Wraps all cookies persisted in a <strong>Netscape Cookie File Format</strong>
  * being referenced via the git config <a href=
  * "https://git-scm.com/docs/git-config#git-config-httpcookieFile">http.cookieFile</a>.
- *
+ * <p>
  * It will only load the cookies lazily, i.e. before calling
  * {@link #getCookies(boolean)} the file is not evaluated. This class also
  * allows persisting cookies in that file format.
@@ -134,6 +134,7 @@
 
 	/**
 	 * @param path
+	 *            where to find the cookie file
 	 */
 	public NetscapeCookieFile(Path path) {
 		this(path, new Date());
@@ -146,13 +147,17 @@
 	}
 
 	/**
-	 * @return the path to the underlying cookie file
+	 * Path to the underlying cookie file.
+	 *
+	 * @return the path
 	 */
 	public Path getPath() {
 		return path;
 	}
 
 	/**
+	 * Return all cookies from the underlying cookie file.
+	 *
 	 * @param refresh
 	 *            if {@code true} updates the list from the underlying cookie
 	 *            file if it has been modified since the last read otherwise
@@ -205,7 +210,7 @@
 	 * @throws IOException
 	 *             if the given file could not be read for some reason
 	 * @throws IllegalArgumentException
-	 *             if the given file does not have a proper format.
+	 *             if the given file does not have a proper format
 	 */
 	private static Set<HttpCookie> parseCookieFile(@NonNull byte[] input,
 			@NonNull Date creationDate)
@@ -273,72 +278,18 @@
 	}
 
 	/**
-	 * Writes all the cookies being maintained in the set being returned by
-	 * {@link #getCookies(boolean)} to the underlying file.
-	 *
-	 * Session-cookies will not be persisted.
-	 *
-	 * @param url
-	 *            url for which to write the cookies (important to derive
-	 *            default values for non-explicitly set attributes)
-	 * @throws IOException
-	 * @throws IllegalArgumentException
-	 * @throws InterruptedException
-	 */
-	public void write(URL url)
-			throws IllegalArgumentException, IOException, InterruptedException {
-		try {
-			byte[] cookieFileContent = getFileContentIfModified();
-			if (cookieFileContent != null) {
-				LOG.debug(
-						"Reading the underlying cookie file '{}' as it has been modified since the last access", //$NON-NLS-1$
-						path);
-				// reread new changes if necessary
-				Set<HttpCookie> cookiesFromFile = NetscapeCookieFile
-						.parseCookieFile(cookieFileContent, creationDate);
-				this.cookies = mergeCookies(cookiesFromFile, cookies);
-			}
-		} catch (FileNotFoundException e) {
-			// ignore if file previously did not exist yet!
-		}
-
-		ByteArrayOutputStream output = new ByteArrayOutputStream();
-		try (Writer writer = new OutputStreamWriter(output,
-				StandardCharsets.US_ASCII)) {
-			write(writer, cookies, url, creationDate);
-		}
-		LockFile lockFile = new LockFile(path.toFile());
-		for (int retryCount = 0; retryCount < LOCK_ACQUIRE_MAX_RETRY_COUNT; retryCount++) {
-			if (lockFile.lock()) {
-				try {
-					lockFile.setNeedSnapshot(true);
-					lockFile.write(output.toByteArray());
-					if (!lockFile.commit()) {
-						throw new IOException(MessageFormat.format(
-								JGitText.get().cannotCommitWriteTo, path));
-					}
-				} finally {
-					lockFile.unlock();
-				}
-				return;
-			}
-			Thread.sleep(LOCK_ACQUIRE_RETRY_SLEEP);
-		}
-		throw new IOException(
-				MessageFormat.format(JGitText.get().cannotLock, lockFile));
-
-	}
-
-	/**
 	 * Read the underying file and return its content but only in case it has
-	 * been modified since the last access. Internally calculates the hash and
-	 * maintains {@link FileSnapshot}s to prevent issues described as <a href=
+	 * been modified since the last access.
+	 * <p>
+	 * Internally calculates the hash and maintains {@link FileSnapshot}s to
+	 * prevent issues described as <a href=
 	 * "https://github.com/git/git/blob/master/Documentation/technical/racy-git.txt">"Racy
 	 * Git problem"</a>. Inspired by {@link FileBasedConfig#load()}.
 	 *
 	 * @return the file contents in case the file has been modified since the
 	 *         last access, otherwise {@code null}
 	 * @throws IOException
+	 *             if the file is not found or cannot be read
 	 */
 	private byte[] getFileContentIfModified() throws IOException {
 		final int maxStaleRetries = 5;
@@ -386,16 +337,74 @@
 
 	}
 
-	private byte[] hash(final byte[] in) {
+	private static byte[] hash(final byte[] in) {
 		return Constants.newMessageDigest().digest(in);
 	}
 
 	/**
+	 * Writes all the cookies being maintained in the set being returned by
+	 * {@link #getCookies(boolean)} to the underlying file.
+	 * <p>
+	 * Session-cookies will not be persisted.
+	 *
+	 * @param url
+	 *            url for which to write the cookies (important to derive
+	 *            default values for non-explicitly set attributes)
+	 * @throws IOException
+	 *             if the underlying cookie file could not be read or written or
+	 *             a problem with the lock file
+	 * @throws InterruptedException
+	 *             if the thread is interrupted while waiting for the lock
+	 */
+	public void write(URL url) throws IOException, InterruptedException {
+		try {
+			byte[] cookieFileContent = getFileContentIfModified();
+			if (cookieFileContent != null) {
+				LOG.debug("Reading the underlying cookie file '{}' " //$NON-NLS-1$
+						+ "as it has been modified since " //$NON-NLS-1$
+						+ "the last access", //$NON-NLS-1$
+						path);
+				// reread new changes if necessary
+				Set<HttpCookie> cookiesFromFile = NetscapeCookieFile
+						.parseCookieFile(cookieFileContent, creationDate);
+				this.cookies = mergeCookies(cookiesFromFile, cookies);
+			}
+		} catch (FileNotFoundException e) {
+			// ignore if file previously did not exist yet!
+		}
+
+		ByteArrayOutputStream output = new ByteArrayOutputStream();
+		try (Writer writer = new OutputStreamWriter(output,
+				StandardCharsets.US_ASCII)) {
+			write(writer, cookies, url, creationDate);
+		}
+		LockFile lockFile = new LockFile(path.toFile());
+		for (int retryCount = 0; retryCount < LOCK_ACQUIRE_MAX_RETRY_COUNT; retryCount++) {
+			if (lockFile.lock()) {
+				try {
+					lockFile.setNeedSnapshot(true);
+					lockFile.write(output.toByteArray());
+					if (!lockFile.commit()) {
+						throw new IOException(MessageFormat.format(
+								JGitText.get().cannotCommitWriteTo, path));
+					}
+				} finally {
+					lockFile.unlock();
+				}
+				return;
+			}
+			Thread.sleep(LOCK_ACQUIRE_RETRY_SLEEP);
+		}
+		throw new IOException(
+				MessageFormat.format(JGitText.get().cannotLock, lockFile));
+	}
+
+	/**
 	 * Writes the given cookies to the file in the Netscape Cookie File Format
-	 * (also used by curl)
+	 * (also used by curl).
 	 *
 	 * @param writer
-	 *            the writer to use to persist the cookies.
+	 *            the writer to use to persist the cookies
 	 * @param cookies
 	 *            the cookies to write into the file
 	 * @param url
@@ -404,8 +413,9 @@
 	 * @param creationDate
 	 *            the date when the cookie has been created. Important for
 	 *            calculation the cookie expiration time (calculated from
-	 *            cookie's maxAge and this creation time).
+	 *            cookie's maxAge and this creation time)
 	 * @throws IOException
+	 *             if an I/O error occurs
 	 */
 	static void write(@NonNull Writer writer,
 			@NonNull Collection<HttpCookie> cookies, @NonNull URL url,
@@ -461,7 +471,9 @@
 	 * the entry from set {@code cookies1} ends up in the resulting set.
 	 *
 	 * @param cookies1
+	 *            first set of cookies
 	 * @param cookies2
+	 *            second set of cookies
 	 *
 	 * @return the merged cookies
 	 */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
index e8a6ba7..c1e94a0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
@@ -50,6 +50,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -65,6 +66,7 @@
 import org.eclipse.jgit.errors.InvalidPatternException;
 import org.eclipse.jgit.fnmatch.FileNameMatcher;
 import org.eclipse.jgit.transport.SshConstants;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.StringUtils;
 import org.eclipse.jgit.util.SystemReader;
 
@@ -129,7 +131,7 @@
 	private final String localUserName;
 
 	/** Modification time of {@link #configFile} when it was last loaded. */
-	private long lastModified;
+	private Instant lastModified;
 
 	/**
 	 * Encapsulates entries read out of the configuration file, and a cache of
@@ -223,8 +225,8 @@
 	}
 
 	private synchronized State refresh() {
-		final long mtime = configFile.lastModified();
-		if (mtime != lastModified) {
+		final Instant mtime = FS.DETECTED.lastModifiedInstant(configFile);
+		if (!mtime.equals(lastModified)) {
 			State newState = new State();
 			try (BufferedReader br = Files
 					.newBufferedReader(configFile.toPath(), UTF_8)) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java
index 978dd3a..4f90e69 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java
@@ -49,6 +49,7 @@
 import java.nio.ByteBuffer;
 
 import org.eclipse.jgit.util.NB;
+import org.eclipse.jgit.util.References;
 
 /**
  * A (possibly mutable) SHA-1 abstraction.
@@ -60,19 +61,37 @@
 public abstract class AnyObjectId implements Comparable<AnyObjectId> {
 
 	/**
-	 * Compare to object identifier byte sequences for equality.
+	 * Compare two object identifier byte sequences for equality.
 	 *
 	 * @param firstObjectId
 	 *            the first identifier to compare. Must not be null.
 	 * @param secondObjectId
 	 *            the second identifier to compare. Must not be null.
 	 * @return true if the two identifiers are the same.
+	 * @deprecated use {@link #isEqual(AnyObjectId, AnyObjectId)} instead
 	 */
+	@Deprecated
+	@SuppressWarnings("AmbiguousMethodReference")
 	public static boolean equals(final AnyObjectId firstObjectId,
 			final AnyObjectId secondObjectId) {
-		if (firstObjectId == secondObjectId)
-			return true;
+		return isEqual(firstObjectId, secondObjectId);
+	}
 
+	/**
+	 * Compare two object identifier byte sequences for equality.
+	 *
+	 * @param firstObjectId
+	 *            the first identifier to compare. Must not be null.
+	 * @param secondObjectId
+	 *            the second identifier to compare. Must not be null.
+	 * @return true if the two identifiers are the same.
+	 * @since 5.4
+	 */
+	public static boolean isEqual(final AnyObjectId firstObjectId,
+			final AnyObjectId secondObjectId) {
+		if (References.isSameObject(firstObjectId, secondObjectId)) {
+			return true;
+		}
 		// We test word 3 first since the git file-based ODB
 		// uses the first byte of w1, and we use w2 as the
 		// hash code, one of those probably came up with these
@@ -80,7 +99,6 @@
 		// Therefore the first two words are very likely to be
 		// identical. We want to break away from collisions as
 		// quickly as possible.
-		//
 		return firstObjectId.w3 == secondObjectId.w3
 				&& firstObjectId.w4 == secondObjectId.w4
 				&& firstObjectId.w5 == secondObjectId.w5
@@ -276,8 +294,9 @@
 	 *            the other id to compare to. May be null.
 	 * @return true only if both ObjectIds have identical bits.
 	 */
+	@SuppressWarnings({ "NonOverridingEquals", "AmbiguousMethodReference" })
 	public final boolean equals(AnyObjectId other) {
-		return other != null ? equals(this, other) : false;
+		return other != null ? isEqual(this, other) : false;
 	}
 
 	/** {@inheritDoc} */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java
index 6cbddec..13f71a7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java
@@ -57,6 +57,7 @@
 import java.util.List;
 
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.util.References;
 
 /**
  * Mutable builder to construct a commit recording the state of a project.
@@ -365,7 +366,7 @@
 				os.write('\n');
 			}
 
-			if (getEncoding() != UTF_8) {
+			if (!References.isSameObject(getEncoding(), UTF_8)) {
 				os.write(hencoding);
 				os.write(' ');
 				os.write(Constants.encodeASCII(getEncoding().name()));
@@ -474,7 +475,7 @@
 		r.append(gpgSignature != null ? gpgSignature.toString() : "NOT_SET");
 		r.append("\n");
 
-		if (encoding != null && encoding != UTF_8) {
+		if (encoding != null && !References.isSameObject(encoding, UTF_8)) {
 			r.append("encoding ");
 			r.append(encoding.name());
 			r.append("\n");
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
index 1032fd0..71f8635 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
@@ -131,10 +131,11 @@
 	/**
 	 * Check if a given string is the "missing" value.
 	 *
-	 * @param value
+	 * @param value string to be checked.
 	 * @return true if the given string is the "missing" value.
 	 * @since 5.4
 	 */
+	@SuppressWarnings({ "ReferenceEquality", "StringEquality" })
 	public static boolean isMissing(String value) {
 		return value == MISSING_ENTRY;
 	}
@@ -1052,7 +1053,7 @@
 				if (e.prefix == null || "".equals(e.prefix)) //$NON-NLS-1$
 					out.append('\t');
 				out.append(e.name);
-				if (MISSING_ENTRY != e.value) {
+				if (!isMissing(e.value)) {
 					out.append(" ="); //$NON-NLS-1$
 					if (e.value != null) {
 						out.append(' ');
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
index 54f6bbc..8f40db6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -476,4 +476,23 @@
 	 * @since 5.2
 	 */
 	public static final String CONFIG_KEY_LOG_OUTPUT_ENCODING = "logOutputEncoding";
+
+	/**
+	 * The "filesystem" section
+	 * @since 5.1.9
+	 */
+	public static final String CONFIG_FILESYSTEM_SECTION = "filesystem";
+
+	/**
+	 * The "timestampResolution" key
+	 * @since 5.1.9
+	 */
+	public static final String CONFIG_KEY_TIMESTAMP_RESOLUTION = "timestampResolution";
+
+	/**
+	 * The "minRacyThreshold" key
+	 *
+	 * @since 5.1.9
+	 */
+	public static final String CONFIG_KEY_MIN_RACY_THRESHOLD = "minRacyThreshold";
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
index 4c70d20..e865da8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
@@ -226,6 +226,14 @@
 			inputUnit = wantUnit;
 			inputMul = 1;
 
+		} else if (match(unitName, "ns", "nanoseconds")) { //$NON-NLS-1$ //$NON-NLS-2$
+			inputUnit = TimeUnit.NANOSECONDS;
+			inputMul = 1;
+
+		} else if (match(unitName, "us", "microseconds")) { //$NON-NLS-1$ //$NON-NLS-2$
+			inputUnit = TimeUnit.MICROSECONDS;
+			inputMul = 1;
+
 		} else if (match(unitName, "ms", "milliseconds")) { //$NON-NLS-1$ //$NON-NLS-2$
 			inputUnit = TimeUnit.MILLISECONDS;
 			inputMul = 1;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java
index d4c4d5b..8fa8d5f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java
@@ -88,6 +88,7 @@
 	public static final FileMode TREE = new FileMode(TYPE_TREE,
 			Constants.OBJ_TREE) {
 		@Override
+		@SuppressWarnings("NonOverridingEquals")
 		public boolean equals(int modeBits) {
 			return (modeBits & TYPE_MASK) == TYPE_TREE;
 		}
@@ -97,6 +98,7 @@
 	public static final FileMode SYMLINK = new FileMode(TYPE_SYMLINK,
 			Constants.OBJ_BLOB) {
 		@Override
+		@SuppressWarnings("NonOverridingEquals")
 		public boolean equals(int modeBits) {
 			return (modeBits & TYPE_MASK) == TYPE_SYMLINK;
 		}
@@ -106,6 +108,7 @@
 	public static final FileMode REGULAR_FILE = new FileMode(0100644,
 			Constants.OBJ_BLOB) {
 		@Override
+		@SuppressWarnings("NonOverridingEquals")
 		public boolean equals(int modeBits) {
 			return (modeBits & TYPE_MASK) == TYPE_FILE && (modeBits & 0111) == 0;
 		}
@@ -115,6 +118,7 @@
 	public static final FileMode EXECUTABLE_FILE = new FileMode(0100755,
 			Constants.OBJ_BLOB) {
 		@Override
+		@SuppressWarnings("NonOverridingEquals")
 		public boolean equals(int modeBits) {
 			return (modeBits & TYPE_MASK) == TYPE_FILE && (modeBits & 0111) != 0;
 		}
@@ -124,6 +128,7 @@
 	public static final FileMode GITLINK = new FileMode(TYPE_GITLINK,
 			Constants.OBJ_COMMIT) {
 		@Override
+		@SuppressWarnings("NonOverridingEquals")
 		public boolean equals(int modeBits) {
 			return (modeBits & TYPE_MASK) == TYPE_GITLINK;
 		}
@@ -133,6 +138,7 @@
 	public static final FileMode MISSING = new FileMode(TYPE_MISSING,
 			Constants.OBJ_BAD) {
 		@Override
+		@SuppressWarnings("NonOverridingEquals")
 		public boolean equals(int modeBits) {
 			return modeBits == 0;
 		}
@@ -165,6 +171,7 @@
 
 		return new FileMode(bits, Constants.OBJ_BAD) {
 			@Override
+			@SuppressWarnings("NonOverridingEquals")
 			public boolean equals(int a) {
 				return bits == a;
 			}
@@ -206,6 +213,7 @@
 	 *            a int.
 	 * @return true if the mode bits represent the same mode as this object
 	 */
+	@SuppressWarnings("NonOverridingEquals")
 	public abstract boolean equals(int modebits);
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java
index 42d1330..ce1eb59 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java
@@ -642,11 +642,12 @@
 	private void addConflict(String path, int stage) {
 		StageState existingStageStates = conflicts.get(path);
 		byte stageMask = 0;
-		if (existingStageStates != null)
-			stageMask |= existingStageStates.getStageMask();
+		if (existingStageStates != null) {
+			stageMask |= (byte) existingStageStates.getStageMask();
+		}
 		// stage 1 (base) should be shifted 0 times
 		int shifts = stage - 1;
-		stageMask |= (1 << shifts);
+		stageMask |= (byte) (1 << shifts);
 		StageState stageState = StageState.fromMask(stageMask);
 		conflicts.put(path, stageState);
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java
index cd57bda..470275b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java
@@ -103,8 +103,9 @@
 		V obj;
 
 		while ((obj = tbl[i]) != null) {
-			if (AnyObjectId.equals(obj, toFind))
+			if (AnyObjectId.isEqual(obj, toFind)) {
 				return obj;
+			}
 			i = (i + 1) & msk;
 		}
 		return null;
@@ -162,7 +163,7 @@
 		V obj;
 
 		while ((obj = tbl[i]) != null) {
-			if (AnyObjectId.equals(obj, newValue))
+			if (AnyObjectId.isEqual(obj, newValue))
 				return obj;
 			i = (i + 1) & msk;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java
index 7841627..eca15c0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java
@@ -53,6 +53,7 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.util.References;
 
 /**
  * Creates, updates or deletes any reference.
@@ -753,7 +754,7 @@
 			if (expValue != null) {
 				final ObjectId o;
 				o = oldValue != null ? oldValue : ObjectId.zeroId();
-				if (!AnyObjectId.equals(expValue, o)) {
+				if (!AnyObjectId.isEqual(expValue, o)) {
 					return Result.LOCK_FAILURE;
 				}
 			}
@@ -768,7 +769,8 @@
 			}
 
 			oldObj = safeParseOld(walk, oldValue);
-			if (newObj == oldObj && !detachingSymbolicRef) {
+			if (References.isSameObject(newObj, oldObj)
+					&& !detachingSymbolicRef) {
 				return store.execute(Result.NO_CHANGE);
 			}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
index d53b0c9..68866ea 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
@@ -1536,19 +1536,22 @@
 		final String filePath = file.getPath();
 		final String workDirPath = workDir.getPath();
 
-		if (filePath.length() <= workDirPath.length() ||
-		    filePath.charAt(workDirPath.length()) != File.separatorChar ||
-		    !filePath.startsWith(workDirPath)) {
-			File absWd = workDir.isAbsolute() ? workDir : workDir.getAbsoluteFile();
+		if (filePath.length() <= workDirPath.length()
+				|| filePath.charAt(workDirPath.length()) != File.separatorChar
+				|| !filePath.startsWith(workDirPath)) {
+			File absWd = workDir.isAbsolute() ? workDir
+					: workDir.getAbsoluteFile();
 			File absFile = file.isAbsolute() ? file : file.getAbsoluteFile();
-			if (absWd == workDir && absFile == file)
+			if (absWd.equals(workDir) && absFile.equals(file)) {
 				return ""; //$NON-NLS-1$
+			}
 			return stripWorkDir(absWd, absFile);
 		}
 
 		String relName = filePath.substring(workDirPath.length() + 1);
-		if (File.separatorChar != '/')
+		if (File.separatorChar != '/') {
 			relName = relName.replace(File.separatorChar, '/');
+		}
 		return relName;
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
index dd42e43..a77cb4f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
@@ -86,6 +86,11 @@
 	private final static Edit END_EDIT = new Edit(Integer.MAX_VALUE,
 			Integer.MAX_VALUE);
 
+	@SuppressWarnings("ReferenceEquality")
+	private static boolean isEndEdit(Edit edit) {
+		return edit == END_EDIT;
+	}
+
 	/**
 	 * Does the three way merge between a common base and two sequences.
 	 *
@@ -145,7 +150,7 @@
 		// iterate over all edits from base to ours and from base to theirs
 		// leave the loop when there are no edits more for ours or for theirs
 		// (or both)
-		while (theirsEdit != END_EDIT || oursEdit != END_EDIT) {
+		while (!isEndEdit(theirsEdit) || !isEndEdit(oursEdit)) {
 			if (oursEdit.getEndA() < theirsEdit.getBeginA()) {
 				// something was changed in ours not overlapping with any change
 				// from theirs. First add the common part in front of the edit
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
index 0c3d3fe..0b423fb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -47,6 +47,7 @@
 package org.eclipse.jgit.merge;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.time.Instant.EPOCH;
 import static org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm.HISTOGRAM;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_ALGORITHM;
@@ -59,6 +60,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -464,7 +466,7 @@
 	 * @return the entry which was added to the index
 	 */
 	private DirCacheEntry add(byte[] path, CanonicalTreeParser p, int stage,
-			long lastMod, long len) {
+			Instant lastMod, long len) {
 		if (p != null && !p.getEntryFileMode().equals(FileMode.TREE)) {
 			DirCacheEntry e = new DirCacheEntry(path, stage);
 			e.setFileMode(p.getEntryFileMode());
@@ -491,7 +493,7 @@
 				e.getStage());
 		newEntry.setFileMode(e.getFileMode());
 		newEntry.setObjectId(e.getObjectId());
-		newEntry.setLastModified(e.getLastModified());
+		newEntry.setLastModified(e.getLastModifiedInstant());
 		newEntry.setLength(e.getLength());
 		builder.add(newEntry);
 		return newEntry;
@@ -667,16 +669,17 @@
 						// we know about length and lastMod only after we have written the new content.
 						// This will happen later. Set these values to 0 for know.
 						DirCacheEntry e = add(tw.getRawPath(), theirs,
-								DirCacheEntry.STAGE_0, 0, 0);
+								DirCacheEntry.STAGE_0, EPOCH, 0);
 						addToCheckout(tw.getPathString(), e, attributes);
 					}
 					return true;
 				} else {
 					// FileModes are not mergeable. We found a conflict on modes.
 					// For conflicting entries we don't know lastModified and length.
-					add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0);
-					add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0);
-					add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0);
+					add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
+					add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
+					add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH,
+							0);
 					unmergedPaths.add(tw.getPathString());
 					mergeResults.put(
 							tw.getPathString(),
@@ -708,7 +711,7 @@
 				// the new content.
 				// This will happen later. Set these values to 0 for know.
 				DirCacheEntry e = add(tw.getRawPath(), theirs,
-						DirCacheEntry.STAGE_0, 0, 0);
+						DirCacheEntry.STAGE_0, EPOCH, 0);
 				if (e != null) {
 					addToCheckout(tw.getPathString(), e, attributes);
 				}
@@ -737,16 +740,16 @@
 			// detected later
 			if (nonTree(modeO) && !nonTree(modeT)) {
 				if (nonTree(modeB))
-					add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0);
-				add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0);
+					add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
+				add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
 				unmergedPaths.add(tw.getPathString());
 				enterSubtree = false;
 				return true;
 			}
 			if (nonTree(modeT) && !nonTree(modeO)) {
 				if (nonTree(modeB))
-					add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0);
-				add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0);
+					add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
+				add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
 				unmergedPaths.add(tw.getPathString());
 				enterSubtree = false;
 				return true;
@@ -773,9 +776,9 @@
 			boolean gitlinkConflict = isGitLink(modeO) || isGitLink(modeT);
 			// Don't attempt to resolve submodule link conflicts
 			if (gitlinkConflict || !attributes.canBeContentMerged()) {
-				add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0);
-				add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0);
-				add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0);
+				add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
+				add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
+				add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
 
 				if (gitlinkConflict) {
 					MergeResult<SubmoduleConflict> result = new MergeResult<>(
@@ -822,10 +825,10 @@
 				MergeResult<RawText> result = contentMerge(base, ours, theirs,
 						attributes);
 
-				add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0);
-				add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0);
+				add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
+				add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
 				DirCacheEntry e = add(tw.getRawPath(), theirs,
-						DirCacheEntry.STAGE_3, 0, 0);
+						DirCacheEntry.STAGE_3, EPOCH, 0);
 
 				// OURS was deleted checkout THEIRS
 				if (modeO == 0) {
@@ -957,9 +960,9 @@
 				// A conflict occurred, the file will contain conflict markers
 				// the index will be populated with the three stages and the
 				// workdir (if used) contains the halfway merged content.
-				add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0);
-				add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0);
-				add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0);
+				add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
+				add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
+				add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
 				mergeResults.put(tw.getPathString(), result);
 				return;
 			}
@@ -976,7 +979,7 @@
 					? FileMode.REGULAR_FILE : FileMode.fromBits(newMode));
 			if (mergedFile != null) {
 				dce.setLastModified(
-						nonNullRepo().getFS().lastModified(mergedFile));
+						nonNullRepo().getFS().lastModifiedInstant(mergedFile));
 				dce.setLength((int) mergedFile.length());
 			}
 			dce.setObjectId(insertMergeResult(rawMerged, attributes));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/nls/GlobalBundleCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/nls/GlobalBundleCache.java
index 84bf214..b437f63 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/nls/GlobalBundleCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/nls/GlobalBundleCache.java
@@ -43,6 +43,7 @@
 
 package org.eclipse.jgit.nls;
 
+import java.lang.reflect.InvocationTargetException;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
@@ -92,12 +93,13 @@
 			}
 			TranslationBundle bundle = bundles.get(type);
 			if (bundle == null) {
-				bundle = type.newInstance();
+				bundle = type.getDeclaredConstructor().newInstance();
 				bundle.load(locale);
 				bundles.put(type, bundle);
 			}
 			return (T) bundle;
-		} catch (InstantiationException | IllegalAccessException e) {
+		} catch (InstantiationException | IllegalAccessException
+				| InvocationTargetException | NoSuchMethodException e) {
 			throw new Error(e);
 		}
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java
index 325ff4f..ba7223b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java
@@ -295,14 +295,14 @@
 	private static boolean sameNote(Note a, Note b) {
 		if (a == null && b == null)
 			return true;
-		return a != null && b != null && AnyObjectId.equals(a, b);
+		return a != null && b != null && AnyObjectId.isEqual(a, b);
 	}
 
 	private static boolean sameContent(Note a, Note b) {
 		if (a == null && b == null)
 			return true;
 		return a != null && b != null
-				&& AnyObjectId.equals(a.getData(), b.getData());
+				&& AnyObjectId.isEqual(a.getData(), b.getData());
 	}
 
 	private static InMemoryNoteBucket addIfNotNull(InMemoryNoteBucket result,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java
index d6fed66..84b6d2e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java
@@ -95,7 +95,7 @@
 		for (int kPtr = 0; kPtr < len;) {
 			byte b = buffer[bPtr++];
 			if ('A' <= b && b <= 'Z')
-				b += 'a' - 'A';
+				b += (byte) ('a' - 'A');
 			if (b != kRaw[kPtr++])
 				return false;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java
index af6040f..b6c5810 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java
@@ -497,7 +497,7 @@
 				continue;
 			}
 			visitationPolicy.visited(o);
-			if ((o.flags & UNINTERESTING) == 0 | boundary) {
+			if ((o.flags & UNINTERESTING) == 0 || boundary) {
 				if (o instanceof RevTree) {
 					// The previous while loop should have exhausted the stack
 					// of trees.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
index 634ea4a..4aa8325 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
@@ -72,6 +72,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.filter.RevFilter;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.References;
 
 /**
  * Walks a commit graph and produces the matching commits in order.
@@ -435,9 +436,11 @@
 			markStart(tip);
 			markStart(base);
 			RevCommit mergeBase;
-			while ((mergeBase = next()) != null)
-				if (mergeBase == base)
+			while ((mergeBase = next()) != null) {
+				if (References.isSameObject(mergeBase, base)) {
 					return true;
+				}
+			}
 			return false;
 		} finally {
 			filter = oldRF;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java
index fc6f4a3..10b00b7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java
@@ -148,12 +148,38 @@
 	 */
 	@Override
 	public void load() throws IOException, ConfigInvalidException {
+		load(true);
+	}
+
+	/**
+	 * Load the configuration as a Git text style configuration file.
+	 * <p>
+	 * If the file does not exist, this configuration is cleared, and thus
+	 * behaves the same as though the file exists, but is empty.
+	 *
+	 * @param useFileSnapshotWithConfig
+	 *            if {@code true} use the FileSnapshot with config, otherwise
+	 *            use it without config
+	 * @throws IOException
+	 *             if IO failed
+	 * @throws ConfigInvalidException
+	 *             if config is invalid
+	 * @since 5.1.9
+	 */
+	public void load(boolean useFileSnapshotWithConfig)
+			throws IOException, ConfigInvalidException {
 		final int maxRetries = 5;
 		int retryDelayMillis = 20;
 		int retries = 0;
 		while (true) {
 			final FileSnapshot oldSnapshot = snapshot;
-			final FileSnapshot newSnapshot = FileSnapshot.save(getFile());
+			final FileSnapshot newSnapshot;
+			if (useFileSnapshotWithConfig) {
+				newSnapshot = FileSnapshot.save(getFile());
+			} else {
+				// don't use config in this snapshot to avoid endless recursion
+				newSnapshot = FileSnapshot.saveNoConfig(getFile());
+			}
 			try {
 				final byte[] in = IO.readFully(getFile());
 				final ObjectId newHash = hash(in);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
index ea8da42..27cc205 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
@@ -670,7 +670,7 @@
 				}
 			}
 
-			if (noDone & receivedReady) {
+			if (noDone && receivedReady) {
 				break SEND_HAVES;
 			}
 			if (statelessRPC) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java
index 8562376..7dd019b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java
@@ -49,6 +49,7 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.time.Instant;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Locale;
@@ -126,7 +127,7 @@
 
 	private File netrc;
 
-	private long lastModified;
+	private Instant lastModified;
 
 	private Map<String, NetRCEntry> hosts = new HashMap<>();
 
@@ -190,8 +191,10 @@
 		if (netrc == null)
 			return null;
 
-		if (this.lastModified != this.netrc.lastModified())
+		if (!this.lastModified
+				.equals(FS.DETECTED.lastModifiedInstant(this.netrc))) {
 			parse();
+		}
 
 		NetRCEntry entry = this.hosts.get(host);
 
@@ -212,7 +215,7 @@
 
 	private void parse() {
 		this.hosts.clear();
-		this.lastModified = this.netrc.lastModified();
+		this.lastModified = FS.DETECTED.lastModifiedInstant(this.netrc);
 
 		try (BufferedReader r = new BufferedReader(
 				new InputStreamReader(new FileInputStream(netrc), UTF_8))) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java
index 90f1b37..d73e193 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java
@@ -259,6 +259,7 @@
 	 * @return true if the given string is {@link #DELIM}, otherwise false.
 	 * @since 5.4
 	 */
+	@SuppressWarnings({ "ReferenceEquality", "StringEquality" })
 	public static boolean isDelimiter(String s) {
 		return s == DELIM;
 	}
@@ -293,6 +294,7 @@
 	 * @return true if the given string is {@link #END}, otherwise false.
 	 * @since 5.4
 	 */
+	@SuppressWarnings({ "ReferenceEquality", "StringEquality" })
 	public static boolean isEnd(String s) {
 		return s == END;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
index d61aeb0..a9a995c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
@@ -429,7 +429,7 @@
 		this.newId = ObjectId.zeroId();
 		this.newSymref = newSymref;
 		this.name = name;
-		if (AnyObjectId.equals(ObjectId.zeroId(), oldId)) {
+		if (AnyObjectId.isEqual(ObjectId.zeroId(), oldId)) {
 			type = Type.CREATE;
 		} else if (newSymref != null) {
 			type = Type.UPDATE;
@@ -468,7 +468,7 @@
 		this.name = name;
 		if (oldSymref == null) {
 			type = Type.CREATE;
-		} else if (!AnyObjectId.equals(ObjectId.zeroId(), newId)) {
+		} else if (!AnyObjectId.isEqual(ObjectId.zeroId(), newId)) {
 			type = Type.UPDATE;
 		} else {
 			type = Type.DELETE;
@@ -750,7 +750,7 @@
 	public void updateType(RevWalk walk) throws IOException {
 		if (typeIsCorrect)
 			return;
-		if (type == Type.UPDATE && !AnyObjectId.equals(oldId, newId)) {
+		if (type == Type.UPDATE && !AnyObjectId.isEqual(oldId, newId)) {
 			RevObject o = walk.parseAny(oldId);
 			RevObject n = walk.parseAny(newId);
 			if (!(o instanceof RevCommit)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
index afd3ada..16c6faf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
@@ -49,6 +49,7 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.util.References;
 
 /**
  * Describes how refs in one repository copy into another repository.
@@ -585,8 +586,9 @@
 	}
 
 	private static boolean eq(String a, String b) {
-		if (a == b)
+		if (References.isSameObject(a, b)) {
 			return true;
+		}
 		if (a == null || b == null)
 			return false;
 		return a.equals(b);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java
index ba2a673..550a9ef 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java
@@ -175,7 +175,7 @@
 		private RefUpdate.Result decode(ReceiveCommand.Result status) {
 			switch (status) {
 			case OK:
-				if (AnyObjectId.equals(oldObjectId, newObjectId))
+				if (AnyObjectId.isEqual(oldObjectId, newObjectId))
 					return RefUpdate.Result.NO_CHANGE;
 				switch (getType()) {
 				case CREATE:
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
index 34730d3..7ca9cc1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
@@ -62,6 +62,7 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.References;
 import org.eclipse.jgit.util.StringUtils;
 
 /**
@@ -624,8 +625,9 @@
 	}
 
 	private static boolean eq(String a, String b) {
-		if (a == b)
+		if (References.isSameObject(a, b)) {
 			return true;
+		}
 		if (StringUtils.isEmptyOrNull(a) && StringUtils.isEmptyOrNull(b))
 			return true;
 		if (a == null || b == null)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
index 2bb5814..b289e42 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
@@ -657,7 +657,7 @@
 		}
 
 		ObjectId act = inserter.insert(type, raw);
-		if (!AnyObjectId.equals(id, act)) {
+		if (!AnyObjectId.isEqual(id, act)) {
 			throw new TransportException(MessageFormat.format(
 					JGitText.get().incorrectHashFor, id.name(), act.name(),
 					Constants.typeString(type),
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java
index 4c75425..d9103f8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java
@@ -165,7 +165,7 @@
 				continue;
 			}
 
-			if (AnyObjectId.equals(ObjectId.zeroId(), u.getNewObjectId()))
+			if (AnyObjectId.isEqual(ObjectId.zeroId(), u.getNewObjectId()))
 				deleteCommand(u);
 			else
 				updates.add(u);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
index 3d25c23..d432c94 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
@@ -53,6 +53,7 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.time.Instant;
 
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -406,8 +407,14 @@
 		}
 
 		@Override
+		@Deprecated
 		public long getLastModified() {
-			return attributes.getLastModifiedTime();
+			return attributes.getLastModifiedInstant().toEpochMilli();
+		}
+
+		@Override
+		public Instant getLastModifiedInstant() {
+			return attributes.getLastModifiedInstant();
 		}
 
 		@Override
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
index 3efa664..f816ff3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
@@ -59,6 +59,7 @@
 import java.nio.charset.CharacterCodingException;
 import java.nio.charset.CharsetEncoder;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
@@ -646,12 +647,24 @@
 	 *
 	 * @return last modified time of this file, in milliseconds since the epoch
 	 *         (Jan 1, 1970 UTC).
+	 * @deprecated use {@link #getEntryLastModifiedInstant()} instead
 	 */
+	@Deprecated
 	public long getEntryLastModified() {
 		return current().getLastModified();
 	}
 
 	/**
+	 * Get the last modified time of this entry.
+	 *
+	 * @return last modified time of this file
+	 * @since 5.1.9
+	 */
+	public Instant getEntryLastModifiedInstant() {
+		return current().getLastModifiedInstant();
+	}
+
+	/**
 	 * Obtain an input stream to read the file content.
 	 * <p>
 	 * Efficient implementations are not required. The caller will usually
@@ -921,30 +934,28 @@
 
 		// Git under windows only stores seconds so we round the timestamp
 		// Java gives us if it looks like the timestamp in index is seconds
-		// only. Otherwise we compare the timestamp at millisecond precision,
+		// only. Otherwise we compare the timestamp at nanosecond precision,
 		// unless core.checkstat is set to "minimal", in which case we only
 		// compare the whole second part.
-		long cacheLastModified = entry.getLastModified();
-		long fileLastModified = getEntryLastModified();
-		long lastModifiedMillis = fileLastModified % 1000;
-		long cacheMillis = cacheLastModified % 1000;
-		if (getOptions().getCheckStat() == CheckStat.MINIMAL) {
-			fileLastModified = fileLastModified - lastModifiedMillis;
-			cacheLastModified = cacheLastModified - cacheMillis;
-		} else if (cacheMillis == 0)
-			fileLastModified = fileLastModified - lastModifiedMillis;
-		// Some Java version on Linux return whole seconds only even when
-		// the file systems supports more precision.
-		else if (lastModifiedMillis == 0)
-			cacheLastModified = cacheLastModified - cacheMillis;
-
-		if (fileLastModified != cacheLastModified)
+		Instant cacheLastModified = entry.getLastModifiedInstant();
+		Instant fileLastModified = getEntryLastModifiedInstant();
+		if ((getOptions().getCheckStat() == CheckStat.MINIMAL)
+				|| (cacheLastModified.getNano() == 0)
+				// Some Java version on Linux return whole seconds only even
+				// when the file systems supports more precision.
+				|| (fileLastModified.getNano() == 0)) {
+			if (fileLastModified.getEpochSecond() != cacheLastModified
+					.getEpochSecond()) {
+				return MetadataDiff.DIFFER_BY_TIMESTAMP;
+			}
+		}
+		if (!fileLastModified.equals(cacheLastModified)) {
 			return MetadataDiff.DIFFER_BY_TIMESTAMP;
-		else if (!entry.isSmudged())
-			// The file is clean when you look at timestamps.
-			return MetadataDiff.EQUAL;
-		else
+		} else if (entry.isSmudged()) {
 			return MetadataDiff.SMUDGED;
+		}
+		// The file is clean when when comparing timestamps
+		return MetadataDiff.EQUAL;
 	}
 
 	/**
@@ -1271,10 +1282,26 @@
 		 * instance member instead.
 		 *
 		 * @return time since the epoch (in ms) of the last change.
+		 * @deprecated use {@link #getLastModifiedInstant()} instead
 		 */
+		@Deprecated
 		public abstract long getLastModified();
 
 		/**
+		 * Get the last modified time of this entry.
+		 * <p>
+		 * <b>Note: Efficient implementation required.</b>
+		 * <p>
+		 * The implementation of this method must be efficient. If a subclass
+		 * needs to compute the value they should cache the reference within an
+		 * instance member instead.
+		 *
+		 * @return time of the last change.
+		 * @since 5.1.9
+		 */
+		public abstract Instant getLastModifiedInstant();
+
+		/**
 		 * Get the name of this entry within its directory.
 		 * <p>
 		 * Efficient implementations are not required. The caller will obtain
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilterMarker.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilterMarker.java
index 738ccbd..c28f035 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilterMarker.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilterMarker.java
@@ -113,7 +113,7 @@
 				try {
 					boolean marked = filter.include(walk);
 					if (marked)
-						marks |= (1L << index);
+						marks |= (1 << index);
 				} catch (StopWalkException e) {
 					// Don't check tree filter anymore, it will no longer
 					// match
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
index bde750b..421bf81 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
@@ -44,6 +44,7 @@
 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;
@@ -53,7 +54,9 @@
 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;
@@ -65,27 +68,39 @@
 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.stream.Collectors;
+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.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry;
 import org.eclipse.jgit.treewalk.FileTreeIterator.FileModeStrategy;
 import org.eclipse.jgit.treewalk.WorkingTreeIterator.Entry;
@@ -188,81 +203,478 @@
 		}
 	}
 
-	private static final class FileStoreAttributeCache {
+	/**
+	 * 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);
+
 		/**
-		 * The last modified time granularity of FAT filesystems is 2 seconds.
+		 * Fallback filesystem timestamp resolution. The worst case timestamp
+		 * resolution on FAT filesystems is 2 seconds.
 		 */
-		private static final Duration FALLBACK_TIMESTAMP_RESOLUTION = Duration
+		public static final Duration FALLBACK_TIMESTAMP_RESOLUTION = Duration
 				.ofMillis(2000);
 
-		private static final Map<FileStore, FileStoreAttributeCache> attributeCache = new ConcurrentHashMap<>();
+		/**
+		 * 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);
 
-		static Duration getFsTimestampResolution(Path file) {
+		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 = 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) {
+			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;
+		}
+
+		private static FileStoreAttributes getFileStoreAttributes(Path dir) {
+			FileStore s;
 			try {
-				Path dir = Files.isDirectory(file) ? file : file.getParent();
-				if (!dir.toFile().canWrite()) {
-					// can not determine FileStore of an unborn directory or in
-					// a read-only directory
-					return FALLBACK_TIMESTAMP_RESOLUTION;
-				}
-				FileStore s = Files.getFileStore(dir);
-				FileStoreAttributeCache c = attributeCache.get(s);
-				if (c == null) {
-					c = new FileStoreAttributeCache(dir);
-					attributeCache.put(s, c);
-					if (LOG.isDebugEnabled()) {
-						LOG.debug(c.toString());
+				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;
 				}
-				return c.getFsTimestampResolution();
 
-			} catch (IOException | InterruptedException e) {
-				LOG.warn(e.getMessage(), e);
-				return FALLBACK_TIMESTAMP_RESOLUTION;
+				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 Duration fsTimestampResolution;
+		private static String read(Path p) throws IOException {
+			final byte[] body = IO.readFully(p.toFile());
+			return new String(body, 0, body.length, UTF_8);
+		}
 
-		Duration getFsTimestampResolution() {
+		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) {
+			FileBasedConfig userConfig = SystemReader.getInstance()
+					.openUserConfig(null, FS.DETECTED);
+			try {
+				userConfig.load(false);
+			} catch (IOException e) {
+				LOG.error(MessageFormat.format(JGitText.get().readConfigFailed,
+						userConfig.getFile().getAbsolutePath()), e);
+			} catch (ConfigInvalidException e) {
+				LOG.error(MessageFormat.format(
+						JGitText.get().repositoryConfigFileInvalid,
+						userConfig.getFile().getAbsolutePath(),
+						e.getMessage()));
+			}
+			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) {
+			FileBasedConfig userConfig = SystemReader.getInstance()
+					.openUserConfig(null, FS.DETECTED);
+			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 {
+					userConfig.load(false);
+					userConfig.setString(
+							ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
+							ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION,
+							String.format("%d %s", //$NON-NLS-1$
+									Long.valueOf(resolutionValue),
+									resolutionUnit.name().toLowerCase()));
+					userConfig.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()));
+					userConfig.save();
+					succeeded = true;
+				} catch (LockFailedException e) {
+					// race with another thread, wait a bit and try again
+					try {
+						LOG.warn(MessageFormat.format(JGitText.get().cannotLock,
+								userConfig.getFile().getAbsolutePath()));
+						retries++;
+						Thread.sleep(20);
+					} catch (InterruptedException e1) {
+						Thread.interrupted();
+					}
+				} catch (IOException e) {
+					LOG.error(MessageFormat.format(
+							JGitText.get().cannotSaveConfig,
+							userConfig.getFile().getAbsolutePath()), e);
+				} catch (ConfigInvalidException e) {
+					LOG.error(MessageFormat.format(
+							JGitText.get().repositoryConfigFileInvalid,
+							userConfig.getFile().getAbsolutePath(),
+							e.getMessage()));
+				}
+			}
+		}
+
+		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;
 		}
 
-		private FileStoreAttributeCache(Path dir)
-				throws IOException, InterruptedException {
-			Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$
-			Files.createFile(probe);
-			try {
-				FileTime startTime = Files.getLastModifiedTime(probe);
-				FileTime actTime = startTime;
-				long sleepTime = 512;
-				while (actTime.compareTo(startTime) <= 0) {
-					TimeUnit.NANOSECONDS.sleep(sleepTime);
-					FileUtils.touch(probe);
-					actTime = Files.getLastModifiedTime(probe);
-					// limit sleep time to max. 100ms
-					if (sleepTime < 100_000_000L) {
-						sleepTime = sleepTime * 2;
-					}
-				}
-				fsTimestampResolution = Duration.between(startTime.toInstant(),
-						actTime.toInstant());
-			} catch (AccessDeniedException e) {
-				LOG.error(e.getLocalizedMessage(), e);
-			} finally {
-				Files.delete(probe);
-			}
+		/**
+		 * 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")
+		@SuppressWarnings({ "nls", "boxing" })
 		@Override
 		public String toString() {
-			return "FileStoreAttributeCache[" + attributeCache.keySet()
-					.stream()
-					.map(key -> "FileStore[" + key + "]: fsTimestampResolution="
-							+ attributeCache.get(key).getFsTimestampResolution())
-					.collect(Collectors.joining(",\n")) + "]";
+			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. */
@@ -280,6 +692,19 @@
 	}
 
 	/**
+	 * 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)
@@ -307,18 +732,18 @@
 	}
 
 	/**
-	 * Get an estimate for the filesystem timestamp resolution from a cache of
-	 * timestamp resolution per FileStore, if not yet available it is measured
-	 * for a probe file under the given directory.
+	 * 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.2.3
+	 * @since 5.1.9
 	 */
-	public static Duration getFsTimerResolution(@NonNull Path dir) {
-		return FileStoreAttributeCache.getFsTimestampResolution(dir);
+	public static FileStoreAttributes getFileStoreAttributes(
+			@NonNull Path dir) {
+		return FileStoreAttributes.get(dir);
 	}
 
 	private volatile Holder<File> userHome;
@@ -432,12 +857,42 @@
 	 * @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,
 	 *
@@ -447,12 +902,29 @@
 	 *            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.
 	 *
@@ -1522,9 +1994,19 @@
 		/**
 		 * @return the time (milliseconds since 1970-01-01) when this object was
 		 *         last modified
+		 * @deprecated use getLastModifiedInstant instead
 		 */
+		@Deprecated
 		public long getLastModifiedTime() {
-			return lastModifiedTime;
+			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;
@@ -1535,7 +2017,7 @@
 
 		private final long creationTime;
 
-		private final long lastModifiedTime;
+		private final Instant lastModifiedInstant;
 
 		private final boolean isExecutable;
 
@@ -1553,7 +2035,7 @@
 		Attributes(FS fs, File file, boolean exists, boolean isDirectory,
 				boolean isExecutable, boolean isSymbolicLink,
 				boolean isRegularFile, long creationTime,
-				long lastModifiedTime, long length) {
+				Instant lastModifiedInstant, long length) {
 			this.fs = fs;
 			this.file = file;
 			this.exists = exists;
@@ -1562,7 +2044,7 @@
 			this.isSymbolicLink = isSymbolicLink;
 			this.isRegularFile = isRegularFile;
 			this.creationTime = creationTime;
-			this.lastModifiedTime = lastModifiedTime;
+			this.lastModifiedInstant = lastModifiedInstant;
 			this.length = length;
 		}
 
@@ -1574,7 +2056,7 @@
 		 * @param path
 		 */
 		public Attributes(File path, FS fs) {
-			this(fs, path, false, false, false, false, false, 0L, 0L, 0L);
+			this(fs, path, false, false, false, false, false, 0L, EPOCH, 0L);
 		}
 
 		/**
@@ -1620,7 +2102,7 @@
 		boolean exists = isDirectory || isFile;
 		boolean canExecute = exists && !isDirectory && canExecute(path);
 		boolean isSymlink = false;
-		long lastModified = exists ? path.lastModified() : 0L;
+		Instant lastModified = exists ? lastModifiedInstant(path) : EPOCH;
 		long createTime = 0L;
 		return new Attributes(this, path, exists, isDirectory, canExecute,
 				isSymlink, isFile, createTime, lastModified, -1);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java
index faef9fd..6ec50c2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java
@@ -396,7 +396,12 @@
 		}
 		Path lockPath = lock.toPath();
 		Path link = null;
-		FileStore store = Files.getFileStore(lockPath);
+		FileStore store = null;
+		try {
+			store = Files.getFileStore(lockPath);
+		} catch (SecurityException e) {
+			return true;
+		}
 		try {
 			Boolean canLink = CAN_HARD_LINK.computeIfAbsent(store,
 					s -> Boolean.TRUE);
@@ -462,7 +467,12 @@
 		}
 		Path link = null;
 		Path path = file.toPath();
-		FileStore store = Files.getFileStore(path);
+		FileStore store = null;
+		try {
+			store = Files.getFileStore(path);
+		} catch (SecurityException e) {
+			return token(true, null);
+		}
 		try {
 			Boolean canLink = CAN_HARD_LINK.computeIfAbsent(store,
 					s -> Boolean.TRUE);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java
index 3ccbd72..7fe80bb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java
@@ -149,7 +149,7 @@
 									attrs.isSymbolicLink(),
 									attrs.isRegularFile(),
 									attrs.creationTime().toMillis(),
-									attrs.lastModifiedTime().toMillis(),
+									attrs.lastModifiedTime().toInstant(),
 									attrs.size());
 							result.add(new FileEntry(f, fs, attributes,
 									fileModeStrategy));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
index 0e8732d..4a773b0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
@@ -50,7 +50,7 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.OutputStream;
+import java.nio.channels.FileChannel;
 import java.nio.file.AtomicMoveNotSupportedException;
 import java.nio.file.CopyOption;
 import java.nio.file.Files;
@@ -58,6 +58,7 @@
 import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.BasicFileAttributeView;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.nio.file.attribute.FileTime;
@@ -67,6 +68,7 @@
 import java.text.MessageFormat;
 import java.text.Normalizer;
 import java.text.Normalizer.Form;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
@@ -75,11 +77,14 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.util.FS.Attributes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * File Utilities
  */
 public class FileUtils {
+	private static final Logger LOG = LoggerFactory.getLogger(FileUtils.class);
 
 	/**
 	 * Option to delete given {@code File}
@@ -674,13 +679,32 @@
 	 * @return lastModified attribute for given file, not following symbolic
 	 *         links
 	 * @throws IOException
+	 * @deprecated use {@link #lastModifiedInstant(Path)} instead which returns
+	 *             FileTime
 	 */
+	@Deprecated
 	static long lastModified(File file) throws IOException {
 		return Files.getLastModifiedTime(toPath(file), LinkOption.NOFOLLOW_LINKS)
 				.toMillis();
 	}
 
 	/**
+	 * @param path
+	 * @return lastModified attribute for given file, not following symbolic
+	 *         links
+	 */
+	static Instant lastModifiedInstant(Path path) {
+		try {
+			return Files.getLastModifiedTime(path, LinkOption.NOFOLLOW_LINKS)
+					.toInstant();
+		} catch (IOException e) {
+			LOG.error(MessageFormat
+					.format(JGitText.get().readLastModifiedFailed, path));
+			return Instant.ofEpochMilli(path.toFile().lastModified());
+		}
+	}
+
+	/**
 	 * Return all the attributes of a file, without following symbolic links.
 	 *
 	 * @param file
@@ -698,11 +722,22 @@
 	 * @param time
 	 * @throws IOException
 	 */
+	@Deprecated
 	static void setLastModified(File file, long time) throws IOException {
 		Files.setLastModifiedTime(toPath(file), FileTime.fromMillis(time));
 	}
 
 	/**
+	 * @param path
+	 * @param time
+	 * @throws IOException
+	 */
+	static void setLastModified(Path path, Instant time)
+			throws IOException {
+		Files.setLastModifiedTime(path, FileTime.from(time));
+	}
+
+	/**
 	 * @param file
 	 * @return {@code true} if the given file exists, not following symbolic
 	 *         links
@@ -806,7 +841,7 @@
 					readAttributes.isSymbolicLink(),
 					readAttributes.isRegularFile(), //
 					readAttributes.creationTime().toMillis(), //
-					readAttributes.lastModifiedTime().toMillis(),
+					readAttributes.lastModifiedTime().toInstant(),
 					readAttributes.isSymbolicLink() ? Constants
 							.encode(readSymLink(file)).length
 							: readAttributes.size());
@@ -845,7 +880,7 @@
 					readAttributes.isSymbolicLink(),
 					readAttributes.isRegularFile(), //
 					readAttributes.creationTime().toMillis(), //
-					readAttributes.lastModifiedTime().toMillis(),
+					readAttributes.lastModifiedTime().toInstant(),
 					readAttributes.size());
 			return attributes;
 		} catch (IOException e) {
@@ -936,11 +971,13 @@
 	 * @param f
 	 *            the file to touch
 	 * @throws IOException
-	 * @since 5.2.3
+	 * @since 5.1.8
 	 */
 	public static void touch(Path f) throws IOException {
-		try (OutputStream fos = Files.newOutputStream(f)) {
-			// touch the file
+		try (FileChannel fc = FileChannel.open(f, StandardOpenOption.CREATE,
+				StandardOpenOption.APPEND, StandardOpenOption.SYNC)) {
+			// touch
 		}
+		Files.setLastModifiedTime(f, FileTime.from(Instant.now()));
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java
index a339b9a..56a1731 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java
@@ -126,7 +126,7 @@
 		DEFAULT("EEE MMM dd HH:mm:ss yyyy Z"), // //$NON-NLS-1$
 		LOCAL("EEE MMM dd HH:mm:ss yyyy"); //$NON-NLS-1$
 
-		String formatStr;
+		private final String formatStr;
 
 		private ParseableSimpleDateFormat(String formatStr) {
 			this.formatStr = formatStr;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java
index d7a4c25..9663e3c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java
@@ -443,8 +443,10 @@
 					if (r.getName().equals(ref.getName())) {
 						final ObjectId a = r.getObjectId();
 						final ObjectId b = ref.getObjectId();
-						if (a != null && b != null && AnyObjectId.equals(a, b))
+						if (a != null && b != null
+								&& AnyObjectId.isEqual(a, b)) {
 							return true;
+						}
 					}
 				}
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/References.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/References.java
new file mode 100644
index 0000000..341fbfa
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/References.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com>
+ * 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;
+
+/**
+ * Utility methods for object references
+ *
+ * @since 5.4
+ */
+public interface References {
+
+	/**
+	 * Compare two references
+	 *
+	 * @param <T>
+	 *            type of the references
+	 * @param ref1
+	 *            first reference
+	 * @param ref2
+	 *            second reference
+	 * @return {@code true} if both references refer to the same object
+	 */
+	@SuppressWarnings("ReferenceEquality")
+	public static <T> boolean isSameObject(T ref1, T ref2) {
+		return ref1 == ref2;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SimpleLruCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SimpleLruCache.java
new file mode 100644
index 0000000..7235b15
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SimpleLruCache.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2019, Marc Strapetz <marc.strapetz@syntevo.com>
+ * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com>
+ * 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 java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.JGitText;
+
+/**
+ * Simple limited size cache based on ConcurrentHashMap purging entries in LRU
+ * order when reaching size limit
+ *
+ * @param <K>
+ *            the type of keys maintained by this cache
+ * @param <V>
+ *            the type of mapped values
+ *
+ * @since 5.1.9
+ */
+public class SimpleLruCache<K, V> {
+
+	private static class Entry<K, V> {
+
+		private final K key;
+
+		private final V value;
+
+		// pseudo clock timestamp of the last access to this entry
+		private volatile long lastAccessed;
+
+		private long lastAccessedSorting;
+
+		Entry(K key, V value, long lastAccessed) {
+			this.key = key;
+			this.value = value;
+			this.lastAccessed = lastAccessed;
+		}
+
+		void copyAccessTime() {
+			lastAccessedSorting = lastAccessed;
+		}
+
+		@SuppressWarnings("nls")
+		@Override
+		public String toString() {
+			return "Entry [lastAccessed=" + lastAccessed + ", key=" + key
+					+ ", value=" + value + "]";
+		}
+	}
+
+	private Lock lock = new ReentrantLock();
+
+	private Map<K, Entry<K,V>> map = new ConcurrentHashMap<>();
+
+	private volatile int maximumSize;
+
+	private int purgeSize;
+
+	// pseudo clock to implement LRU order of access to entries
+	private volatile long time = 0L;
+
+	private static void checkPurgeFactor(float purgeFactor) {
+		if (purgeFactor <= 0 || purgeFactor >= 1) {
+			throw new IllegalArgumentException(
+					MessageFormat.format(JGitText.get().invalidPurgeFactor,
+							Float.valueOf(purgeFactor)));
+		}
+	}
+
+	private static int purgeSize(int maxSize, float purgeFactor) {
+		return (int) ((1 - purgeFactor) * maxSize);
+	}
+
+	/**
+	 * Create a new cache
+	 *
+	 * @param maxSize
+	 *            maximum size of the cache, to reduce need for synchronization
+	 *            this is not a hard limit. The real size of the cache could be
+	 *            slightly above this maximum if multiple threads put new values
+	 *            concurrently
+	 * @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
+	 */
+	public SimpleLruCache(int maxSize, float purgeFactor) {
+		checkPurgeFactor(purgeFactor);
+		this.maximumSize = maxSize;
+		this.purgeSize = purgeSize(maxSize, purgeFactor);
+	}
+
+	/**
+	 * Returns the value to which the specified key is mapped, or {@code null}
+	 * if this map contains no mapping for the key.
+	 *
+	 * <p>
+	 * More formally, if this cache contains a mapping from a key {@code k} to a
+	 * value {@code v} such that {@code key.equals(k)}, then this method returns
+	 * {@code v}; otherwise it returns {@code null}. (There can be at most one
+	 * such mapping.)
+	 *
+	 * @param key
+	 *            the key
+	 *
+	 * @throws NullPointerException
+	 *             if the specified key is null
+	 *
+	 * @return value mapped for this key, or {@code null} if no value is mapped
+	 */
+	@SuppressWarnings("NonAtomicVolatileUpdate")
+	public V get(Object key) {
+		Entry<K, V> entry = map.get(key);
+		if (entry != null) {
+			entry.lastAccessed = ++time;
+			return entry.value;
+		}
+		return null;
+	}
+
+	/**
+	 * Maps the specified key to the specified value in this cache. Neither the
+	 * key nor the value can be null.
+	 *
+	 * <p>
+	 * The value can be retrieved by calling the {@code get} method with a key
+	 * that is equal to the original key.
+	 *
+	 * @param key
+	 *            key with which the specified value is to be associated
+	 * @param value
+	 *            value to be associated with the specified key
+	 * @return the previous value associated with {@code key}, or {@code null}
+	 *         if there was no mapping for {@code key}
+	 * @throws NullPointerException
+	 *             if the specified key or value is null
+	 */
+	@SuppressWarnings("NonAtomicVolatileUpdate")
+	public V put(@NonNull K key, @NonNull V value) {
+		map.put(key, new Entry<>(key, value, ++time));
+		if (map.size() > maximumSize) {
+			purge();
+		}
+		return value;
+	}
+
+	/**
+	 * Returns the current size of this cache
+	 *
+	 * @return the number of key-value mappings in this cache
+	 */
+	public int size() {
+		return map.size();
+	}
+
+	/**
+	 * Reconfigures the cache. If {@code maxSize} is reduced some entries will
+	 * be purged.
+	 *
+	 * @param maxSize
+	 *            maximum size of the cache
+	 *
+	 * @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
+	 */
+	public void configure(int maxSize, float purgeFactor) {
+		lock.lock();
+		try {
+			checkPurgeFactor(purgeFactor);
+			this.maximumSize = maxSize;
+			this.purgeSize = purgeSize(maxSize, purgeFactor);
+			if (map.size() >= maximumSize) {
+				purge();
+			}
+		} finally {
+			lock.unlock();
+		}
+	}
+
+	private void purge() {
+		// don't try to compete if another thread already has the lock
+		if (lock.tryLock()) {
+			try {
+				List<Entry> entriesToPurge = new ArrayList<>(map.values());
+				// copy access times to avoid other threads interfere with
+				// sorting
+				for (Entry e : entriesToPurge) {
+					e.copyAccessTime();
+				}
+				Collections.sort(entriesToPurge,
+						Comparator.comparingLong(o -> -o.lastAccessedSorting));
+				for (int index = purgeSize; index < entriesToPurge
+						.size(); index++) {
+					map.remove(entriesToPurge.get(index).key);
+				}
+			} finally {
+				lock.unlock();
+			}
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java
new file mode 100644
index 0000000..e9307d3
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com>
+ * 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;
+
+/**
+ * Simple double statistics, computed incrementally, variance and standard
+ * deviation using Welford's online algorithm, see
+ * https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm
+ *
+ * @since 5.1.9
+ */
+public class Stats {
+	private int n = 0;
+
+	private double avg = 0.0;
+
+	private double min = 0.0;
+
+	private double max = 0.0;
+
+	private double sum = 0.0;
+
+	/**
+	 * Add a value
+	 *
+	 * @param x
+	 *            value
+	 */
+	public void add(double x) {
+		n++;
+		min = n == 1 ? x : Math.min(min, x);
+		max = n == 1 ? x : Math.max(max, x);
+		double d = x - avg;
+		avg += d / n;
+		sum += d * d * (n - 1) / n;
+	}
+
+	/**
+	 * @return number of the added values
+	 */
+	public int count() {
+		return n;
+	}
+
+	/**
+	 * @return minimum of the added values
+	 */
+	public double min() {
+		if (n < 1) {
+			return Double.NaN;
+		}
+		return min;
+	}
+
+	/**
+	 * @return maximum of the added values
+	 */
+	public double max() {
+		if (n < 1) {
+			return Double.NaN;
+		}
+		return max;
+	}
+
+	/**
+	 * @return average of the added values
+	 */
+
+	public double avg() {
+		if (n < 1) {
+			return Double.NaN;
+		}
+		return avg;
+	}
+
+	/**
+	 * @return variance of the added values
+	 */
+	public double var() {
+		if (n < 2) {
+			return Double.NaN;
+		}
+		return sum / (n - 1);
+	}
+
+	/**
+	 * @return standard deviation of the added values
+	 */
+	public double stddev() {
+		return Math.sqrt(this.var());
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java
index 3868e56..f4b6f9d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java
@@ -139,8 +139,9 @@
 	 * @return true if a equals b
 	 */
 	public static boolean equalsIgnoreCase(String a, String b) {
-		if (a == b)
+		if (References.isSameObject(a, b)) {
 			return true;
+		}
 		if (a.length() != b.length())
 			return false;
 		for (int i = 0; i < a.length(); i++) {
diff --git a/pom.xml b/pom.xml
index 2ad57b5..8da9a88 100644
--- a/pom.xml
+++ b/pom.xml
@@ -194,7 +194,7 @@
     <osgi-core-version>4.3.1</osgi-core-version>
     <servlet-api-version>3.1.0</servlet-api-version>
     <jetty-version>9.4.14.v20181114</jetty-version>
-    <japicmp-version>0.13.0</japicmp-version>
+    <japicmp-version>0.14.1</japicmp-version>
     <httpclient-version>4.5.6</httpclient-version>
     <httpcore-version>4.4.10</httpcore-version>
     <slf4j-version>1.7.2</slf4j-version>
@@ -204,10 +204,11 @@
     <gson-version>2.8.2</gson-version>
     <bouncycastle-version>1.61</bouncycastle-version>
     <spotbugs-maven-plugin-version>3.1.12.2</spotbugs-maven-plugin-version>
-    <maven-surefire-version>2.22.2</maven-surefire-version>
-    <maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version>
     <maven-project-info-reports-plugin-version>3.0.0</maven-project-info-reports-plugin-version>
     <maven-jxr-plugin-version>3.0.0</maven-jxr-plugin-version>
+    <maven-surefire-plugin-version>3.0.0-M3</maven-surefire-plugin-version>
+    <maven-surefire-report-plugin-version>${maven-surefire-plugin-version}</maven-surefire-report-plugin-version>
+    <maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version>
 
     <!-- Properties to enable jacoco code coverage analysis -->
     <sonar.core.codeCoveragePlugin>jacoco</sonar.core.codeCoveragePlugin>
@@ -294,7 +295,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-surefire-plugin</artifactId>
-          <version>${maven-surefire-version}</version>
+          <version>${maven-surefire-plugin-version}</version>
           <configuration>
             <forkCount>${test-fork-count}</forkCount>
             <reuseForks>true</reuseForks>
@@ -383,7 +384,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-surefire-report-plugin</artifactId>
-          <version>${maven-surefire-version}</version>
+          <version>${maven-surefire-report-plugin-version}</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
@@ -395,7 +396,26 @@
           <artifactId>maven-project-info-reports-plugin</artifactId>
           <version>${maven-project-info-reports-plugin-version}</version>
         </plugin>
-
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-deploy-plugin</artifactId>
+          <version>3.0.0-M1</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-install-plugin</artifactId>
+          <version>3.0.0-M1</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>${maven-compiler-plugin-version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-resources-plugin</artifactId>
+          <version>3.1.0</version>
+        </plugin>
         <plugin>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-maven-plugin</artifactId>
@@ -856,7 +876,7 @@
               <dependency>
                 <groupId>org.codehaus.plexus</groupId>
                 <artifactId>plexus-compiler-javac</artifactId>
-                <version>2.8.4</version>
+                <version>2.8.5</version>
               </dependency>
               <dependency>
                 <groupId>org.codehaus.plexus</groupId>
diff --git a/tools/BUILD b/tools/BUILD
index d94ce02..bfe0d6e 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -26,13 +26,13 @@
         "-Xep:ReferenceEquality:WARN",
         "-Xep:StringEquality:WARN",
         "-Xep:WildcardImport:ERROR",
-        "-Xep:AmbiguousMethodReference:WARN",
+        "-Xep:AmbiguousMethodReference:ERROR",
         "-Xep:BadAnnotationImplementation:ERROR",
         "-Xep:BadComparable:WARN",
         "-Xep:BoxedPrimitiveConstructor:ERROR",
         "-Xep:CannotMockFinalClass:ERROR",
         "-Xep:ClassCanBeStatic:ERROR",
-        "-Xep:ClassNewInstance:WARN",
+        "-Xep:ClassNewInstance:ERROR",
         "-Xep:DefaultCharset:ERROR",
         "-Xep:DoubleCheckedLocking:ERROR",
         "-Xep:ElementsCountedInLoop:ERROR",
@@ -47,7 +47,7 @@
         "-Xep:FutureReturnValueIgnored:ERROR",
         "-Xep:GetClassOnEnum:ERROR",
         "-Xep:ImmutableAnnotationChecker:ERROR",
-        "-Xep:ImmutableEnumChecker:WARN",
+        "-Xep:ImmutableEnumChecker:ERROR",
         "-Xep:IncompatibleModifiers:ERROR",
         "-Xep:InjectOnConstructorOfAbstractClass:ERROR",
         "-Xep:InputStreamSlowMultibyteRead:ERROR",
@@ -58,9 +58,9 @@
         "-Xep:MissingFail:ERROR",
         "-Xep:MissingOverride:ERROR",
         "-Xep:MutableConstantField:ERROR",
-        "-Xep:NarrowingCompoundAssignment:WARN",
+        "-Xep:NarrowingCompoundAssignment:ERROR",
         "-Xep:NonAtomicVolatileUpdate:ERROR",
-        "-Xep:NonOverridingEquals:WARN",
+        "-Xep:NonOverridingEquals:ERROR",
         "-Xep:NullableConstructor:ERROR",
         "-Xep:NullablePrimitive:ERROR",
         "-Xep:NullableVoid:ERROR",
@@ -70,7 +70,7 @@
         "-Xep:ProtoFieldPreconditionsCheckNotNull:ERROR",
         "-Xep:ProtocolBufferOrdinal:ERROR",
         "-Xep:RequiredModifiers:ERROR",
-        "-Xep:ShortCircuitBoolean:WARN",
+        "-Xep:ShortCircuitBoolean:ERROR",
         "-Xep:SimpleDateFormatConstant:ERROR",
         "-Xep:StaticGuardedByInstance:ERROR",
         "-Xep:SynchronizeOnNonFinalField:ERROR",