aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit')
-rw-r--r--org.eclipse.jgit/.settings/.api_filters132
-rw-r--r--org.eclipse.jgit/META-INF/MANIFEST.MF2
-rw-r--r--org.eclipse.jgit/pom.xml1
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java35
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java15
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java79
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java244
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java9
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java26
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java7
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java8
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java19
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java8
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java45
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java28
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java9
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java9
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java67
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java604
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java49
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/SimpleLruCache.java253
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java132
31 files changed, 1613 insertions, 207 deletions
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 4131901d78..2c9b3fe422 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -8,6 +8,26 @@
</message_arguments>
</filter>
</resource>
+ <resource path="src/org/eclipse/jgit/dircache/DirCacheEntry.java" type="org.eclipse.jgit.dircache.DirCacheEntry">
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="getLastModifiedInstant()"/>
+ </message_arguments>
+ </filter>
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="mightBeRacilyClean(Instant)"/>
+ </message_arguments>
+ </filter>
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="setLastModified(Instant)"/>
+ </message_arguments>
+ </filter>
+ </resource>
<resource path="src/org/eclipse/jgit/errors/PackInvalidException.java" type="org.eclipse.jgit.errors.PackInvalidException">
<filter id="1142947843">
<message_arguments>
@@ -22,6 +42,34 @@
</message_arguments>
</filter>
</resource>
+ <resource path="src/org/eclipse/jgit/lib/ConfigConstants.java" type="org.eclipse.jgit.lib.ConfigConstants">
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="CONFIG_FILESYSTEM_SECTION"/>
+ </message_arguments>
+ </filter>
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="CONFIG_KEY_MIN_RACY_THRESHOLD"/>
+ </message_arguments>
+ </filter>
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="CONFIG_KEY_TIMESTAMP_RESOLUTION"/>
+ </message_arguments>
+ </filter>
+ </resource>
+ <resource path="src/org/eclipse/jgit/storage/file/FileBasedConfig.java" type="org.eclipse.jgit.storage.file.FileBasedConfig">
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="load(boolean)"/>
+ </message_arguments>
+ </filter>
+ </resource>
<resource path="src/org/eclipse/jgit/storage/pack/PackConfig.java" type="org.eclipse.jgit.storage.pack.PackConfig">
<filter id="336658481">
<message_arguments>
@@ -92,6 +140,28 @@
</message_arguments>
</filter>
</resource>
+ <resource path="src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java" type="org.eclipse.jgit.treewalk.WorkingTreeIterator">
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="getEntryLastModifiedInstant()"/>
+ </message_arguments>
+ </filter>
+ </resource>
+ <resource path="src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java" type="org.eclipse.jgit.treewalk.WorkingTreeIterator$Entry">
+ <filter id="336695337">
+ <message_arguments>
+ <message_argument value="org.eclipse.jgit.treewalk.WorkingTreeIterator.Entry"/>
+ <message_argument value="getLastModifiedInstant()"/>
+ </message_arguments>
+ </filter>
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="getLastModifiedInstant()"/>
+ </message_arguments>
+ </filter>
+ </resource>
<resource path="src/org/eclipse/jgit/util/FS.java" type="org.eclipse.jgit.util.FS">
<filter id="1142947843">
<message_arguments>
@@ -101,17 +171,73 @@
</filter>
<filter id="1142947843">
<message_arguments>
- <message_argument value="5.2.3"/>
- <message_argument value="getFsTimerResolution(Path)"/>
+ <message_argument value="5.1.9"/>
+ <message_argument value="getFileStoreAttributes(Path)"/>
+ </message_arguments>
+ </filter>
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="lastModifiedInstant(File)"/>
+ </message_arguments>
+ </filter>
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="lastModifiedInstant(Path)"/>
+ </message_arguments>
+ </filter>
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="setAsyncFileStoreAttributes(boolean)"/>
+ </message_arguments>
+ </filter>
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="setLastModified(Path, Instant)"/>
+ </message_arguments>
+ </filter>
+ </resource>
+ <resource path="src/org/eclipse/jgit/util/FS.java" type="org.eclipse.jgit.util.FS$Attributes">
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="getLastModifiedInstant()"/>
+ </message_arguments>
+ </filter>
+ </resource>
+ <resource path="src/org/eclipse/jgit/util/FS.java" type="org.eclipse.jgit.util.FS$FileStoreAttributes">
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="FileStoreAttributes"/>
</message_arguments>
</filter>
</resource>
<resource path="src/org/eclipse/jgit/util/FileUtils.java" type="org.eclipse.jgit.util.FileUtils">
<filter id="1142947843">
<message_arguments>
- <message_argument value="5.2.3"/>
+ <message_argument value="5.1.8"/>
<message_argument value="touch(Path)"/>
</message_arguments>
</filter>
</resource>
+ <resource path="src/org/eclipse/jgit/util/SimpleLruCache.java" type="org.eclipse.jgit.util.SimpleLruCache">
+ <filter id="1109393411">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="org.eclipse.jgit.util.SimpleLruCache"/>
+ </message_arguments>
+ </filter>
+ </resource>
+ <resource path="src/org/eclipse/jgit/util/Stats.java" type="org.eclipse.jgit.util.Stats">
+ <filter id="1109393411">
+ <message_arguments>
+ <message_argument value="5.1.9"/>
+ <message_argument value="org.eclipse.jgit.util.Stats"/>
+ </message_arguments>
+ </filter>
+ </resource>
</component>
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index b03ff4ac94..66701b0b38 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 c74e26f6d4..c30d648c69 100644
--- a/org.eclipse.jgit/pom.xml
+++ b/org.eclipse.jgit/pom.xml
@@ -209,6 +209,7 @@
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
+ <version>${spotbugs-maven-plugin-version}</version>
<configuration>
<excludeFilterFile>findBugs/FindBugsExcludeFilter.xml</excludeFilterFile>
</configuration>
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index 12fded8361..35f3dabe53 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 f0408ab3d4..a2cd4aeb2a 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<DirCache> {
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<DirCache> {
}
} 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 27bb5a90b9..3f7306bf37 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<OutputStream> {
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<OutputStream> {
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<OutputStream> {
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 d07532c092..cea28fac18 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<RevCommit> {
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 13ce4e7690..d7c9ad5e04 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<Ref> {
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 01d070cbd9..ff7c4c64bc 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<ObjectId> {
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 c32890d8a4..0d010adb26 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<RevCommit> {
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 14653fe17f..0cfd16b58a 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 db6073f89a..8aa97df777 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<String> filterOut(ArrayList<String> 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 6b1d4f4d8a..d2a59c1310 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?
+ * <p>
+ * The "racy git" problem happens when a work file can be updated faster
+ * than the filesystem records file modification timestamps. It is possible
+ * for an application to edit a work file, update the index, then edit it
+ * again before the filesystem will give the work file a new modification
+ * timestamp. This method tests to see if file was written out at the same
+ * time as the index.
+ *
+ * @param smudge
+ * index's last modified time.
+ * @return true if extra careful checks should be used.
+ * @since 5.1.9
+ */
+ public final boolean mightBeRacilyClean(Instant smudge) {
// If the index has a modification time then it came from disk
// and was not generated from scratch in memory. In such cases
// the entry is 'racily clean' if the entry's cached modification
@@ -355,8 +378,9 @@ 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,22 +587,51 @@ 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.
+ * <p>
+ * One of the indicators that the file has been modified by an application
+ * changing the working tree is if the last modification time for the file
+ * differs from the time stored in this entry.
+ *
+ * @return last modification time of this file.
+ * @since 5.1.9
+ */
+ public Instant getLastModifiedInstant() {
+ return decodeTSInstant(P_MTIME);
+ }
+
+ /**
* Set the cached last modification date of this file, using milliseconds.
*
* @param when
* new cached modification date of the file, in milliseconds.
+ * @deprecated use {@link #setLastModified(Instant)} instead
*/
+ @Deprecated
public void setLastModified(long when) {
encodeTS(P_MTIME, when);
}
/**
+ * Set the cached last modification date of this file.
+ *
+ * @param when
+ * new cached modification date of the file.
+ * @since 5.1.9
+ */
+ public void setLastModified(Instant when) {
+ encodeTS(P_MTIME, when);
+ }
+
+ /**
* Get the cached size (mod 4 GB) (in bytes) of this file.
* <p>
* One of the indicators that the file has been modified by an application
@@ -692,7 +745,8 @@ 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 8f884b44f5..4914927ad5 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 1de3135001..976f946e5d 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.
* <p>
@@ -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.
+ * <p>
+ * This method should be invoked before the file is accessed. It is used by
+ * FileBasedConfig to avoid endless recursion.
+ *
+ * @param path
+ * the path to later remember. The path's current status
+ * information is saved.
+ * @return the snapshot.
+ */
+ public static FileSnapshot saveNoConfig(File path) {
+ return new FileSnapshot(path, false);
+ }
+
private static Object getFileKey(BasicFileAttributes fileAttributes) {
Object fileKey = fileAttributes.fileKey();
return fileKey == null ? MISSING_FILEKEY : fileKey;
@@ -141,18 +170,41 @@ 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.
+ * <p>
+ * This method should be invoked before the file is accessed.
+ * <p>
+ * Note that this method cannot rely on measuring file timestamp resolution
+ * to avoid racy git issues caused by finite file timestamp resolution since
+ * it's unknown in which filesystem the file is located. Hence the worst
+ * case fallback for timestamp resolution is used.
+ *
+ * @param modified
+ * the last modification time of the file
+ * @return the snapshot.
+ */
+ public static FileSnapshot save(Instant modified) {
+ final Instant read = Instant.now();
+ return new FileSnapshot(read, modified, UNKNOWN_SIZE,
+ FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY);
}
/** Last observed modification time of the path. */
- private final long lastModified;
+ private final Instant lastModified;
/** Last wall-clock time the path was read. */
- private volatile long lastRead;
+ private volatile Instant lastRead;
/** True once {@link #lastRead} is far later than {@link #lastModified}. */
private boolean cannotBeRacilyClean;
@@ -162,8 +214,8 @@ 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.
* <p>
* This method should be invoked before the file is accessed.
*
- * @param path
- * the path to later remember. The path's current status
+ * @param file
+ * the path to remember meta data for. The path's current status
* information is saved.
*/
- protected FileSnapshot(File path) {
- this.lastRead = System.currentTimeMillis();
- this.fsTimestampResolution = FS
- .getFsTimerResolution(path.toPath().getParent());
+ protected FileSnapshot(File file) {
+ this(file, true);
+ }
+
+ /**
+ * Record a snapshot for a specific file path.
+ * <p>
+ * This method should be invoked before the file is accessed.
+ *
+ * @param file
+ * the path to remember meta data for. The path's current status
+ * information is saved.
+ * @param useConfig
+ * if {@code true} read filesystem time resolution from
+ * configuration file otherwise use fallback resolution
+ */
+ protected FileSnapshot(File file, boolean useConfig) {
+ this.file = file;
+ this.lastRead = Instant.now();
+ this.fileStoreAttributeCache = useConfig
+ ? FS.getFileStoreAttributes(file.toPath().getParent())
+ : FALLBACK_FILESTORE_ATTRIBUTES;
BasicFileAttributes fileAttributes = null;
try {
- fileAttributes = FS.DETECTED.fileAttributes(path);
+ fileAttributes = FS.DETECTED.fileAttributes(file);
} catch (IOException e) {
- this.lastModified = path.lastModified();
- this.size = path.length();
+ this.lastModified = Instant.ofEpochMilli(file.lastModified());
+ this.size = file.length();
this.fileKey = MISSING_FILEKEY;
return;
}
- this.lastModified = fileAttributes.lastModifiedTime().toMillis();
+ this.lastModified = fileAttributes.lastModifiedTime().toInstant();
this.size = fileAttributes.size();
this.fileKey = getFileKey(fileAttributes);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("file={}, create new FileSnapshot: lastRead={}, lastModified={}, size={}, fileKey={}", //$NON-NLS-1$
+ file, dateFmt.format(lastRead),
+ dateFmt.format(lastModified), Long.valueOf(size),
+ fileKey.toString());
+ }
}
private boolean sizeChanged;
@@ -206,11 +284,17 @@ 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 3c830e88c1..791a108289 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 b80c58ca9c..420e737543 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,12 +485,23 @@ 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.
*
* @return 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 73ad38c95a..88e05af414 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<PackIndex.MutableEntry> {
public static final Comparator<PackFile> SORT = new Comparator<PackFile>() {
@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<PackIndex.MutableEntry> {
private int activeCopyRawData;
- int packLastModified;
+ Instant packLastModified;
private PackFileSnapshot fileSnapshot;
@@ -172,7 +173,7 @@ public class PackFile implements Iterable<PackIndex.MutableEntry> {
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 e8a6ba7308..c1e94a0a3e 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 9763fb72fe..5ae9d41db2 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 6a66cf682f..fb239399ed 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 412d9bba73..909f3b15d8 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<SubmoduleConflict> result = new MergeResult<>(
@@ -822,10 +825,10 @@ public class ResolveMerger extends ThreeWayMerger {
MergeResult<RawText> result = contentMerge(base, ours, theirs,
attributes);
- add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0);
- add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0);
+ add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
+ add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
DirCacheEntry e = add(tw.getRawPath(), theirs,
- DirCacheEntry.STAGE_3, 0, 0);
+ DirCacheEntry.STAGE_3, EPOCH, 0);
// OURS was deleted checkout THEIRS
if (modeO == 0) {
@@ -957,9 +960,9 @@ 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 2b31ebd8e3..84cd6adb8d 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.
+ * <p>
+ * If the file does not exist, this configuration is cleared, and thus
+ * behaves the same as though the file exists, but is empty.
+ *
+ * @param useFileSnapshotWithConfig
+ * if {@code true} use the FileSnapshot with config, otherwise
+ * use it without config
+ * @throws IOException
+ * if IO failed
+ * @throws ConfigInvalidException
+ * if config is invalid
+ * @since 5.1.9
+ */
+ public void load(boolean useFileSnapshotWithConfig)
+ throws IOException, ConfigInvalidException {
final int 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 8562376aa9..7dd019ba27 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<String, NetRCEntry> 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 3d25c2314e..d432c94450 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 1fa1db5847..ddf916f41f 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,12 +646,24 @@ 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.
* <p>
* Efficient implementations are not required. The caller will usually
@@ -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,10 +1285,26 @@ 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.
+ * <p>
+ * <b>Note: Efficient implementation required.</b>
+ * <p>
+ * The implementation of this method must be efficient. If a subclass
+ * needs to compute the value they should cache the reference within an
+ * instance member instead.
+ *
+ * @return time of the last change.
+ * @since 5.1.9
+ */
+ public abstract Instant getLastModifiedInstant();
+
+ /**
* Get the name of this entry within its directory.
* <p>
* Efficient implementations are not required. The caller will obtain
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
index fb958872ac..10a9919391 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<FileStore, FileStoreAttributeCache> attributeCache = new ConcurrentHashMap<>();
+ /**
+ * Fallback FileStore attributes used when we can't measure the
+ * filesystem timestamp resolution. The last modified time granularity
+ * of FAT filesystems is 2 seconds.
+ */
+ public static final FileStoreAttributes FALLBACK_FILESTORE_ATTRIBUTES = new FileStoreAttributes(
+ FALLBACK_TIMESTAMP_RESOLUTION);
+
+ private static final Map<FileStore, FileStoreAttributes> attributeCache = new ConcurrentHashMap<>();
+
+ private static final SimpleLruCache<Path, FileStoreAttributes> attrCacheByPath = new SimpleLruCache<>(
+ 100, 0.2f);
+
+ private static AtomicBoolean background = new AtomicBoolean();
+
+ private static Map<FileStore, Lock> locks = new ConcurrentHashMap<>();
+
+ private static void setBackground(boolean async) {
+ background.set(async);
+ }
+
+ private static final String javaVersionPrefix = System
+ .getProperty("java.vendor") + '|' //$NON-NLS-1$
+ + System.getProperty("java.version") + '|'; //$NON-NLS-1$
+
+ private static final Duration FALLBACK_MIN_RACY_INTERVAL = Duration
+ .ofMillis(10);
+
+ /**
+ * Configures size and purge factor of the path-based cache for file
+ * system attributes. Caching of file system attributes avoids recurring
+ * lookup of @{code FileStore} of files which may be expensive on some
+ * platforms.
+ *
+ * @param maxSize
+ * maximum size of the cache, default is 100
+ * @param purgeFactor
+ * when the size of the map reaches maxSize the oldest
+ * entries will be purged to free up some space for new
+ * entries, {@code purgeFactor} is the fraction of
+ * {@code maxSize} to purge when this happens
+ * @since 5.1.9
+ */
+ public static void configureAttributesPathCache(int maxSize,
+ float purgeFactor) {
+ FileStoreAttributes.attrCacheByPath.configure(maxSize, purgeFactor);
+ }
+
+ /**
+ * Get the FileStoreAttributes for the given FileStore
+ *
+ * @param path
+ * file residing in the FileStore to get attributes for
+ * @return FileStoreAttributes for the given path.
+ */
+ public static FileStoreAttributes get(Path path) {
+ path = path.toAbsolutePath();
+ Path dir = Files.isDirectory(path) ? path : path.getParent();
+ FileStoreAttributes cached = attrCacheByPath.get(dir);
+ if (cached != null) {
+ return cached;
+ }
+ FileStoreAttributes attrs = getFileStoreAttributes(dir);
+ attrCacheByPath.put(dir, attrs);
+ return attrs;
+ }
- 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<Optional<FileStoreAttributes>> f = CompletableFuture
+ .supplyAsync(() -> {
+ Lock lock = locks.computeIfAbsent(s,
+ l -> new ReentrantLock());
+ if (!lock.tryLock()) {
+ LOG.debug(
+ "{}: couldn't get lock to measure timestamp resolution in {}", //$NON-NLS-1$
+ Thread.currentThread(), dir);
+ return Optional.empty();
+ }
+ Optional<FileStoreAttributes> attributes = Optional
+ .empty();
+ try {
+ // Some earlier future might have set the value
+ // and removed itself since we checked for the
+ // value above. Hence check cache again.
+ FileStoreAttributes c = attributeCache
+ .get(s);
+ if (c != null) {
+ return Optional.of(c);
+ }
+ attributes = readFromConfig(s);
+ if (attributes.isPresent()) {
+ attributeCache.put(s, attributes.get());
+ return attributes;
+ }
+
+ Optional<Duration> resolution = measureFsTimestampResolution(
+ s, dir);
+ if (resolution.isPresent()) {
+ c = new FileStoreAttributes(
+ resolution.get());
+ attributeCache.put(s, c);
+ // for high timestamp resolution measure
+ // minimal racy interval
+ if (c.fsTimestampResolution
+ .toNanos() < 100_000_000L) {
+ c.minimalRacyInterval = measureMinimalRacyInterval(
+ dir);
+ }
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(c.toString());
+ }
+ saveToConfig(s, c);
+ }
+ attributes = Optional.of(c);
+ } finally {
+ lock.unlock();
+ locks.remove(s);
+ }
+ return attributes;
+ });
+ f.exceptionally(e -> {
+ LOG.error(e.getLocalizedMessage(), e);
+ return Optional.empty();
+ });
+ // even if measuring in background wait a little - if the result
+ // arrives, it's better than returning the large fallback
+ Optional<FileStoreAttributes> d = background.get() ? f.get(
+ 100, TimeUnit.MILLISECONDS) : f.get();
+ if (d.isPresent()) {
+ return d.get();
+ }
+ // return fallback until measurement is finished
+ } catch (IOException | InterruptedException
+ | ExecutionException | CancellationException e) {
+ LOG.error(e.getMessage(), e);
+ } catch (TimeoutException | SecurityException e) {
+ // use fallback
}
+ LOG.debug("{}: use fallback timestamp resolution for directory {}", //$NON-NLS-1$
+ Thread.currentThread(), dir);
+ return FALLBACK_FILESTORE_ATTRIBUTES;
}
- 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<Long> deltas = new ArrayList<>();
+ Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$
+ Instant end = Instant.now().plusSeconds(3);
+ try {
+ Files.createFile(probe);
+ do {
+ n++;
+ write(probe, "a"); //$NON-NLS-1$
+ FileSnapshot snapshot = FileSnapshot.save(probe.toFile());
+ read(probe);
+ write(probe, "b"); //$NON-NLS-1$
+ if (!snapshot.isModified(probe.toFile())) {
+ deltas.add(Long.valueOf(snapshot.lastDelta()));
+ racyNanos = snapshot.lastRacyThreshold();
+ failures++;
+ }
+ } while (Instant.now().compareTo(end) < 0);
+ } catch (IOException e) {
+ LOG.error(e.getMessage(), e);
+ return FALLBACK_MIN_RACY_INTERVAL;
+ } finally {
+ deleteProbe(probe);
+ }
+ if (failures > 0) {
+ Stats stats = new Stats();
+ for (Long d : deltas) {
+ stats.add(d);
+ }
+ LOG.debug(
+ "delta [ns] since modification FileSnapshot failed to detect\n" //$NON-NLS-1$
+ + "count, failures, racy limit [ns], delta min [ns]," //$NON-NLS-1$
+ + " delta max [ns], delta avg [ns]," //$NON-NLS-1$
+ + " delta stddev [ns]\n" //$NON-NLS-1$
+ + "{}, {}, {}, {}, {}, {}, {}", //$NON-NLS-1$
+ n, failures, racyNanos, stats.min(), stats.max(),
+ stats.avg(), stats.stddev());
+ return Duration
+ .ofNanos(Double.valueOf(stats.max()).longValue());
+ }
+ // since no failures occurred using the measured filesystem
+ // timestamp resolution there is no need for minimal racy interval
+ LOG.debug("{}: no failures when measuring minimal racy interval", //$NON-NLS-1$
+ Thread.currentThread());
+ return Duration.ZERO;
+ }
- 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<Duration> measureFsTimestampResolution(
+ FileStore s, Path dir) {
+ LOG.debug("{}: start measure timestamp resolution {} in {}", //$NON-NLS-1$
+ Thread.currentThread(), s, dir);
Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$
- 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<FileStoreAttributes> readFromConfig(
+ FileStore s) {
+ FileBasedConfig userConfig = SystemReader.getInstance()
+ .openUserConfig(null, FS.DETECTED);
+ try {
+ userConfig.load(false);
+ } catch (IOException e) {
+ LOG.error(MessageFormat.format(JGitText.get().readConfigFailed,
+ userConfig.getFile().getAbsolutePath()), e);
+ } catch (ConfigInvalidException e) {
+ LOG.error(MessageFormat.format(
+ JGitText.get().repositoryConfigFileInvalid,
+ userConfig.getFile().getAbsolutePath(),
+ e.getMessage()));
+ }
+ String key = getConfigKey(s);
+ Duration resolution = Duration.ofNanos(userConfig.getTimeUnit(
+ ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
+ ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION,
+ UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS));
+ if (UNDEFINED_DURATION.equals(resolution)) {
+ return Optional.empty();
+ }
+ Duration minRacyThreshold = Duration.ofNanos(userConfig.getTimeUnit(
+ ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
+ ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD,
+ UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS));
+ FileStoreAttributes c = new FileStoreAttributes(resolution);
+ if (!UNDEFINED_DURATION.equals(minRacyThreshold)) {
+ c.minimalRacyInterval = minRacyThreshold;
+ }
+ return Optional.of(c);
+ }
+
+ private static void saveToConfig(FileStore s,
+ FileStoreAttributes c) {
+ FileBasedConfig userConfig = SystemReader.getInstance()
+ .openUserConfig(null, FS.DETECTED);
+ long resolution = c.getFsTimestampResolution().toNanos();
+ TimeUnit resolutionUnit = getUnit(resolution);
+ long resolutionValue = resolutionUnit.convert(resolution,
+ TimeUnit.NANOSECONDS);
+
+ long minRacyThreshold = c.getMinimalRacyInterval().toNanos();
+ TimeUnit minRacyThresholdUnit = getUnit(minRacyThreshold);
+ long minRacyThresholdValue = minRacyThresholdUnit
+ .convert(minRacyThreshold, TimeUnit.NANOSECONDS);
+
+ final int max_retries = 5;
+ int retries = 0;
+ boolean succeeded = false;
+ String key = getConfigKey(s);
+ while (!succeeded && retries < max_retries) {
+ try {
+ userConfig.load(false);
+ userConfig.setString(
+ ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
+ ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION,
+ String.format("%d %s", //$NON-NLS-1$
+ Long.valueOf(resolutionValue),
+ resolutionUnit.name().toLowerCase()));
+ userConfig.setString(
+ ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
+ ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD,
+ String.format("%d %s", //$NON-NLS-1$
+ Long.valueOf(minRacyThresholdValue),
+ minRacyThresholdUnit.name().toLowerCase()));
+ userConfig.save();
+ succeeded = true;
+ } catch (LockFailedException e) {
+ // race with another thread, wait a bit and try again
+ try {
+ LOG.warn(MessageFormat.format(JGitText.get().cannotLock,
+ userConfig.getFile().getAbsolutePath()));
+ retries++;
+ Thread.sleep(20);
+ } catch (InterruptedException e1) {
+ Thread.interrupted();
+ }
+ } catch (IOException e) {
+ LOG.error(MessageFormat.format(
+ JGitText.get().cannotSaveConfig,
+ userConfig.getFile().getAbsolutePath()), e);
+ } catch (ConfigInvalidException e) {
+ LOG.error(MessageFormat.format(
+ JGitText.get().repositoryConfigFileInvalid,
+ userConfig.getFile().getAbsolutePath(),
+ e.getMessage()));
+ }
+ }
+ }
+
+ private static String getConfigKey(FileStore s) {
+ final String storeKey;
+ if (SystemReader.getInstance().isWindows()) {
+ Object attribute = null;
+ try {
+ attribute = s.getAttribute("volume:vsn"); //$NON-NLS-1$
+ } catch (IOException ignored) {
+ // ignore
+ }
+ if (attribute instanceof Integer) {
+ storeKey = attribute.toString();
+ } else {
+ storeKey = s.name();
+ }
+ } else {
+ storeKey = s.name();
+ }
+ return javaVersionPrefix + storeKey;
+ }
+
+ private static TimeUnit getUnit(long nanos) {
+ TimeUnit unit;
+ if (nanos < 200_000L) {
+ unit = TimeUnit.NANOSECONDS;
+ } else if (nanos < 200_000_000L) {
+ unit = TimeUnit.MICROSECONDS;
+ } else {
+ unit = TimeUnit.MILLISECONDS;
+ }
+ return unit;
+ }
+
+ private final @NonNull Duration fsTimestampResolution;
+
+ private Duration minimalRacyInterval;
+
+ /**
+ * @return the measured minimal interval after a file has been modified
+ * in which we cannot rely on lastModified to detect
+ * modifications
+ */
+ public Duration getMinimalRacyInterval() {
+ return minimalRacyInterval;
+ }
+
+ /**
+ * @return the measured filesystem timestamp resolution
+ */
+ @NonNull
+ public Duration getFsTimestampResolution() {
+ return fsTimestampResolution;
+ }
+
+ /**
+ * 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. */
@@ -280,6 +692,19 @@ public abstract class FS {
}
/**
+ * Whether FileStore attributes should be determined asynchronously
+ *
+ * @param asynch
+ * whether FileStore attributes should be determined
+ * asynchronously. If false access to cached attributes may block
+ * for some seconds for the first call per FileStore
+ * @since 5.1.9
+ */
+ public static void setAsyncFileStoreAttributes(boolean asynch) {
+ FileStoreAttributes.setBackground(asynch);
+ }
+
+ /**
* Auto-detect the appropriate file system abstraction, taking into account
* the presence of a Cygwin installation on the system. Using jgit in
* combination with Cygwin requires a more elaborate (and possibly slower)
@@ -307,18 +732,18 @@ 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<File> userHome;
@@ -432,12 +857,42 @@ 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,12 +902,29 @@ 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 98797dc64f..7c07270363 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 9bba6ca8a3..80f188cb2c 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,13 +659,32 @@ 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.
*
* @param file
@@ -678,11 +702,22 @@ 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
* links
@@ -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 0000000000..709d9ee73d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SimpleLruCache.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2019, Marc Strapetz <marc.strapetz@syntevo.com>
+ * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.util;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.JGitText;
+
+/**
+ * Simple limited size cache based on ConcurrentHashMap purging entries in LRU
+ * order when reaching size limit
+ *
+ * @param <K>
+ * the type of keys maintained by this cache
+ * @param <V>
+ * the type of mapped values
+ *
+ * @since 5.1.9
+ */
+public class SimpleLruCache<K, V> {
+
+ private static class Entry<K, V> {
+
+ private final K key;
+
+ private final V value;
+
+ // pseudo clock timestamp of the last access to this entry
+ private volatile long lastAccessed;
+
+ private long lastAccessedSorting;
+
+ Entry(K key, V value, long lastAccessed) {
+ this.key = key;
+ this.value = value;
+ this.lastAccessed = lastAccessed;
+ }
+
+ void copyAccessTime() {
+ lastAccessedSorting = lastAccessed;
+ }
+
+ @SuppressWarnings("nls")
+ @Override
+ public String toString() {
+ return "Entry [lastAccessed=" + lastAccessed + ", key=" + key
+ + ", value=" + value + "]";
+ }
+ }
+
+ private Lock lock = new ReentrantLock();
+
+ private Map<K, Entry<K,V>> map = new ConcurrentHashMap<>();
+
+ private volatile int maximumSize;
+
+ private int purgeSize;
+
+ // pseudo clock to implement LRU order of access to entries
+ private volatile long time = 0L;
+
+ private static void checkPurgeFactor(float purgeFactor) {
+ if (purgeFactor <= 0 || purgeFactor >= 1) {
+ throw new IllegalArgumentException(
+ MessageFormat.format(JGitText.get().invalidPurgeFactor,
+ Float.valueOf(purgeFactor)));
+ }
+ }
+
+ private static int purgeSize(int maxSize, float purgeFactor) {
+ return (int) ((1 - purgeFactor) * maxSize);
+ }
+
+ /**
+ * Create a new cache
+ *
+ * @param maxSize
+ * maximum size of the cache, to reduce need for synchronization
+ * this is not a hard limit. The real size of the cache could be
+ * slightly above this maximum if multiple threads put new values
+ * concurrently
+ * @param purgeFactor
+ * when the size of the map reaches maxSize the oldest entries
+ * will be purged to free up some space for new entries,
+ * {@code purgeFactor} is the fraction of {@code maxSize} to
+ * purge when this happens
+ */
+ public SimpleLruCache(int maxSize, float purgeFactor) {
+ checkPurgeFactor(purgeFactor);
+ this.maximumSize = maxSize;
+ this.purgeSize = purgeSize(maxSize, purgeFactor);
+ }
+
+ /**
+ * Returns the value to which the specified key is mapped, or {@code null}
+ * if this map contains no mapping for the key.
+ *
+ * <p>
+ * More formally, if this cache contains a mapping from a key {@code k} to a
+ * value {@code v} such that {@code key.equals(k)}, then this method returns
+ * {@code v}; otherwise it returns {@code null}. (There can be at most one
+ * such mapping.)
+ *
+ * @param key
+ * the key
+ *
+ * @throws NullPointerException
+ * if the specified key is null
+ *
+ * @return value mapped for this key, or {@code null} if no value is mapped
+ */
+ public V get(Object key) {
+ Entry<K, V> entry = map.get(key);
+ if (entry != null) {
+ entry.lastAccessed = ++time;
+ return entry.value;
+ }
+ return null;
+ }
+
+ /**
+ * Maps the specified key to the specified value in this cache. Neither the
+ * key nor the value can be null.
+ *
+ * <p>
+ * The value can be retrieved by calling the {@code get} method with a key
+ * that is equal to the original key.
+ *
+ * @param key
+ * key with which the specified value is to be associated
+ * @param value
+ * value to be associated with the specified key
+ * @return the previous value associated with {@code key}, or {@code null}
+ * if there was no mapping for {@code key}
+ * @throws NullPointerException
+ * if the specified key or value is null
+ */
+ public V put(@NonNull K key, @NonNull V value) {
+ map.put(key, new Entry<>(key, value, ++time));
+ if (map.size() > maximumSize) {
+ purge();
+ }
+ return value;
+ }
+
+ /**
+ * Returns the current size of this cache
+ *
+ * @return the number of key-value mappings in this cache
+ */
+ public int size() {
+ return map.size();
+ }
+
+ /**
+ * Reconfigures the cache. If {@code maxSize} is reduced some entries will
+ * be purged.
+ *
+ * @param maxSize
+ * maximum size of the cache
+ *
+ * @param purgeFactor
+ * when the size of the map reaches maxSize the oldest entries
+ * will be purged to free up some space for new entries,
+ * {@code purgeFactor} is the fraction of {@code maxSize} to
+ * purge when this happens
+ */
+ public void configure(int maxSize, float purgeFactor) {
+ lock.lock();
+ try {
+ checkPurgeFactor(purgeFactor);
+ this.maximumSize = maxSize;
+ this.purgeSize = purgeSize(maxSize, purgeFactor);
+ if (map.size() >= maximumSize) {
+ purge();
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private void purge() {
+ // don't try to compete if another thread already has the lock
+ if (lock.tryLock()) {
+ try {
+ List<Entry> entriesToPurge = new ArrayList<>(map.values());
+ // copy access times to avoid other threads interfere with
+ // sorting
+ for (Entry e : entriesToPurge) {
+ e.copyAccessTime();
+ }
+ Collections.sort(entriesToPurge,
+ Comparator.comparingLong(o -> -o.lastAccessedSorting));
+ for (int index = purgeSize; index < entriesToPurge
+ .size(); index++) {
+ map.remove(entriesToPurge.get(index).key);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java
new file mode 100644
index 0000000000..e9307d342b
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.util;
+
+/**
+ * Simple double statistics, computed incrementally, variance and standard
+ * deviation using Welford's online algorithm, see
+ * https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm
+ *
+ * @since 5.1.9
+ */
+public class Stats {
+ private int n = 0;
+
+ private double avg = 0.0;
+
+ private double min = 0.0;
+
+ private double max = 0.0;
+
+ private double sum = 0.0;
+
+ /**
+ * Add a value
+ *
+ * @param x
+ * value
+ */
+ public void add(double x) {
+ n++;
+ min = n == 1 ? x : Math.min(min, x);
+ max = n == 1 ? x : Math.max(max, x);
+ double d = x - avg;
+ avg += d / n;
+ sum += d * d * (n - 1) / n;
+ }
+
+ /**
+ * @return number of the added values
+ */
+ public int count() {
+ return n;
+ }
+
+ /**
+ * @return minimum of the added values
+ */
+ public double min() {
+ if (n < 1) {
+ return Double.NaN;
+ }
+ return min;
+ }
+
+ /**
+ * @return maximum of the added values
+ */
+ public double max() {
+ if (n < 1) {
+ return Double.NaN;
+ }
+ return max;
+ }
+
+ /**
+ * @return average of the added values
+ */
+
+ public double avg() {
+ if (n < 1) {
+ return Double.NaN;
+ }
+ return avg;
+ }
+
+ /**
+ * @return variance of the added values
+ */
+ public double var() {
+ if (n < 2) {
+ return Double.NaN;
+ }
+ return sum / (n - 1);
+ }
+
+ /**
+ * @return standard deviation of the added values
+ */
+ public double stddev() {
+ return Math.sqrt(this.var());
+ }
+}