- * 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.
- *
- * 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) <= time < 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.
- *
- * This class was written especially to test racy-git problems
- */
-public class FileTreeIteratorWithTimeControl extends FileTreeIterator {
- private TreeSet modTimes;
-
- public FileTreeIteratorWithTimeControl(FileTreeIterator p, Repository repo,
- TreeSet modTimes) {
- super(p, repo.getWorkTree(), repo.getFS());
- this.modTimes = modTimes;
- }
-
- public FileTreeIteratorWithTimeControl(FileTreeIterator p, File f, FS fs,
- TreeSet modTimes) {
- super(p, f, fs);
- this.modTimes = modTimes;
- }
-
- public FileTreeIteratorWithTimeControl(Repository repo,
- TreeSet modTimes) {
- super(repo);
- this.modTimes = modTimes;
- }
-
- public FileTreeIteratorWithTimeControl(File f, FS fs,
- TreeSet 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 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 59c8e31c0..2054e1efa 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;
@@ -65,6 +66,7 @@ import java.util.concurrent.TimeUnit;
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;
@@ -105,7 +107,7 @@ public class FSTest {
assertTrue(fs.exists(link));
String targetName = fs.readSymLink(link);
assertEquals("å", targetName);
- assertTrue(fs.lastModified(link) > 0);
+ assertTrue(fs.lastModifiedInstant(link).compareTo(EPOCH) > 0);
assertTrue(fs.exists(link));
assertFalse(fs.canExecute(link));
assertEquals(2, fs.length(link));
@@ -118,8 +120,9 @@ public class FSTest {
// 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));
@@ -200,7 +203,8 @@ public class FSTest {
.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++) {
@@ -223,4 +227,11 @@ public class FSTest {
}
}
}
+
+ // 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 000000000..5894f7dc5
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/SimpleLruCacheTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2019, Matthias Sohn
+ * 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 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 000000000..8b253828c
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StatsTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2019, Matthias Sohn
+ * 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/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 4131901d7..2c9b3fe42 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -8,6 +8,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -22,6 +42,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -92,6 +140,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -101,17 +171,73 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index b03ff4ac9..66701b0b3 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -86,7 +86,7 @@ Export-Package: org.eclipse.jgit.annotations;version="5.2.3",
org.eclipse.jgit.pgm",
org.eclipse.jgit.internal.storage.reftree;version="5.2.3";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
org.eclipse.jgit.internal.submodule;version="5.2.3";x-internal:=true,
- org.eclipse.jgit.internal.transport.parser;version="5.2.3";x-friends:="org.eclipse.jgit.test",
+ org.eclipse.jgit.internal.transport.parser;version="5.2.3";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.server",
org.eclipse.jgit.internal.transport.ssh;version="5.2.3";x-friends:="org.eclipse.jgit.ssh.apache",
org.eclipse.jgit.lib;version="5.2.3";
uses:="org.eclipse.jgit.revwalk,
diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml
index c74e26f6d..c30d648c6 100644
--- a/org.eclipse.jgit/pom.xml
+++ b/org.eclipse.jgit/pom.xml
@@ -209,6 +209,7 @@
com.github.spotbugs
spotbugs-maven-plugin
+ ${spotbugs-maven-plugin-version}
findBugs/FindBugsExcludeFilter.xml
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 12fded836..35f3dabe5 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -104,6 +104,7 @@ cannotReadObjectsPath=Cannot read {0}/{1}: {2}
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
@@ -389,6 +390,7 @@ invalidPathContainsSeparator=Invalid path (contains separator ''{0}''): {1}
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}
@@ -558,8 +560,10 @@ pushIsNotSupportedForBundleTransport=Push is not supported for bundle transport
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.
@@ -680,6 +684,7 @@ theFactoryMustNotBeNull=The factory must not be null
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 f0408ab3d..a2cd4aeb2 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 static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
import java.io.IOException;
import java.io.InputStream;
+import java.time.Instant;
import java.util.Collection;
import java.util.LinkedList;
@@ -228,7 +229,7 @@ public class AddCommand extends GitCommand {
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 @@ public class AddCommand extends GitCommand {
}
} 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 27bb5a90b..3f7306bf3 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 java.util.concurrent.ConcurrentMap;
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 @@ public class ArchiveCommand extends GitCommand {
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 @@ public class ArchiveCommand extends GitCommand {
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 @@ public class ArchiveCommand extends GitCommand {
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/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
index d07532c09..cea28fac1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
@@ -392,7 +392,7 @@ public class CommitCommand extends GitCommand {
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/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
index 13ce4e769..d7c9ad5e0 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 @@ public class ResetCommand extends GitCommand[ {
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/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
index 01d070cbd..ff7c4c64b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
@@ -332,7 +332,7 @@ public class StashApplyCommand extends GitCommand {
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 c32890d8a..0d010adb2 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 @@ public class StashCreateCommand extends GitCommand {
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 14653fe17..0cfd16b58 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.io.OutputStream;
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;
@@ -497,8 +498,7 @@ public class DirCache {
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.
//
@@ -507,8 +507,9 @@ public class DirCache {
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.
//
@@ -679,8 +680,8 @@ public class DirCache {
// 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;
@@ -1025,7 +1026,7 @@ public class DirCache {
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 db6073f89..8aa97df77 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.IOException;
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;
@@ -425,8 +426,10 @@ public class DirCacheCheckout {
// 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);
}
} else
@@ -638,7 +641,7 @@ public class DirCacheCheckout {
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 filterOut(ArrayList strings,
@@ -1460,7 +1463,7 @@ public class DirCacheCheckout {
}
fs.createSymLink(f, target);
entry.setLength(bytes.length);
- entry.setLastModified(fs.lastModified(f));
+ entry.setLastModified(fs.lastModifiedInstant(f));
return;
}
@@ -1529,7 +1532,7 @@ public class DirCacheCheckout {
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 6b1d4f4d8..d2a59c131 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.io.OutputStream;
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 @@ public class DirCacheEntry {
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 @@ public class DirCacheEntry {
md.update(nullpad, 0, padLen);
}
- if (mightBeRacilyClean(smudge_s, smudge_ns))
+ if (mightBeRacilyClean(smudge)) {
smudgeRacilyClean();
+ }
}
/**
@@ -344,8 +346,29 @@ public class DirCacheEntry {
* @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?
+ * ]
+ * 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 @@ public class DirCacheEntry {
//
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;
}
@@ -563,21 +587,50 @@ public class DirCacheEntry {
*
* @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.
+ *
+ * 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.
*
@@ -692,7 +745,8 @@ public class DirCacheEntry {
@SuppressWarnings("nls")
@Override
public String toString() {
- return getFileMode() + " " + getLength() + " " + getLastModified()
+ return getFileMode() + " " + getLength() + " "
+ + getLastModifiedInstant()
+ " " + getObjectId() + " " + getStage() + " "
+ getPathString() + "\n";
}
@@ -750,12 +804,25 @@ public class DirCacheEntry {
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/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index 8f884b44f..4914927ad 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -165,6 +165,7 @@ public class JGitText extends TranslationBundle {
/***/ public String cannotReadTree;
/***/ public String cannotRebaseWithoutCurrentHead;
/***/ public String cannotResolveLocalTrackingRefForUpdating;
+ /***/ public String cannotSaveConfig;
/***/ public String cannotSquashFixupWithoutPreviousCommit;
/***/ public String cannotStoreObjects;
/***/ public String cannotResolveUniquelyAbbrevObjectId;
@@ -450,6 +451,7 @@ public class JGitText extends TranslationBundle {
/***/ public String invalidPathPeriodAtEndWindows;
/***/ public String invalidPathSpaceAtEndWindows;
/***/ public String invalidPathReservedOnWindows;
+ /***/ public String invalidPurgeFactor;
/***/ public String invalidRedirectLocation;
/***/ public String invalidRefAdvertisementLine;
/***/ public String invalidReflogRevision;
@@ -619,8 +621,10 @@ public class JGitText extends TranslationBundle {
/***/ 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;
@@ -736,6 +740,7 @@ public class JGitText extends TranslationBundle {
/***/ 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/storage/file/FileSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java
index 1de313500..976f946e5 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 @@ import org.eclipse.jgit.util.FS;
* 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 class FileSnapshot {
*/
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.
*
@@ -90,8 +103,8 @@ public class FileSnapshot {
* 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 @@ public class FileSnapshot {
* 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 @@ public class FileSnapshot {
return new FileSnapshot(path);
}
+ /**
+ * Record a snapshot for a specific file path without using config file to
+ * get filesystem timestamp resolution.
+ *
+ * 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 @@ public class FileSnapshot {
* @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.
+ *
+ * This method should be invoked before the file is accessed.
+ *
+ * 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 @@ public class FileSnapshot {
* 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 @@ public class FileSnapshot {
*/
private final Object fileKey;
+ private final File file;
+
/**
* Record a snapshot for a specific file path.
*
* 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.
+ *
+ * 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 @@ public class FileSnapshot {
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 @@ public class FileSnapshot {
* 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 @@ public class FileSnapshot {
* @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 @@ public class FileSnapshot {
* 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 @@ public class FileSnapshot {
* 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);
}
}
@@ -318,7 +414,8 @@ public class FileSnapshot {
* @return true if the two snapshots share the same information.
*/
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 +438,7 @@ public class FileSnapshot {
/** {@inheritDoc} */
@Override
public int hashCode() {
- return Objects.hash(Long.valueOf(lastModified), Long.valueOf(size),
- fileKey);
+ return Objects.hash(lastModified, Long.valueOf(size), fileKey);
}
/**
@@ -377,6 +473,22 @@ public class FileSnapshot {
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")
@Override
@@ -387,24 +499,46 @@ public class FileSnapshot {
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 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(long currLastModified) {
+ 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 +546,40 @@ public class FileSnapshot {
// 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 3c830e88c..791a10828 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
@@ -372,8 +372,9 @@ public class GC {
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);
@@ -561,8 +562,10 @@ public class GC {
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 b80c58ca9..420e73754 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.io.OutputStream;
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;
@@ -422,9 +426,16 @@ public class LockFile {
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);
}
}
@@ -474,11 +485,22 @@ public class LockFile {
* 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.
*
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 73ad38c95..88e05af41 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.channels.FileChannel.MapMode;
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;
@@ -107,7 +108,7 @@ public class PackFile implements Iterable {
public static final Comparator SORT = new Comparator() {
@Override
public int compare(PackFile a, PackFile b) {
- return b.packLastModified - a.packLastModified;
+ return b.packLastModified.compareTo(a.packLastModified);
}
};
@@ -132,7 +133,7 @@ public class PackFile implements Iterable {
private int activeCopyRawData;
- int packLastModified;
+ Instant packLastModified;
private PackFileSnapshot fileSnapshot;
@@ -172,7 +173,7 @@ public class PackFile implements Iterable {
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/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
index e8a6ba730..c1e94a0a3 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.BufferedReader;
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.annotations.NonNull;
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 @@ public class OpenSshConfigFile {
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 @@ public class OpenSshConfigFile {
}
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/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
index 9763fb72f..5ae9d41db 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 @@ public final class ConfigConstants {
* @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 6a66cf682..fb239399e 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 @@ public class DefaultTypedConfigGetter implements TypedConfigGetter {
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/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
index 412d9bba7..909f3b15d 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.FileOutputStream;
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 @@ public class ResolveMerger extends ThreeWayMerger {
* @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 @@ public class ResolveMerger extends ThreeWayMerger {
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 @@ public class ResolveMerger extends ThreeWayMerger {
// 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 @@ public class ResolveMerger extends ThreeWayMerger {
// 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 @@ public class ResolveMerger extends ThreeWayMerger {
// 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 @@ public class ResolveMerger extends ThreeWayMerger {
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 result = new MergeResult<>(
@@ -822,10 +825,10 @@ public class ResolveMerger extends ThreeWayMerger {
MergeResult 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 @@ public class ResolveMerger extends ThreeWayMerger {
// 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 @@ public class ResolveMerger extends ThreeWayMerger {
? 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/storage/file/FileBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java
index 2b31ebd8e..84cd6adb8 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,11 +148,37 @@ public class FileBasedConfig extends StoredConfig {
*/
@Override
public void load() throws IOException, ConfigInvalidException {
+ load(true);
+ }
+
+ /**
+ * Load the configuration as a Git text style configuration file.
+ *
+ * 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 maxStaleRetries = 5;
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/NetRC.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java
index 8562376aa..7dd019ba2 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.File;
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 @@ public class NetRC {
private File netrc;
- private long lastModified;
+ private Instant lastModified;
private Map hosts = new HashMap<>();
@@ -190,8 +191,10 @@ public class NetRC {
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 @@ public class NetRC {
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/treewalk/FileTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
index 3d25c2314..d432c9445 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.File;
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 @@ public class FileTreeIterator extends WorkingTreeIterator {
}
@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 1fa1db584..ddf916f41 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.CharBuffer;
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;
@@ -645,11 +646,23 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
*
* @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.
*
@@ -924,30 +937,28 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
// 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;
}
/**
@@ -1274,9 +1285,25 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
* 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.
+ *
+ * Note: Efficient implementation required.
+ *
+ * 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.
*
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 fb958872a..10a991939 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.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
+import java.io.OutputStreamWriter;
import java.io.PrintStream;
+import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileStore;
@@ -65,27 +68,39 @@ import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.MessageFormat;
import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
-import java.util.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 @@ public abstract class FS {
}
}
- 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 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);
+
+ private static final Map attributeCache = new ConcurrentHashMap<>();
+
+ private static final SimpleLruCache attrCacheByPath = new SimpleLruCache<>(
+ 100, 0.2f);
+
+ private static AtomicBoolean background = new AtomicBoolean();
+
+ private static Map 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;
+ }
- static Duration getFsTimestampResolution(Path file) {
+ 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> 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 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 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.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 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;
}
- private Duration fsTimestampResolution;
+ @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 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;
+ }
- Duration getFsTimestampResolution() {
- return fsTimestampResolution;
+ private static void write(Path p, String body) throws IOException {
+ FileUtils.mkdirs(p.getParent().toFile(), true);
+ try (Writer w = new OutputStreamWriter(Files.newOutputStream(p),
+ UTF_8)) {
+ w.write(body);
+ }
+ }
+
+ private static String read(Path p) throws IOException {
+ final byte[] body = IO.readFully(p.toFile());
+ return new String(body, 0, body.length, UTF_8);
}
- private FileStoreAttributeCache(Path dir)
- throws IOException, InterruptedException {
+ private static Optional 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$
- 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;
- }
+ 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);
}
- fsTimestampResolution = Duration.between(startTime.toInstant(),
- actTime.toInstant());
+ 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 {
- Files.delete(probe);
+ deleteProbe(probe);
}
+ return Optional.empty();
}
- @SuppressWarnings("nls")
+ 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 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;
+ }
+
+ /**
+ * Construct a FileStoreAttributeCache entry for the given filesystem
+ * timestamp resolution
+ *
+ * @param fsTimestampResolution
+ */
+ public FileStoreAttributes(
+ @NonNull Duration fsTimestampResolution) {
+ this.fsTimestampResolution = fsTimestampResolution;
+ this.minimalRacyInterval = Duration.ZERO;
+ }
+
+ @SuppressWarnings({ "nls", "boxing" })
@Override
public String toString() {
- return "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. */
@@ -279,6 +691,19 @@ public abstract class FS {
return detect(null);
}
+ /**
+ * Whether FileStore attributes should be determined asynchronously
+ *
+ * @param asynch
+ * whether FileStore attributes should be determined
+ * asynchronously. If false access to cached attributes may block
+ * for some seconds for the first call per FileStore
+ * @since 5.1.9
+ */
+ public static void setAsyncFileStoreAttributes(boolean asynch) {
+ FileStoreAttributes.setBackground(asynch);
+ }
+
/**
* Auto-detect the appropriate file system abstraction, taking into account
* the presence of a Cygwin installation on the system. Using jgit in
@@ -307,18 +732,18 @@ public abstract class FS {
}
/**
- * 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 userHome;
@@ -432,11 +857,41 @@ public abstract class FS {
* @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,11 +902,28 @@ public abstract class FS {
* 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.
@@ -1526,9 +1998,19 @@ public abstract class FS {
/**
* @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;
@@ -1539,7 +2021,7 @@ public abstract class FS {
private final long creationTime;
- private final long lastModifiedTime;
+ private final Instant lastModifiedInstant;
private final boolean isExecutable;
@@ -1557,7 +2039,7 @@ public abstract class FS {
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;
@@ -1566,7 +2048,7 @@ public abstract class FS {
this.isSymbolicLink = isSymbolicLink;
this.isRegularFile = isRegularFile;
this.creationTime = creationTime;
- this.lastModifiedTime = lastModifiedTime;
+ this.lastModifiedInstant = lastModifiedInstant;
this.length = length;
}
@@ -1578,7 +2060,7 @@ public abstract class FS {
* @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);
}
/**
@@ -1624,7 +2106,7 @@ public abstract class FS {
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_Win32.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java
index 98797dc64..7c0727036 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 @@ public class FS_Win32 extends FS {
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 9bba6ca8a..80f188cb2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
@@ -49,7 +49,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.File;
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;
@@ -57,6 +57,7 @@ import java.nio.file.InvalidPathException;
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;
@@ -66,6 +67,7 @@ import java.nio.file.attribute.PosixFilePermission;
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;
@@ -74,11 +76,14 @@ import java.util.regex.Pattern;
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}
@@ -654,12 +659,31 @@ public class FileUtils {
* @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.
*
@@ -678,10 +702,21 @@ public class FileUtils {
* @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
@@ -786,7 +821,7 @@ public class FileUtils {
readAttributes.isSymbolicLink(),
readAttributes.isRegularFile(), //
readAttributes.creationTime().toMillis(), //
- readAttributes.lastModifiedTime().toMillis(),
+ readAttributes.lastModifiedTime().toInstant(),
readAttributes.isSymbolicLink() ? Constants
.encode(readSymLink(file)).length
: readAttributes.size());
@@ -825,7 +860,7 @@ public class FileUtils {
readAttributes.isSymbolicLink(),
readAttributes.isRegularFile(), //
readAttributes.creationTime().toMillis(), //
- readAttributes.lastModifiedTime().toMillis(),
+ readAttributes.lastModifiedTime().toInstant(),
readAttributes.size());
return attributes;
} catch (IOException e) {
@@ -916,11 +951,13 @@ public class FileUtils {
* @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/SimpleLruCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SimpleLruCache.java
new file mode 100644
index 000000000..709d9ee73
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SimpleLruCache.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2019, Marc Strapetz
+ * Copyright (C) 2019, Matthias Sohn
+ * 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
+ * the type of keys maintained by this cache
+ * @param
+ * the type of mapped values
+ *
+ * @since 5.1.9
+ */
+public class SimpleLruCache {
+
+ private static class Entry {
+
+ 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> 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.
+ *
+ *
+ * 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
+ */
+ public V get(Object key) {
+ Entry 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.
+ *
+ *
+ * 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
+ */
+ 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 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 000000000..e9307d342
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019, Matthias Sohn
+ * 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/pom.xml b/pom.xml
index 25fa24d61..a7d8c8014 100644
--- a/pom.xml
+++ b/pom.xml
@@ -194,19 +194,20 @@
4.3.1
3.1.0
9.4.11.v20180605
- 0.13.0
+ 0.14.1
4.5.5
4.4.9
1.7.2
1.2.15
- 3.0.1
- 1.2.0
+ 3.1.0
+ 1.3.0
2.8.2
- 3.1.8
- 2.22.1
- 3.8.0
3.0.0
3.0.0
+ 3.1.12
+ 3.0.0-M3
+ ${maven-surefire-plugin-version}
+ 3.8.1
jacoco
@@ -238,7 +239,7 @@
org.apache.maven.plugins
maven-jar-plugin
- 3.1.0
+ 3.1.2
@@ -281,7 +282,7 @@
org.apache.maven.plugins
maven-source-plugin
- 3.0.1
+ 3.1.0
@@ -293,7 +294,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- ${maven-surefire-version}
+ ${maven-surefire-plugin-version}
${test-fork-count}
true
@@ -326,7 +327,7 @@
org.apache.maven.plugins
maven-pmd-plugin
- 3.11.0
+ 3.12.0
utf-8
100
@@ -364,7 +365,7 @@
org.jacoco
jacoco-maven-plugin
- 0.8.2
+ 0.8.4
org.apache.maven.plugins
@@ -381,7 +382,7 @@
org.apache.maven.plugins
maven-surefire-report-plugin
- ${maven-surefire-version}
+ ${maven-surefire-report-plugin-version}
org.apache.maven.plugins
@@ -393,6 +394,26 @@
maven-project-info-reports-plugin
${maven-project-info-reports-plugin-version}
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 3.0.0-M1
+
+
+ org.apache.maven.plugins
+ maven-install-plugin
+ 3.0.0-M1
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven-compiler-plugin-version}
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.1.0
+
@@ -778,19 +799,19 @@
org.codehaus.plexus
plexus-compiler-javac
- 2.8.4
+ 2.8.5
org.codehaus.plexus
plexus-compiler-javac-errorprone
- 2.8.4
+ 2.8.5
com.google.errorprone
error_prone_core
- 2.3.2
+ 2.3.3
@@ -823,12 +844,12 @@
org.codehaus.plexus
plexus-compiler-eclipse
- 2.8.4
+ 2.8.5
org.eclipse.jdt
ecj
- 3.15.0
+ 3.17.0