diff --git a/WORKSPACE b/WORKSPACE index 0eabeccbc..6f28ea4f5 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -15,7 +15,7 @@ versions.check(minimum_bazel_version = "0.19.0") load("//tools:bazlets.bzl", "load_bazlets") -load_bazlets(commit = "3afbeab55ece585dbfc7a980bf7214b24ddbbe86") +load_bazlets(commit = "8528a0df69dadf6311d8d3f81c1b693afda8bcf1") load( "@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", diff --git a/lib/BUILD b/lib/BUILD index 0c5c22426..7bef90af4 100644 --- a/lib/BUILD +++ b/lib/BUILD @@ -12,6 +12,7 @@ java_library( visibility = [ "//org.eclipse.jgit.archive:__pkg__", "//org.eclipse.jgit.pgm.test:__pkg__", + "//org.eclipse.jgit.test:__pkg__", ], exports = ["@commons-compress//jar"], ) diff --git a/org.eclipse.jgit.ant.test/src/org/eclipse/jgit/ant/tasks/GitCloneTaskTest.java b/org.eclipse.jgit.ant.test/src/org/eclipse/jgit/ant/tasks/GitCloneTaskTest.java index 3ce066376..8043d2b18 100644 --- a/org.eclipse.jgit.ant.test/src/org/eclipse/jgit/ant/tasks/GitCloneTaskTest.java +++ b/org.eclipse.jgit.ant.test/src/org/eclipse/jgit/ant/tasks/GitCloneTaskTest.java @@ -65,12 +65,13 @@ public class GitCloneTaskTest extends LocalDiskRepositoryTestCase { @Before public void before() throws IOException { + dest = createTempFile(); + FS.getFileStoreAttributes(dest.toPath().getParent()); project = new Project(); project.init(); enableLogging(); project.addTaskDefinition("git-clone", GitCloneTask.class); task = (GitCloneTask) project.createTask("git-clone"); - dest = createTempFile(); task.setDest(dest); } diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java index 05510a05b..946fb15a3 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java @@ -58,12 +58,14 @@ import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; import java.text.MessageFormat; +import java.time.Instant; import java.util.Enumeration; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.util.FS; /** * Dumps a file over HTTP GET (or its information via HEAD). @@ -76,7 +78,7 @@ final class FileSender { private final RandomAccessFile source; - private final long lastModified; + private final Instant lastModified; private final long fileLen; @@ -89,7 +91,7 @@ final class FileSender { this.source = new RandomAccessFile(path, "r"); try { - this.lastModified = path.lastModified(); + this.lastModified = FS.DETECTED.lastModifiedInstant(path); this.fileLen = source.getChannel().size(); this.end = fileLen; } catch (IOException e) { @@ -114,7 +116,7 @@ final class FileSender { } } - long getLastModified() { + Instant getLastModified() { return lastModified; } diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ObjectFileServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ObjectFileServlet.java index 62f075c73..5a27be643 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ObjectFileServlet.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ObjectFileServlet.java @@ -54,6 +54,7 @@ import static org.eclipse.jgit.util.HttpSupport.HDR_LAST_MODIFIED; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.time.Instant; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -76,7 +77,9 @@ abstract class ObjectFileServlet extends HttpServlet { @Override String etag(FileSender sender) throws IOException { - return Long.toHexString(sender.getLastModified()); + Instant lastModified = sender.getLastModified(); + return Long.toHexString(lastModified.getEpochSecond()) + + Long.toHexString(lastModified.getNano()); } } @@ -145,7 +148,9 @@ abstract class ObjectFileServlet extends HttpServlet { try { final String etag = etag(sender); - final long lastModified = (sender.getLastModified() / 1000) * 1000; + // HTTP header Last-Modified header has a resolution of 1 sec, see + // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.29 + final long lastModified = sender.getLastModified().getEpochSecond(); String ifNoneMatch = req.getHeader(HDR_IF_NONE_MATCH); if (etag != null && etag.equals(ifNoneMatch)) { diff --git a/org.eclipse.jgit.junit/.settings/org.eclipse.jdt.core.prefs b/org.eclipse.jgit.junit/.settings/org.eclipse.jdt.core.prefs index 2ca78ff2d..b6750293d 100644 --- a/org.eclipse.jgit.junit/.settings/org.eclipse.jdt.core.prefs +++ b/org.eclipse.jgit.junit/.settings/org.eclipse.jdt.core.prefs @@ -1,10 +1,13 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=enabled org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore -org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull -org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault -org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable -org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jgit.annotations.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jgit.annotations.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jgit.annotations.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 @@ -14,6 +17,7 @@ org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.doc.comment.support=enabled +org.eclipse.jdt.core.compiler.problem.APILeak=warning org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=warning @@ -48,7 +52,7 @@ org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=error -org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore +org.eclipse.jdt.core.compiler.problem.missingJavadocComments=error org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=protected org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=return_tag @@ -64,14 +68,16 @@ org.eclipse.jdt.core.compiler.problem.noEffectAssignment=error org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=error org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error org.eclipse.jdt.core.compiler.problem.nullReference=error org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=error -org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=error org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning @@ -86,16 +92,22 @@ org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=warning org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=error org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=error +org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java index 12b216950..af23ad1e3 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java @@ -51,6 +51,8 @@ import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; +import java.io.PrintStream; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -126,6 +128,10 @@ public abstract class LocalDiskRepositoryTestCase { if (!tmp.delete() || !tmp.mkdir()) throw new IOException("Cannot create " + tmp); + // measure timer resolution before the test to avoid time critical tests + // are affected by time needed for measurement + FS.getFileStoreAttributes(tmp.toPath().getParent()); + mockSystemReader = new MockSystemReader(); mockSystemReader.userGitConfig = new FileBasedConfig(new File(tmp, "usergitconfig"), FS.DETECTED); @@ -232,35 +238,30 @@ public abstract class LocalDiskRepositoryTestCase { private static boolean recursiveDelete(final File dir, boolean silent, boolean failOnError) { assert !(silent && failOnError); - if (!dir.exists()) - return silent; - final File[] ls = dir.listFiles(); - if (ls != null) - for (int k = 0; k < ls.length; k++) { - final File e = ls[k]; - if (e.isDirectory()) - silent = recursiveDelete(e, silent, failOnError); - else if (!e.delete()) { - if (!silent) - reportDeleteFailure(failOnError, e); - silent = !failOnError; - } - } - if (!dir.delete()) { - if (!silent) - reportDeleteFailure(failOnError, dir); - silent = !failOnError; + int options = FileUtils.RECURSIVE | FileUtils.RETRY + | FileUtils.SKIP_MISSING; + if (silent) { + options |= FileUtils.IGNORE_ERRORS; } - return silent; + try { + FileUtils.delete(dir, options); + } catch (IOException e) { + reportDeleteFailure(failOnError, dir, e); + return !failOnError; + } + return true; } - private static void reportDeleteFailure(boolean failOnError, File e) { + private static void reportDeleteFailure(boolean failOnError, File f, + Exception cause) { String severity = failOnError ? "ERROR" : "WARNING"; - String msg = severity + ": Failed to delete " + e; - if (failOnError) + String msg = severity + ": Failed to delete " + f; + if (failOnError) { fail(msg); - else + } else { System.err.println(msg); + } + cause.printStackTrace(new PrintStream(System.err)); } /** Constant MOD_TIME=1 */ @@ -322,12 +323,13 @@ public abstract class LocalDiskRepositoryTestCase { throws IllegalStateException, IOException { DirCache dc = repo.readDirCache(); StringBuilder sb = new StringBuilder(); - TreeSet timeStamps = new TreeSet<>(); + TreeSet timeStamps = new TreeSet<>(); // iterate once over the dircache just to collect all time stamps if (0 != (includedOptions & MOD_TIME)) { - for (int i=0; i 0) { + RepeatedTestException e = new RepeatedTestException( + MessageFormat.format( + "Test failed {0} times out of {1} repeated executions", + Integer.valueOf(failures), + Integer.valueOf(repetitions))); + LOG.log(Level.SEVERE, e.getMessage(), e); + throw e; + } } } @@ -125,7 +164,8 @@ public class RepeatRule implements TestRule { Repeat repeat = description.getAnnotation(Repeat.class); if (repeat != null) { int n = repeat.n(); - result = new RepeatStatement(n, statement); + boolean abortOnFailure = repeat.abortOnFailure(); + result = new RepeatStatement(n, abortOnFailure, statement); } return result; } diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java index 987f923b0..5aacbbade 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java @@ -57,7 +57,9 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.nio.file.Path; +import java.time.Instant; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; @@ -284,7 +286,7 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { dce = new DirCacheEntry(treeItr.getEntryPathString()); dce.setFileMode(treeItr.getEntryFileMode()); - dce.setLastModified(treeItr.getEntryLastModified()); + dce.setLastModified(treeItr.getEntryLastModifiedInstant()); dce.setLength((int) len); try (FileInputStream in = new FileInputStream( treeItr.getEntryFile())) { @@ -361,7 +363,8 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { * @throws InterruptedException * @throws IOException */ - public static long fsTick(File lastFile) throws InterruptedException, + public static Instant fsTick(File lastFile) + throws InterruptedException, IOException { File tmp; FS fs = FS.DETECTED; @@ -375,15 +378,16 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { tmp = File.createTempFile("fsTickTmpFile", null, lastFile.getParentFile()); } - long res = FS.getFsTimerResolution(tmp.toPath()).toMillis(); + long res = FS.getFileStoreAttributes(tmp.toPath()) + .getFsTimestampResolution().toNanos(); long sleepTime = res / 10; try { - long startTime = fs.lastModified(lastFile); - long actTime = fs.lastModified(tmp); - while (actTime <= startTime) { - Thread.sleep(sleepTime); + Instant startTime = fs.lastModifiedInstant(lastFile); + Instant actTime = fs.lastModifiedInstant(tmp); + while (actTime.compareTo(startTime) <= 0) { + TimeUnit.NANOSECONDS.sleep(sleepTime); FileUtils.touch(tmp.toPath()); - actTime = fs.lastModified(tmp); + actTime = fs.lastModifiedInstant(tmp); } return actTime; } finally { diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java index c9fa2f506..49a2b2010 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java @@ -1059,6 +1059,14 @@ public class TestRepository { parents.add(prior.create()); } + /** + * set parent commit + * + * @param p + * parent commit + * @return this commit builder + * @throws Exception + */ public CommitBuilder parent(RevCommit p) throws Exception { if (parents.isEmpty()) { DirCacheBuilder b = tree.builder(); @@ -1071,29 +1079,71 @@ public class TestRepository { return this; } + /** + * Get parent commits + * + * @return parent commits + */ public List parents() { return Collections.unmodifiableList(parents); } + /** + * Remove parent commits + * + * @return this commit builder + */ public CommitBuilder noParents() { parents.clear(); return this; } + /** + * Remove files + * + * @return this commit builder + */ public CommitBuilder noFiles() { tree.clear(); return this; } + /** + * Set top level tree + * + * @param treeId + * the top level tree + * @return this commit builder + */ public CommitBuilder setTopLevelTree(ObjectId treeId) { topLevelTree = treeId; return this; } + /** + * Add file with given content + * + * @param path + * path of the file + * @param content + * the file content + * @return this commit builder + * @throws Exception + */ public CommitBuilder add(String path, String content) throws Exception { return add(path, blob(content)); } + /** + * Add file with given path and blob + * + * @param path + * path of the file + * @param id + * blob for this file + * @return this commit builder + * @throws Exception + */ public CommitBuilder add(String path, RevBlob id) throws Exception { return edit(new PathEdit(path) { @@ -1105,6 +1155,13 @@ public class TestRepository { }); } + /** + * Edit the index + * + * @param edit + * the index record update + * @return this commit builder + */ public CommitBuilder edit(PathEdit edit) { DirCacheEditor e = tree.editor(); e.add(edit); @@ -1112,6 +1169,13 @@ public class TestRepository { return this; } + /** + * Remove a file + * + * @param path + * path of the file + * @return this commit builder + */ public CommitBuilder rm(String path) { DirCacheEditor e = tree.editor(); e.add(new DeletePath(path)); @@ -1120,49 +1184,111 @@ public class TestRepository { return this; } + /** + * Set commit message + * + * @param m + * the message + * @return this commit builder + */ public CommitBuilder message(String m) { message = m; return this; } + /** + * Get the commit message + * + * @return the commit message + */ public String message() { return message; } + /** + * Tick the clock + * + * @param secs + * number of seconds + * @return this commit builder + */ public CommitBuilder tick(int secs) { tick = secs; return this; } + /** + * Set author and committer identity + * + * @param ident + * identity to set + * @return this commit builder + */ public CommitBuilder ident(PersonIdent ident) { author = ident; committer = ident; return this; } + /** + * Set the author identity + * + * @param a + * the author's identity + * @return this commit builder + */ public CommitBuilder author(PersonIdent a) { author = a; return this; } + /** + * Get the author identity + * + * @return the author identity + */ public PersonIdent author() { return author; } + /** + * Set the committer identity + * + * @param c + * the committer identity + * @return this commit builder + */ public CommitBuilder committer(PersonIdent c) { committer = c; return this; } + /** + * Get the committer identity + * + * @return the committer identity + */ public PersonIdent committer() { return committer; } + /** + * Insert changeId + * + * @return this commit builder + */ public CommitBuilder insertChangeId() { changeId = ""; return this; } + /** + * Insert given changeId + * + * @param c + * changeId + * @return this commit builder + */ public CommitBuilder insertChangeId(String c) { // Validate, but store as a string so we can use "" as a sentinel. ObjectId.fromString(c); @@ -1170,6 +1296,13 @@ public class TestRepository { return this; } + /** + * Create the commit + * + * @return the new commit + * @throws Exception + * if creation failed + */ public RevCommit create() throws Exception { if (self == null) { TestRepository.this.tick(tick); @@ -1230,6 +1363,12 @@ public class TestRepository { + cid.getName() + "\n"); //$NON-NLS-1$ } + /** + * Create child commit builder + * + * @return child commit builder + * @throws Exception + */ public CommitBuilder child() throws Exception { return new CommitBuilder(this); } diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java new file mode 100644 index 000000000..1f8070d91 --- /dev/null +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2019, Matthias Sohn + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.junit.time; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Instant; + +import org.eclipse.jgit.util.FS; + +/** + * Utility methods for handling timestamps + */ +public class TimeUtil { + /** + * Set the lastModified time of a given file by adding a given offset to the + * current lastModified time + * + * @param path + * path of a file to set last modified + * @param offsetMillis + * offset in milliseconds, if negative the new lastModified time + * is offset before the original lastModified time, otherwise + * after the original time + * @return the new lastModified time + */ + public static Instant setLastModifiedWithOffset(Path path, + long offsetMillis) { + Instant mTime = FS.DETECTED.lastModifiedInstant(path) + .plusMillis(offsetMillis); + try { + Files.setLastModifiedTime(path, FileTime.from(mTime)); + return mTime; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Set the lastModified time of file a to the one from file b + * + * @param a + * file to set lastModified time + * @param b + * file to read lastModified time from + */ + public static void setLastModifiedOf(Path a, Path b) { + Instant mTime = FS.DETECTED.lastModifiedInstant(b); + try { + Files.setLastModifiedTime(a, FileTime.from(mTime)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + +} diff --git a/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/LfsServerTest.java b/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/LfsServerTest.java index 10823b878..ec44da4ca 100644 --- a/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/LfsServerTest.java +++ b/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/LfsServerTest.java @@ -82,6 +82,7 @@ import org.eclipse.jgit.lfs.lib.LongObjectId; import org.eclipse.jgit.lfs.server.LargeFileRepository; import org.eclipse.jgit.lfs.server.LfsProtocolServlet; import org.eclipse.jgit.lfs.test.LongObjectIdTestUtils; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.IO; import org.junit.After; @@ -119,6 +120,11 @@ public abstract class LfsServerTest { @Before public void setup() throws Exception { tmp = Files.createTempDirectory("jgit_test_"); + + // measure timer resolution before the test to avoid time critical tests + // are affected by time needed for measurement + FS.getFileStoreAttributes(tmp.getParent()); + server = new AppServer(); ServletContextHandler app = server.addContext("/lfs"); dir = Paths.get(tmp.toString(), "lfs"); diff --git a/org.eclipse.jgit.packaging/pom.xml b/org.eclipse.jgit.packaging/pom.xml index bc069c626..cbb5ee5e9 100644 --- a/org.eclipse.jgit.packaging/pom.xml +++ b/org.eclipse.jgit.packaging/pom.xml @@ -47,10 +47,6 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - - 3.0 - - org.eclipse.jgit jgit.tycho.parent 5.2.3-SNAPSHOT @@ -59,7 +55,7 @@ JGit Tycho Parent - 1.2.0 + 1.3.0 ${tycho-version} jgit-4.6 @@ -231,6 +227,21 @@ tycho-p2-plugin ${tycho-version} + + org.eclipse.tycho + tycho-p2-publisher-plugin + ${tycho-version} + + + org.eclipse.tycho + tycho-p2-repository-plugin + ${tycho-version} + + + org.eclipse.tycho + tycho-packaging-plugin + ${tycho-version} + org.eclipse.tycho.extras tycho-pack200a-plugin @@ -251,6 +262,25 @@ build-helper-maven-plugin 3.0.0 + + maven-clean-plugin + 3.1.0 + + + org.apache.maven.plugins + maven-deploy-plugin + 3.0.0-M1 + + + org.apache.maven.plugins + maven-install-plugin + 3.0.0-M1 + + + org.apache.maven.plugins + maven-site-plugin + 3.7.1 + diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowDirCache.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowDirCache.java index 7f99d76bd..14a60a3b4 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowDirCache.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowDirCache.java @@ -48,8 +48,10 @@ package org.eclipse.jgit.pgm.debug; import static java.lang.Integer.valueOf; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Locale; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheEntry; @@ -67,25 +69,27 @@ class ShowDirCache extends TextBuiltin { /** {@inheritDoc} */ @Override protected void run() throws Exception { - final SimpleDateFormat fmt; - fmt = new SimpleDateFormat("yyyy-MM-dd,HH:mm:ss.SSS"); //$NON-NLS-1$ + final DateTimeFormatter fmt = DateTimeFormatter + .ofPattern("yyyy-MM-dd,HH:mm:ss.nnnnnnnnn") //$NON-NLS-1$ + .withLocale(Locale.getDefault()) + .withZone(ZoneId.systemDefault()); final DirCache cache = db.readDirCache(); for (int i = 0; i < cache.getEntryCount(); i++) { final DirCacheEntry ent = cache.getEntry(i); final FileMode mode = FileMode.fromBits(ent.getRawMode()); final int len = ent.getLength(); - long lastModified = ent.getLastModified(); - final Date mtime = new Date(lastModified); + Instant mtime = ent.getLastModifiedInstant(); final int stage = ent.getStage(); outw.print(mode); outw.format(" %6d", valueOf(len)); //$NON-NLS-1$ outw.print(' '); - if (millis) - outw.print(lastModified); - else + if (millis) { + outw.print(mtime.toEpochMilli()); + } else { outw.print(fmt.format(mtime)); + } outw.print(' '); outw.print(ent.getObjectId().name()); outw.print(' '); diff --git a/org.eclipse.jgit.test/BUILD b/org.eclipse.jgit.test/BUILD index 0b18e5ef7..95586e246 100644 --- a/org.eclipse.jgit.test/BUILD +++ b/org.eclipse.jgit.test/BUILD @@ -23,7 +23,6 @@ HELPERS = glob( "revwalk/RevWalkTestCase.java", "transport/ObjectIdMatcher.java", "transport/SpiTransport.java", - "treewalk/FileTreeIteratorWithTimeControl.java", "treewalk/filter/AlwaysCloneTreeFilter.java", "test/resources/SampleDataRepositoryTestCase.java", "util/CPUTimeStopWatch.java", @@ -37,7 +36,7 @@ DATA = [ RESOURCES = glob(["resources/**"]) -tests(glob( +tests(tests = glob( ["tst/**/*.java"], exclude = HELPERS + DATA, )) diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index 55ccecf46..e0d4ddaa5 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -11,9 +11,16 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", com.jcraft.jsch;version="[0.1.54,0.2.0)", net.bytebuddy.dynamic.loading;version="[1.7.0,2.0.0)", + org.apache.commons.compress.archivers;version="[1.15.0,2.0)", + org.apache.commons.compress.archivers.tar;version="[1.15.0,2.0)", + org.apache.commons.compress.archivers.zip;version="[1.15.0,2.0)", + org.apache.commons.compress.compressors.bzip2;version="[1.15.0,2.0)", + org.apache.commons.compress.compressors.gzip;version="[1.15.0,2.0)", + org.apache.commons.compress.compressors.xz;version="[1.15.0,2.0)", org.eclipse.jgit.annotations;version="[5.2.3,5.3.0)", org.eclipse.jgit.api;version="[5.2.3,5.3.0)", org.eclipse.jgit.api.errors;version="[5.2.3,5.3.0)", + org.eclipse.jgit.archive;version="[5.2.3,5.3.0)", org.eclipse.jgit.attributes;version="[5.2.3,5.3.0)", org.eclipse.jgit.awtui;version="[5.2.3,5.3.0)", org.eclipse.jgit.blame;version="[5.2.3,5.3.0)", @@ -37,6 +44,7 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", org.eclipse.jgit.internal.transport.parser;version="[5.2.3,5.3.0)", org.eclipse.jgit.junit;version="[5.2.3,5.3.0)", org.eclipse.jgit.junit.ssh;version="[5.2.3,5.3.0)", + org.eclipse.jgit.junit.time;version="[5.2.3,5.3.0)", org.eclipse.jgit.lfs;version="[5.2.3,5.3.0)", org.eclipse.jgit.lib;version="[5.2.3,5.3.0)", org.eclipse.jgit.merge;version="[5.2.3,5.3.0)", @@ -69,7 +77,8 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", org.mockito.junit;version="[2.13.0,3.0.0)", org.mockito.stubbing;version="[2.13.0,3.0.0)", org.objenesis;version="[2.6.0,3.0.0)", - org.slf4j;version="[1.7.0,2.0.0)" + org.slf4j;version="[1.7.0,2.0.0)", + org.tukaani.xz;version="[1.6.0,2.0)" Require-Bundle: org.hamcrest.core;bundle-version="[1.1.0,2.0.0)", org.hamcrest.library;bundle-version="[1.1.0,2.0.0)" Export-Package: org.eclipse.jgit.transport.ssh;version="5.2.3";x-friends:="org.eclipse.jgit.ssh.apache.test" diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml index 6b19eb368..149663f64 100644 --- a/org.eclipse.jgit.test/pom.xml +++ b/org.eclipse.jgit.test/pom.xml @@ -122,6 +122,12 @@ org.eclipse.jgit.pgm ${project.version} + + + org.tukaani + xz + true + @@ -133,6 +139,7 @@ org.apache.maven.plugins maven-surefire-plugin + ${maven-surefire-plugin-version} -Djgit.test.long=true diff --git a/org.eclipse.jgit.test/tests.bzl b/org.eclipse.jgit.test/tests.bzl index dc2102964..d2f6d705b 100644 --- a/org.eclipse.jgit.test/tests.bzl +++ b/org.eclipse.jgit.test/tests.bzl @@ -7,15 +7,16 @@ def tests(tests): for src in tests: name = src[len("tst/"):len(src) - len(".java")].replace("/", "_") labels = [] + timeout = "moderate" if name.startswith("org_eclipse_jgit_"): - l = name[len("org.eclipse.jgit_"):] - if l.startswith("internal_storage_"): - l = l[len("internal.storage_"):] - i = l.find("_") - if i > 0: - labels.append(l[:i]) + package = name[len("org.eclipse.jgit_"):] + if package.startswith("internal_storage_"): + package = package[len("internal.storage_"):] + index = package.find("_") + if index > 0: + labels.append(package[:index]) else: - labels.append(i) + labels.append(index) if "lib" not in labels: labels.append("lib") @@ -53,9 +54,17 @@ def tests(tests): additional_deps = [ "//lib:mockito", ] + if src.endswith("ArchiveCommandTest.java"): + additional_deps = [ + "//lib:commons-compress", + "//lib:xz", + "//org.eclipse.jgit.archive:jgit-archive", + ] heap_size = "-Xmx256m" if src.endswith("HugeCommitMessageTest.java"): heap_size = "-Xmx512m" + if src.endswith("EolRepositoryTest.java") or src.endswith("GcCommitSelectionTest.java"): + timeout = "long" junit_tests( name = name, @@ -73,4 +82,5 @@ def tests(tests): ], flaky = flaky, jvm_flags = [heap_size, "-Dfile.encoding=UTF-8"], + timeout = timeout, ) diff --git a/org.eclipse.jgit.test/tst-rsrc/log4j.properties b/org.eclipse.jgit.test/tst-rsrc/log4j.properties index 14620ffae..ee1ac3515 100644 --- a/org.eclipse.jgit.test/tst-rsrc/log4j.properties +++ b/org.eclipse.jgit.test/tst-rsrc/log4j.properties @@ -7,3 +7,8 @@ log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n +#log4j.appender.fileLogger.bufferedIO = true +#log4j.appender.fileLogger.bufferSize = 4096 + +#log4j.logger.org.eclipse.jgit.util.FS = DEBUG +#log4j.logger.org.eclipse.jgit.internal.storage.file.FileSnapshot = DEBUG diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java index 1a5793ce3..687926bd8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java @@ -1268,7 +1268,7 @@ public class AddCommandTest extends RepositoryTestCase { DirCacheEntry entry = new DirCacheEntry(path, stage); entry.setObjectId(id); entry.setFileMode(FileMode.REGULAR_FILE); - entry.setLastModified(file.lastModified()); + entry.setLastModified(FS.DETECTED.lastModifiedInstant(file)); entry.setLength((int) file.length()); builder.add(entry); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java index 1c4101816..0f2e6b8ac 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java @@ -47,20 +47,44 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import java.beans.Statement; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Files; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.ArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; +import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; +import org.eclipse.jgit.api.errors.AbortedByHookException; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.NoFilepatternException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.api.errors.NoMessageException; +import org.eclipse.jgit.api.errors.UnmergedPathsException; +import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.archive.ArchiveFormats; +import org.eclipse.jgit.errors.AmbiguousObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.StringUtils; import org.junit.After; import org.junit.Before; @@ -68,9 +92,14 @@ import org.junit.Test; public class ArchiveCommandTest extends RepositoryTestCase { + // archives store timestamp with 1 second resolution + private static final int WAIT = 2000; private static final String UNEXPECTED_ARCHIVE_SIZE = "Unexpected archive size"; private static final String UNEXPECTED_FILE_CONTENTS = "Unexpected file contents"; private static final String UNEXPECTED_TREE_CONTENTS = "Unexpected tree contents"; + private static final String UNEXPECTED_LAST_MODIFIED = + "Unexpected lastModified mocked by MockSystemReader, truncated to 1 second"; + private static final String UNEXPECTED_DIFFERENT_HASH = "Unexpected different hash"; private MockFormat format = null; @@ -78,25 +107,20 @@ public class ArchiveCommandTest extends RepositoryTestCase { public void setup() { format = new MockFormat(); ArchiveCommand.registerFormat(format.SUFFIXES.get(0), format); + ArchiveFormats.registerAll(); } @Override @After public void tearDown() { ArchiveCommand.unregisterFormat(format.SUFFIXES.get(0)); + ArchiveFormats.unregisterAll(); } @Test public void archiveHeadAllFiles() throws IOException, GitAPIException { try (Git git = new Git(db)) { - writeTrashFile("file_1.txt", "content_1_1"); - git.add().addFilepattern("file_1.txt").call(); - git.commit().setMessage("create file").call(); - - writeTrashFile("file_1.txt", "content_1_2"); - writeTrashFile("file_2.txt", "content_2_2"); - git.add().addFilepattern(".").call(); - git.commit().setMessage("updated file").call(); + createTestContent(git); git.archive().setOutputStream(new MockOutputStream()) .setFormat(format.SUFFIXES.get(0)) @@ -191,6 +215,157 @@ public class ArchiveCommandTest extends RepositoryTestCase { } } + @Test + public void archiveHeadAllFilesTarTimestamps() throws Exception { + try (Git git = new Git(db)) { + createTestContent(git); + String fmt = "tar"; + File archive = new File(getTemporaryDirectory(), + "archive." + format); + archive(git, archive, fmt); + ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive)); + + try (InputStream fi = Files.newInputStream(archive.toPath()); + InputStream bi = new BufferedInputStream(fi); + ArchiveInputStream o = new TarArchiveInputStream(bi)) { + assertEntries(o); + } + + Thread.sleep(WAIT); + archive(git, archive, fmt); + assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1, + ObjectId.fromRaw(IO.readFully(archive))); + } + } + + @Test + public void archiveHeadAllFilesTgzTimestamps() throws Exception { + try (Git git = new Git(db)) { + createTestContent(git); + String fmt = "tgz"; + File archive = new File(getTemporaryDirectory(), + "archive." + fmt); + archive(git, archive, fmt); + ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive)); + + try (InputStream fi = Files.newInputStream(archive.toPath()); + InputStream bi = new BufferedInputStream(fi); + InputStream gzi = new GzipCompressorInputStream(bi); + ArchiveInputStream o = new TarArchiveInputStream(gzi)) { + assertEntries(o); + } + + Thread.sleep(WAIT); + archive(git, archive, fmt); + assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1, + ObjectId.fromRaw(IO.readFully(archive))); + } + } + + @Test + public void archiveHeadAllFilesTbz2Timestamps() throws Exception { + try (Git git = new Git(db)) { + createTestContent(git); + String fmt = "tbz2"; + File archive = new File(getTemporaryDirectory(), + "archive." + fmt); + archive(git, archive, fmt); + ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive)); + + try (InputStream fi = Files.newInputStream(archive.toPath()); + InputStream bi = new BufferedInputStream(fi); + InputStream gzi = new BZip2CompressorInputStream(bi); + ArchiveInputStream o = new TarArchiveInputStream(gzi)) { + assertEntries(o); + } + + Thread.sleep(WAIT); + archive(git, archive, fmt); + assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1, + ObjectId.fromRaw(IO.readFully(archive))); + } + } + + @Test + public void archiveHeadAllFilesTxzTimestamps() throws Exception { + try (Git git = new Git(db)) { + createTestContent(git); + String fmt = "txz"; + File archive = new File(getTemporaryDirectory(), "archive." + fmt); + archive(git, archive, fmt); + ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive)); + + try (InputStream fi = Files.newInputStream(archive.toPath()); + InputStream bi = new BufferedInputStream(fi); + InputStream gzi = new XZCompressorInputStream(bi); + ArchiveInputStream o = new TarArchiveInputStream(gzi)) { + assertEntries(o); + } + + Thread.sleep(WAIT); + archive(git, archive, fmt); + assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1, + ObjectId.fromRaw(IO.readFully(archive))); + } + } + + @Test + public void archiveHeadAllFilesZipTimestamps() throws Exception { + try (Git git = new Git(db)) { + createTestContent(git); + String fmt = "zip"; + File archive = new File(getTemporaryDirectory(), "archive." + fmt); + archive(git, archive, fmt); + ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive)); + + try (InputStream fi = Files.newInputStream(archive.toPath()); + InputStream bi = new BufferedInputStream(fi); + ArchiveInputStream o = new ZipArchiveInputStream(bi)) { + assertEntries(o); + } + + Thread.sleep(WAIT); + archive(git, archive, fmt); + assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1, + ObjectId.fromRaw(IO.readFully(archive))); + } + } + + private void createTestContent(Git git) throws IOException, GitAPIException, + NoFilepatternException, NoHeadException, NoMessageException, + UnmergedPathsException, ConcurrentRefUpdateException, + WrongRepositoryStateException, AbortedByHookException { + writeTrashFile("file_1.txt", "content_1_1"); + git.add().addFilepattern("file_1.txt").call(); + git.commit().setMessage("create file").call(); + + writeTrashFile("file_1.txt", "content_1_2"); + writeTrashFile("file_2.txt", "content_2_2"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("updated file").call(); + } + + private static void archive(Git git, File archive, String fmt) + throws GitAPIException, + FileNotFoundException, AmbiguousObjectException, + IncorrectObjectTypeException, IOException { + git.archive().setOutputStream(new FileOutputStream(archive)) + .setFormat(fmt) + .setTree(git.getRepository().resolve("HEAD")).call(); + } + + private static void assertEntries(ArchiveInputStream o) throws IOException { + ArchiveEntry e; + int n = 0; + while ((e = o.getNextEntry()) != null) { + n++; + assertEquals(UNEXPECTED_LAST_MODIFIED, + (1250379778668L / 1000L) * 1000L, + e.getLastModifiedDate().getTime()); + } + assertEquals(UNEXPECTED_ARCHIVE_SIZE, 2, n); + } + private static class MockFormat implements ArchiveCommand.Format { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java index 498005ded..65c20aa9a 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java @@ -43,6 +43,7 @@ */ package org.eclipse.jgit.api; +import static java.time.Instant.EPOCH; import static org.eclipse.jgit.lib.Constants.MASTER; import static org.eclipse.jgit.lib.Constants.R_HEADS; import static org.hamcrest.CoreMatchers.is; @@ -60,6 +61,9 @@ import java.io.FileInputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import org.eclipse.jgit.api.CheckoutResult.Status; import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode; @@ -74,6 +78,7 @@ import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.time.TimeUtil; import org.eclipse.jgit.lfs.BuiltinLFS; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; @@ -86,6 +91,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.SystemReader; import org.junit.Before; @@ -362,14 +368,14 @@ public class CheckoutCommandTest extends RepositoryTestCase { File file = new File(db.getWorkTree(), "Test.txt"); long size = file.length(); - long mTime = file.lastModified() - 5000L; - assertTrue(file.setLastModified(mTime)); + Instant mTime = TimeUtil.setLastModifiedWithOffset(file.toPath(), + -5000L); DirCache cache = DirCache.lock(db.getIndexFile(), db.getFS()); DirCacheEntry entry = cache.getEntry("Test.txt"); assertNotNull(entry); entry.setLength(0); - entry.setLastModified(0); + entry.setLastModified(EPOCH); cache.write(); assertTrue(cache.commit()); @@ -377,10 +383,12 @@ public class CheckoutCommandTest extends RepositoryTestCase { entry = cache.getEntry("Test.txt"); assertNotNull(entry); assertEquals(0, entry.getLength()); - assertEquals(0, entry.getLastModified()); + assertEquals(EPOCH, entry.getLastModifiedInstant()); - db.getIndexFile().setLastModified( - db.getIndexFile().lastModified() - 5000); + Files.setLastModifiedTime(db.getIndexFile().toPath(), + FileTime.from(FS.DETECTED + .lastModifiedInstant(db.getIndexFile()) + .minusMillis(5000L))); assertNotNull(git.checkout().setName("test").call()); @@ -388,7 +396,7 @@ public class CheckoutCommandTest extends RepositoryTestCase { entry = cache.getEntry("Test.txt"); assertNotNull(entry); assertEquals(size, entry.getLength()); - assertEquals(mTime, entry.getLastModified()); + assertEquals(mTime, entry.getLastModifiedInstant()); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java index 3a13aa5a4..a6072a0f5 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java @@ -61,6 +61,7 @@ import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.time.TimeUtil; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; @@ -311,11 +312,11 @@ public class CommitCommandTest extends RepositoryTestCase { public void commitUpdatesSmudgedEntries() throws Exception { try (Git git = new Git(db)) { File file1 = writeTrashFile("file1.txt", "content1"); - assertTrue(file1.setLastModified(file1.lastModified() - 5000)); + TimeUtil.setLastModifiedWithOffset(file1.toPath(), -5000L); File file2 = writeTrashFile("file2.txt", "content2"); - assertTrue(file2.setLastModified(file2.lastModified() - 5000)); + TimeUtil.setLastModifiedWithOffset(file2.toPath(), -5000L); File file3 = writeTrashFile("file3.txt", "content3"); - assertTrue(file3.setLastModified(file3.lastModified() - 5000)); + TimeUtil.setLastModifiedWithOffset(file3.toPath(), -5000L); assertNotNull(git.add().addFilepattern("file1.txt") .addFilepattern("file2.txt").addFilepattern("file3.txt").call()); @@ -346,11 +347,12 @@ public class CommitCommandTest extends RepositoryTestCase { assertEquals(0, cache.getEntry("file2.txt").getLength()); assertEquals(0, cache.getEntry("file3.txt").getLength()); - long indexTime = db.getIndexFile().lastModified(); - db.getIndexFile().setLastModified(indexTime - 5000); + TimeUtil.setLastModifiedWithOffset(db.getIndexFile().toPath(), + -5000L); write(file1, "content4"); - assertTrue(file1.setLastModified(file1.lastModified() + 2500)); + + TimeUtil.setLastModifiedWithOffset(file1.toPath(), 2500L); assertNotNull(git.commit().setMessage("edit file").setOnly("file1.txt") .call()); @@ -368,9 +370,9 @@ public class CommitCommandTest extends RepositoryTestCase { public void commitIgnoresSmudgedEntryWithDifferentId() throws Exception { try (Git git = new Git(db)) { File file1 = writeTrashFile("file1.txt", "content1"); - assertTrue(file1.setLastModified(file1.lastModified() - 5000)); + TimeUtil.setLastModifiedWithOffset(file1.toPath(), -5000L); File file2 = writeTrashFile("file2.txt", "content2"); - assertTrue(file2.setLastModified(file2.lastModified() - 5000)); + TimeUtil.setLastModifiedWithOffset(file2.toPath(), -5000L); assertNotNull(git.add().addFilepattern("file1.txt") .addFilepattern("file2.txt").call()); @@ -399,11 +401,11 @@ public class CommitCommandTest extends RepositoryTestCase { assertEquals(0, cache.getEntry("file1.txt").getLength()); assertEquals(0, cache.getEntry("file2.txt").getLength()); - long indexTime = db.getIndexFile().lastModified(); - db.getIndexFile().setLastModified(indexTime - 5000); + TimeUtil.setLastModifiedWithOffset(db.getIndexFile().toPath(), + -5000L); write(file1, "content5"); - assertTrue(file1.setLastModified(file1.lastModified() + 1000)); + TimeUtil.setLastModifiedWithOffset(file1.toPath(), 1000L); assertNotNull(git.commit().setMessage("edit file").setOnly("file1.txt") .call()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DiffCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DiffCommandTest.java index 43c3a8cf9..3a93839e8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DiffCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DiffCommandTest.java @@ -44,7 +44,6 @@ package org.eclipse.jgit.api; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; import java.io.File; @@ -55,6 +54,7 @@ import java.util.List; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.time.TimeUtil; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.revwalk.RevWalk; @@ -230,7 +230,7 @@ public class DiffCommandTest extends RepositoryTestCase { @Test public void testNoOutputStreamSet() throws Exception { File file = writeTrashFile("test.txt", "a"); - assertTrue(file.setLastModified(file.lastModified() - 5000)); + TimeUtil.setLastModifiedWithOffset(file.toPath(), -5000L); try (Git git = new Git(db)) { git.add().addFilepattern(".").call(); write(file, "b"); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java index 9b12011aa..dd7230bdb 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java @@ -42,6 +42,7 @@ */ package org.eclipse.jgit.api; +import static java.time.Instant.EPOCH; import static org.eclipse.jgit.api.ResetCommand.ResetType.HARD; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -52,6 +53,9 @@ import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.errors.GitAPIException; @@ -252,13 +256,13 @@ public class ResetCommandTest extends RepositoryTestCase { public void testMixedResetRetainsSizeAndModifiedTime() throws Exception { git = new Git(db); - writeTrashFile("a.txt", "a").setLastModified( - System.currentTimeMillis() - 60 * 1000); + Files.setLastModifiedTime(writeTrashFile("a.txt", "a").toPath(), + FileTime.from(Instant.now().minusSeconds(60))); assertNotNull(git.add().addFilepattern("a.txt").call()); assertNotNull(git.commit().setMessage("a commit").call()); - writeTrashFile("b.txt", "b").setLastModified( - System.currentTimeMillis() - 60 * 1000); + Files.setLastModifiedTime(writeTrashFile("b.txt", "b").toPath(), + FileTime.from(Instant.now().minusSeconds(60))); assertNotNull(git.add().addFilepattern("b.txt").call()); RevCommit commit2 = git.commit().setMessage("b commit").call(); assertNotNull(commit2); @@ -268,12 +272,12 @@ public class ResetCommandTest extends RepositoryTestCase { DirCacheEntry aEntry = cache.getEntry("a.txt"); assertNotNull(aEntry); assertTrue(aEntry.getLength() > 0); - assertTrue(aEntry.getLastModified() > 0); + assertTrue(aEntry.getLastModifiedInstant().compareTo(EPOCH) > 0); DirCacheEntry bEntry = cache.getEntry("b.txt"); assertNotNull(bEntry); assertTrue(bEntry.getLength() > 0); - assertTrue(bEntry.getLastModified() > 0); + assertTrue(bEntry.getLastModifiedInstant().compareTo(EPOCH) > 0); assertSameAsHead(git.reset().setMode(ResetType.MIXED) .setRef(commit2.getName()).call()); @@ -282,13 +286,17 @@ public class ResetCommandTest extends RepositoryTestCase { DirCacheEntry mixedAEntry = cache.getEntry("a.txt"); assertNotNull(mixedAEntry); - assertEquals(aEntry.getLastModified(), mixedAEntry.getLastModified()); - assertEquals(aEntry.getLastModified(), mixedAEntry.getLastModified()); + assertEquals(aEntry.getLastModifiedInstant(), + mixedAEntry.getLastModifiedInstant()); + assertEquals(aEntry.getLastModifiedInstant(), + mixedAEntry.getLastModifiedInstant()); DirCacheEntry mixedBEntry = cache.getEntry("b.txt"); assertNotNull(mixedBEntry); - assertEquals(bEntry.getLastModified(), mixedBEntry.getLastModified()); - assertEquals(bEntry.getLastModified(), mixedBEntry.getLastModified()); + assertEquals(bEntry.getLastModifiedInstant(), + mixedBEntry.getLastModifiedInstant()); + assertEquals(bEntry.getLastModifiedInstant(), + mixedBEntry.getLastModifiedInstant()); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java index b6291bfca..50753ae1b 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java @@ -53,6 +53,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; +import java.time.Instant; import org.eclipse.jgit.events.IndexChangedEvent; import org.eclipse.jgit.events.IndexChangedListener; @@ -99,7 +100,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { public void testBuildOneFile_FinishWriteCommit() throws Exception { final String path = "a-file-path"; final FileMode mode = FileMode.REGULAR_FILE; - final long lastModified = 1218123387057L; + final Instant lastModified = Instant.ofEpochMilli(1218123387057L); final int length = 1342; final DirCacheEntry entOrig; { @@ -117,7 +118,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { assertEquals(ObjectId.zeroId(), entOrig.getObjectId()); assertEquals(mode.getBits(), entOrig.getRawMode()); assertEquals(0, entOrig.getStage()); - assertEquals(lastModified, entOrig.getLastModified()); + assertEquals(lastModified, entOrig.getLastModifiedInstant()); assertEquals(length, entOrig.getLength()); assertFalse(entOrig.isAssumeValid()); b.add(entOrig); @@ -139,7 +140,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { assertEquals(ObjectId.zeroId(), entOrig.getObjectId()); assertEquals(mode.getBits(), entOrig.getRawMode()); assertEquals(0, entOrig.getStage()); - assertEquals(lastModified, entOrig.getLastModified()); + assertEquals(lastModified, entOrig.getLastModifiedInstant()); assertEquals(length, entOrig.getLength()); assertFalse(entOrig.isAssumeValid()); } @@ -149,7 +150,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { public void testBuildOneFile_Commit() throws Exception { final String path = "a-file-path"; final FileMode mode = FileMode.REGULAR_FILE; - final long lastModified = 1218123387057L; + final Instant lastModified = Instant.ofEpochMilli(1218123387057L); final int length = 1342; final DirCacheEntry entOrig; { @@ -167,7 +168,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { assertEquals(ObjectId.zeroId(), entOrig.getObjectId()); assertEquals(mode.getBits(), entOrig.getRawMode()); assertEquals(0, entOrig.getStage()); - assertEquals(lastModified, entOrig.getLastModified()); + assertEquals(lastModified, entOrig.getLastModifiedInstant()); assertEquals(length, entOrig.getLength()); assertFalse(entOrig.isAssumeValid()); b.add(entOrig); @@ -187,7 +188,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { assertEquals(ObjectId.zeroId(), entOrig.getObjectId()); assertEquals(mode.getBits(), entOrig.getRawMode()); assertEquals(0, entOrig.getStage()); - assertEquals(lastModified, entOrig.getLastModified()); + assertEquals(lastModified, entOrig.getLastModifiedInstant()); assertEquals(length, entOrig.getLength()); assertFalse(entOrig.isAssumeValid()); } @@ -204,7 +205,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { final String path = "a-file-path"; final FileMode mode = FileMode.REGULAR_FILE; // "old" date in 2008 - final long lastModified = 1218123387057L; + final Instant lastModified = Instant.ofEpochMilli(1218123387057L); final int length = 1342; DirCacheEntry entOrig; boolean receivedEvent = false; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java index 86e285287..475819dbb 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java @@ -43,6 +43,7 @@ package org.eclipse.jgit.dircache; +import static java.time.Instant.EPOCH; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; @@ -188,7 +189,7 @@ public class DirCacheEntryTest { e.setAssumeValid(false); e.setCreationTime(2L); e.setFileMode(FileMode.EXECUTABLE_FILE); - e.setLastModified(3L); + e.setLastModified(EPOCH.plusMillis(3L)); e.setLength(100L); e.setObjectId(ObjectId .fromString("0123456789012345678901234567890123456789")); @@ -199,7 +200,7 @@ public class DirCacheEntryTest { f.setAssumeValid(true); f.setCreationTime(10L); f.setFileMode(FileMode.SYMLINK); - f.setLastModified(20L); + f.setLastModified(EPOCH.plusMillis(20L)); f.setLength(100000000L); f.setObjectId(ObjectId .fromString("1234567890123456789012345678901234567890")); @@ -212,7 +213,7 @@ public class DirCacheEntryTest { ObjectId.fromString("1234567890123456789012345678901234567890"), e.getObjectId()); assertEquals(FileMode.SYMLINK, e.getFileMode()); - assertEquals(20L, e.getLastModified()); + assertEquals(EPOCH.plusMillis(20L), e.getLastModifiedInstant()); assertEquals(100000000L, e.getLength()); if (keepStage) assertEquals(DirCacheEntry.STAGE_2, e.getStage()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java index 643daa5c9..6bec05673 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java @@ -56,6 +56,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.time.Instant; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -71,6 +72,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.WindowCacheConfig; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.junit.After; import org.junit.Before; @@ -235,7 +237,8 @@ public class ConcurrentRepackTest extends RepositoryTestCase { private static void write(File[] files, PackWriter pw) throws IOException { - final long begin = files[0].getParentFile().lastModified(); + final Instant begin = FS.DETECTED + .lastModifiedInstant(files[0].getParentFile()); NullProgressMonitor m = NullProgressMonitor.INSTANCE; try (OutputStream out = new BufferedOutputStream( @@ -252,7 +255,8 @@ public class ConcurrentRepackTest extends RepositoryTestCase { } private static void delete(File[] list) throws IOException { - final long begin = list[0].getParentFile().lastModified(); + final Instant begin = FS.DETECTED + .lastModifiedInstant(list[0].getParentFile()); for (File f : list) { FileUtils.delete(f); assertFalse(f + " was removed", f.exists()); @@ -260,14 +264,14 @@ public class ConcurrentRepackTest extends RepositoryTestCase { touch(begin, list[0].getParentFile()); } - private static void touch(long begin, File dir) { - while (begin >= dir.lastModified()) { + private static void touch(Instant begin, File dir) throws IOException { + while (begin.compareTo(FS.DETECTED.lastModifiedInstant(dir)) >= 0) { try { Thread.sleep(25); } catch (InterruptedException ie) { // } - dir.setLastModified(System.currentTimeMillis()); + FS.DETECTED.setLastModified(dir.toPath(), Instant.now()); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java index 5ebdeb6e8..6fa35d64b 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java @@ -42,49 +42,68 @@ */ package org.eclipse.jgit.internal.storage.file; +import static org.eclipse.jgit.junit.JGitTestUtil.read; +import static org.eclipse.jgit.junit.JGitTestUtil.write; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileTime; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; -import java.util.List; +import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.FileStoreAttributes; import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.Stats; import org.eclipse.jgit.util.SystemReader; import org.junit.After; import org.junit.Assume; import org.junit.Before; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class FileSnapshotTest { + private static final Logger LOG = LoggerFactory + .getLogger(FileSnapshotTest.class); - private List files = new ArrayList<>(); + private Path trash; - private File trash; + private FileStoreAttributes fsAttrCache; @Before public void setUp() throws Exception { - trash = File.createTempFile("tmp_", ""); - trash.delete(); - assertTrue("mkdir " + trash, trash.mkdir()); + trash = Files.createTempDirectory("tmp_"); + // measure timer resolution before the test to avoid time critical tests + // are affected by time needed for measurement + fsAttrCache = FS + .getFileStoreAttributes(trash.getParent()); } @Before @After public void tearDown() throws Exception { - FileUtils.delete(trash, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); + FileUtils.delete(trash.toFile(), + FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); } - private static void waitNextSec(File f) { - long initialLastModified = f.lastModified(); + private static void waitNextTick(Path f) throws IOException { + Instant initialLastModified = FS.DETECTED.lastModifiedInstant(f); do { - f.setLastModified(System.currentTimeMillis()); - } while (f.lastModified() == initialLastModified); + FS.DETECTED.setLastModified(f, Instant.now()); + } while (FS.DETECTED.lastModifiedInstant(f) + .equals(initialLastModified)); } /** @@ -94,12 +113,12 @@ public class FileSnapshotTest { */ @Test public void testActuallyIsModifiedTrivial() throws Exception { - File f1 = createFile("simple"); - waitNextSec(f1); - FileSnapshot save = FileSnapshot.save(f1); + Path f1 = createFile("simple"); + waitNextTick(f1); + FileSnapshot save = FileSnapshot.save(f1.toFile()); append(f1, (byte) 'x'); - waitNextSec(f1); - assertTrue(save.isModified(f1)); + waitNextTick(f1); + assertTrue(save.isModified(f1.toFile())); } /** @@ -112,11 +131,17 @@ public class FileSnapshotTest { */ @Test public void testNewFileWithWait() throws Exception { - File f1 = createFile("newfile"); - waitNextSec(f1); - FileSnapshot save = FileSnapshot.save(f1); - Thread.sleep(1500); - assertTrue(save.isModified(f1)); + // if filesystem timestamp resolution is high the snapshot won't be + // racily clean + Assume.assumeTrue( + fsAttrCache.getFsTimestampResolution() + .compareTo(Duration.ofMillis(10)) > 0); + Path f1 = createFile("newfile"); + waitNextTick(f1); + FileSnapshot save = FileSnapshot.save(f1.toFile()); + TimeUnit.NANOSECONDS.sleep( + fsAttrCache.getFsTimestampResolution().dividedBy(2).toNanos()); + assertTrue(save.isModified(f1.toFile())); } /** @@ -126,9 +151,33 @@ public class FileSnapshotTest { */ @Test public void testNewFileNoWait() throws Exception { - File f1 = createFile("newfile"); - FileSnapshot save = FileSnapshot.save(f1); - assertTrue(save.isModified(f1)); + // if filesystem timestamp resolution is smaller than time needed to + // create a file and FileSnapshot the snapshot won't be racily clean + Assume.assumeTrue(fsAttrCache.getFsTimestampResolution() + .compareTo(Duration.ofMillis(10)) > 0); + for (int i = 0; i < 50; i++) { + Instant start = Instant.now(); + Path f1 = createFile("newfile"); + FileSnapshot save = FileSnapshot.save(f1.toFile()); + Duration res = FS.getFileStoreAttributes(f1) + .getFsTimestampResolution(); + Instant end = Instant.now(); + if (Duration.between(start, end) + .compareTo(res.multipliedBy(2)) > 0) { + // This test is racy: under load, there may be a delay between createFile() and + // FileSnapshot.save(). This can stretch the time between the read TS and FS + // creation TS to the point that it exceeds the FS granularity, and we + // conclude it cannot be racily clean, and therefore must be really clean. + // + // This should be relatively uncommon. + continue; + } + // The file wasn't really modified, but it looks just like a "maybe racily clean" + // file. + assertTrue(save.isModified(f1.toFile())); + return; + } + fail("too much load for this test"); } /** @@ -142,19 +191,19 @@ public class FileSnapshotTest { @Test public void testSimulatePackfileReplacement() throws Exception { Assume.assumeFalse(SystemReader.getInstance().isWindows()); - File f1 = createFile("file"); // inode y - File f2 = createFile("fool"); // Guarantees new inode x + Path f1 = createFile("file"); // inode y + Path f2 = createFile("fool"); // Guarantees new inode x // wait on f2 since this method resets lastModified of the file // and leaves lastModified of f1 untouched - waitNextSec(f2); - waitNextSec(f2); - FileTime timestamp = Files.getLastModifiedTime(f1.toPath()); - FileSnapshot save = FileSnapshot.save(f1); - Files.move(f2.toPath(), f1.toPath(), // Now "file" is inode x + waitNextTick(f2); + waitNextTick(f2); + FileTime timestamp = Files.getLastModifiedTime(f1); + FileSnapshot save = FileSnapshot.save(f1.toFile()); + Files.move(f2, f1, // Now "file" is inode x StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - Files.setLastModifiedTime(f1.toPath(), timestamp); - assertTrue(save.isModified(f1)); + Files.setLastModifiedTime(f1, timestamp); + assertTrue(save.isModified(f1.toFile())); assertTrue("unexpected change of fileKey", save.wasFileKeyChanged()); assertFalse("unexpected size change", save.wasSizeChanged()); assertFalse("unexpected lastModified change", @@ -171,24 +220,83 @@ public class FileSnapshotTest { */ @Test public void testFileSizeChanged() throws Exception { - File f = createFile("file"); - FileTime timestamp = Files.getLastModifiedTime(f.toPath()); - FileSnapshot save = FileSnapshot.save(f); + Path f = createFile("file"); + FileTime timestamp = Files.getLastModifiedTime(f); + FileSnapshot save = FileSnapshot.save(f.toFile()); append(f, (byte) 'x'); - Files.setLastModifiedTime(f.toPath(), timestamp); - assertTrue(save.isModified(f)); + Files.setLastModifiedTime(f, timestamp); + assertTrue(save.isModified(f.toFile())); assertTrue(save.wasSizeChanged()); } - private File createFile(String string) throws IOException { - trash.mkdirs(); - File f = File.createTempFile(string, "tdat", trash); - files.add(f); - return f; + @Test + public void fileSnapshotEquals() throws Exception { + // 0 sized FileSnapshot. + FileSnapshot fs1 = FileSnapshot.MISSING_FILE; + // UNKNOWN_SIZE FileSnapshot. + FileSnapshot fs2 = FileSnapshot.save(fs1.lastModifiedInstant()); + + assertTrue(fs1.equals(fs2)); + assertTrue(fs2.equals(fs1)); + } + + @SuppressWarnings("boxing") + @Test + public void detectFileModified() throws IOException { + int failures = 0; + long racyNanos = 0; + final int COUNT = 10000; + ArrayList deltas = new ArrayList<>(); + File f = createFile("test").toFile(); + for (int i = 0; i < COUNT; i++) { + write(f, "a"); + FileSnapshot snapshot = FileSnapshot.save(f); + assertEquals("file should contain 'a'", "a", read(f)); + write(f, "b"); + if (!snapshot.isModified(f)) { + deltas.add(snapshot.lastDelta()); + racyNanos = snapshot.lastRacyThreshold(); + failures++; + } + assertEquals("file should contain 'b'", "b", read(f)); + } + if (failures > 0) { + Stats stats = new Stats(); + LOG.debug( + "delta [ns] since modification FileSnapshot failed to detect"); + for (Long d : deltas) { + stats.add(d); + LOG.debug(String.format("%,d", d)); + } + LOG.error( + "count, failures, eff. racy threshold [ns], delta min [ns]," + + " delta max [ns], delta avg [ns]," + + " delta stddev [ns]"); + LOG.error(String.format( + "%,d, %,d, %,d, %,.0f, %,.0f, %,.0f, %,.0f", COUNT, + failures, racyNanos, stats.min(), stats.max(), + stats.avg(), stats.stddev())); + } + assertTrue( + String.format( + "FileSnapshot: failures to detect file modifications" + + " %d out of %d\n" + + "timestamp resolution %d µs" + + " min racy threshold %d µs" + , failures, COUNT, + fsAttrCache.getFsTimestampResolution().toNanos() / 1000, + fsAttrCache.getMinimalRacyInterval().toNanos() / 1000), + failures == 0); + } + + private Path createFile(String string) throws IOException { + Files.createDirectories(trash); + return Files.createTempFile(trash, string, "tdat"); } - private static void append(File f, byte b) throws IOException { - try (FileOutputStream os = new FileOutputStream(f, true)) { + private static void append(Path f, byte b) throws IOException { + try (OutputStream os = Files.newOutputStream(f, + StandardOpenOption.APPEND)) { os.write(b); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java index d16998db5..eaa245b4e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java @@ -147,9 +147,10 @@ public abstract class GcTestCase extends LocalDiskRepositoryTestCase { return tip; } - protected long lastModified(AnyObjectId objectId) throws IOException { - return repo.getFS().lastModified( - repo.getObjectDatabase().fileFor(objectId)); + protected long lastModified(AnyObjectId objectId) { + return repo.getFS() + .lastModifiedInstant(repo.getObjectDatabase().fileFor(objectId)) + .toEpochMilli(); } protected static void fsTick() throws InterruptedException, IOException { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java index 3ca689ac0..1d3ca0317 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java @@ -66,6 +66,7 @@ import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; import org.junit.Assume; import org.junit.Rule; import org.junit.Test; @@ -158,13 +159,14 @@ public class ObjectDirectoryTest extends RepositoryTestCase { // To deal with racy-git situations JGit's Filesnapshot class will // report a file/folder potentially dirty if - // cachedLastReadTime-cachedLastModificationTime < 2500ms. This - // causes JGit to always rescan a file after modification. But: - // this was true only if the difference between current system time - // and cachedLastModification time was less than 2500ms. If the - // modification is more than 2500ms ago we may have reported a - // file/folder to be clean although it has not been rescanned. A - // Bug. To show the bug we sleep for more than 2500ms + // cachedLastReadTime-cachedLastModificationTime < filesystem + // timestamp resolution. This causes JGit to always rescan a file + // after modification. But: this was true only if the difference + // between current system time and cachedLastModification time was + // less than 2500ms. If the modification is more than 2500ms ago we + // may have reported a file/folder to be clean although it has not + // been rescanned. A bug. To show the bug we sleep for more than + // 2500ms Thread.sleep(2600); File[] ret = packsFolder.listFiles(new FilenameFilter() { @@ -174,7 +176,9 @@ public class ObjectDirectoryTest extends RepositoryTestCase { } }); assertTrue(ret != null && ret.length == 1); - Assume.assumeTrue(tmpFile.lastModified() == ret[0].lastModified()); + FS fs = db.getFS(); + Assume.assumeTrue(fs.lastModifiedInstant(tmpFile) + .equals(fs.lastModifiedInstant(ret[0]))); // all objects are in a new packfile but we will not detect it assertFalse(receivingDB.hasObject(unknownID)); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java index a1433e9fe..d5bc61a69 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java @@ -59,6 +59,7 @@ import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; //import java.nio.file.attribute.BasicFileAttributes; import java.text.ParseException; +import java.time.Instant; import java.util.Collection; import java.util.Iterator; import java.util.Random; @@ -80,6 +81,7 @@ import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.storage.pack.PackConfig; +import org.eclipse.jgit.util.FS; import org.junit.Test; public class PackFileSnapshotTest extends RepositoryTestCase { @@ -188,7 +190,8 @@ public class PackFileSnapshotTest extends RepositoryTestCase { AnyObjectId chk1 = pf.getPackChecksum(); String name = pf.getPackName(); Long length = Long.valueOf(pf.getPackFile().length()); - long m1 = packFilePath.toFile().lastModified(); + FS fs = db.getFS(); + Instant m1 = fs.lastModifiedInstant(packFilePath); // Wait for a filesystem timer tick to enhance probability the rest of // this test is done before the filesystem timer ticks again. @@ -198,15 +201,15 @@ public class PackFileSnapshotTest extends RepositoryTestCase { // content and checksum are different since compression level differs AnyObjectId chk2 = repackAndCheck(6, name, length, chk1) .getPackChecksum(); - long m2 = packFilePath.toFile().lastModified(); - assumeFalse(m2 == m1); + Instant m2 = fs.lastModifiedInstant(packFilePath); + assumeFalse(m2.equals(m1)); // Repack to create packfile with same name, length. Lastmodified is // equal to the previous one because we are in the same filesystem timer // slot. Content and its checksum are different AnyObjectId chk3 = repackAndCheck(7, name, length, chk2) .getPackChecksum(); - long m3 = packFilePath.toFile().lastModified(); + Instant m3 = fs.lastModifiedInstant(packFilePath); // ask for an unknown git object to force jgit to rescan the list of // available packs. If we would ask for a known objectid then JGit would @@ -214,7 +217,7 @@ public class PackFileSnapshotTest extends RepositoryTestCase { db.getObjectDatabase().has(unknownID); assertEquals(chk3, getSinglePack(db.getObjectDatabase().getPacks()) .getPackChecksum()); - assumeTrue(m3 == m2); + assumeTrue(m3.equals(m2)); } // Try repacking so fast that we get two new packs which differ only in @@ -253,7 +256,8 @@ public class PackFileSnapshotTest extends RepositoryTestCase { // Repack to create third packfile AnyObjectId chk3 = repackAndCheck(7, name, length, chk2) .getPackChecksum(); - long m3 = packFilePath.toFile().lastModified(); + FS fs = db.getFS(); + Instant m3 = fs.lastModifiedInstant(packFilePath); db.getObjectDatabase().has(unknownID); assertEquals(chk3, getSinglePack(db.getObjectDatabase().getPacks()) .getPackChecksum()); @@ -265,8 +269,8 @@ public class PackFileSnapshotTest extends RepositoryTestCase { // Copy copy2 to packfile data to force modification of packfile without // changing the packfile's filekey. copyPack(packFileBasePath, ".copy2", ""); - long m2 = packFilePath.toFile().lastModified(); - assumeFalse(m3 == m2); + Instant m2 = fs.lastModifiedInstant(packFilePath); + assumeFalse(m3.equals(m2)); db.getObjectDatabase().has(unknownID); assertEquals(chk2, getSinglePack(db.getObjectDatabase().getPacks()) @@ -275,8 +279,8 @@ public class PackFileSnapshotTest extends RepositoryTestCase { // Copy copy2 to packfile data to force modification of packfile without // changing the packfile's filekey. copyPack(packFileBasePath, ".copy1", ""); - long m1 = packFilePath.toFile().lastModified(); - assumeTrue(m2 == m1); + Instant m1 = fs.lastModifiedInstant(packFilePath); + assumeTrue(m2.equals(m1)); db.getObjectDatabase().has(unknownID); assertEquals(chk1, getSinglePack(db.getObjectDatabase().getPacks()) .getPackChecksum()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java index 5a2bd9c33..24e3bc077 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java @@ -59,6 +59,7 @@ import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Map; @@ -70,6 +71,7 @@ import org.eclipse.jgit.events.ListenerHandle; import org.eclipse.jgit.events.RefsChangedEvent; import org.eclipse.jgit.events.RefsChangedListener; import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; +import org.eclipse.jgit.junit.Repeat; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Ref; @@ -78,6 +80,7 @@ import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.util.FS; import org.junit.Before; import org.junit.Test; @@ -642,6 +645,7 @@ public class RefDirectoryTest extends LocalDiskRepositoryTestCase { assertEquals(B, all.get(HEAD).getObjectId()); } + @Repeat(n = 100, abortOnFailure = false) @Test public void testGetRef_DiscoversModifiedLoose() throws IOException { Map all; @@ -1319,10 +1323,8 @@ public class RefDirectoryTest extends LocalDiskRepositoryTestCase { private void writePackedRefs(String content) throws IOException { File pr = new File(diskRepo.getDirectory(), "packed-refs"); write(pr, content); - - final long now = System.currentTimeMillis(); - final int oneHourAgo = 3600 * 1000; - pr.setLastModified(now - oneHourAgo); + FS fs = diskRepo.getFS(); + fs.setLastModified(pr.toPath(), Instant.now().minusSeconds(3600)); } private void deleteLooseRef(String name) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java index 96caa01a9..9eb181635 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java @@ -59,6 +59,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.time.Instant; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -82,6 +83,7 @@ import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.IO; import org.junit.Rule; @@ -790,12 +792,14 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { * * @param name * the file in the repository to force a time change on. + * @throws IOException */ - private void BUG_WorkAroundRacyGitIssues(String name) { + private void BUG_WorkAroundRacyGitIssues(String name) throws IOException { File path = new File(db.getDirectory(), name); - long old = path.lastModified(); + FS fs = db.getFS(); + Instant old = fs.lastModifiedInstant(path); long set = 1250379778668L; // Sat Aug 15 20:12:58 GMT-03:30 2009 - path.setLastModified(set); - assertTrue("time changed", old != path.lastModified()); + fs.setLastModified(path.toPath(), Instant.ofEpochMilli(set)); + assertFalse("time changed", old.equals(fs.lastModifiedInstant(path))); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java index 21d8d66ad..22dc47155 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java @@ -51,8 +51,10 @@ package org.eclipse.jgit.lib; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.HOURS; +import static java.util.concurrent.TimeUnit.MICROSECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.NANOSECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import static org.eclipse.jgit.util.FileUtils.pathToString; import static org.junit.Assert.assertArrayEquals; @@ -1224,8 +1226,18 @@ public class ConfigTest { @Test public void testTimeUnit() throws ConfigInvalidException { + assertEquals(0, parseTime("0", NANOSECONDS)); + assertEquals(2, parseTime("2ns", NANOSECONDS)); + assertEquals(200, parseTime("200 nanoseconds", NANOSECONDS)); + + assertEquals(0, parseTime("0", MICROSECONDS)); + assertEquals(2, parseTime("2us", MICROSECONDS)); + assertEquals(2, parseTime("2000 nanoseconds", MICROSECONDS)); + assertEquals(200, parseTime("200 microseconds", MICROSECONDS)); + assertEquals(0, parseTime("0", MILLISECONDS)); assertEquals(2, parseTime("2ms", MILLISECONDS)); + assertEquals(2, parseTime("2000microseconds", MILLISECONDS)); assertEquals(200, parseTime("200 milliseconds", MILLISECONDS)); assertEquals(0, parseTime("0s", SECONDS)); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexModificationTimesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexModificationTimesTest.java index b9bbbeb9e..a3634141c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexModificationTimesTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexModificationTimesTest.java @@ -37,8 +37,12 @@ */ package org.eclipse.jgit.lib; +import static java.time.Instant.EPOCH; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.time.Instant; + import org.eclipse.jgit.api.Git; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheEntry; @@ -63,11 +67,11 @@ public class IndexModificationTimesTest extends RepositoryTestCase { DirCacheEntry entry = dc.getEntry(path); DirCacheEntry entry2 = dc.getEntry(path); - assertTrue("last modified shall not be zero!", - entry.getLastModified() != 0); + assertFalse("last modified shall not be the epoch!", + entry.getLastModifiedInstant().equals(EPOCH)); - assertTrue("last modified shall not be zero!", - entry2.getLastModified() != 0); + assertFalse("last modified shall not be the epoch!", + entry2.getLastModifiedInstant().equals(EPOCH)); writeTrashFile(path, "new content"); git.add().addFilepattern(path).call(); @@ -77,11 +81,11 @@ public class IndexModificationTimesTest extends RepositoryTestCase { entry = dc.getEntry(path); entry2 = dc.getEntry(path); - assertTrue("last modified shall not be zero!", - entry.getLastModified() != 0); + assertFalse("last modified shall not be the epoch!", + entry.getLastModifiedInstant().equals(EPOCH)); - assertTrue("last modified shall not be zero!", - entry2.getLastModified() != 0); + assertFalse("last modified shall not be the epoch!", + entry2.getLastModifiedInstant().equals(EPOCH)); } } @@ -97,7 +101,7 @@ public class IndexModificationTimesTest extends RepositoryTestCase { DirCache dc = db.readDirCache(); DirCacheEntry entry = dc.getEntry(path); - long masterLastMod = entry.getLastModified(); + Instant masterLastMod = entry.getLastModifiedInstant(); git.checkout().setCreateBranch(true).setName("side").call(); @@ -110,7 +114,7 @@ public class IndexModificationTimesTest extends RepositoryTestCase { dc = db.readDirCache(); entry = dc.getEntry(path); - long sideLastMode = entry.getLastModified(); + Instant sideLastMod = entry.getLastModifiedInstant(); Thread.sleep(2000); @@ -120,9 +124,10 @@ public class IndexModificationTimesTest extends RepositoryTestCase { dc = db.readDirCache(); entry = dc.getEntry(path); - assertTrue("shall have equal mod time!", masterLastMod == sideLastMode); - assertTrue("shall not equal master timestamp!", - entry.getLastModified() == masterLastMod); + assertTrue("shall have equal mod time!", + masterLastMod.equals(sideLastMod)); + assertTrue("shall have equal master timestamp!", + entry.getLastModifiedInstant().equals(masterLastMod)); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java index bb24994ee..d3e4efef5 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java @@ -42,95 +42,29 @@ */ package org.eclipse.jgit.lib; -import static java.lang.Long.valueOf; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeTrue; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.util.TreeSet; +import java.time.Instant; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.time.TimeUtil; import org.eclipse.jgit.treewalk.FileTreeIterator; -import org.eclipse.jgit.treewalk.FileTreeIteratorWithTimeControl; -import org.eclipse.jgit.treewalk.NameConflictTreeWalk; -import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.util.FS; import org.junit.Test; public class RacyGitTests extends RepositoryTestCase { - @Test - public void testIterator() throws IllegalStateException, IOException, - InterruptedException { - TreeSet modTimes = new TreeSet<>(); - File lastFile = null; - for (int i = 0; i < 10; i++) { - lastFile = new File(db.getWorkTree(), "0." + i); - FileUtils.createNewFile(lastFile); - if (i == 5) - fsTick(lastFile); - } - modTimes.add(valueOf(fsTick(lastFile))); - for (int i = 0; i < 10; i++) { - lastFile = new File(db.getWorkTree(), "1." + i); - FileUtils.createNewFile(lastFile); - } - modTimes.add(valueOf(fsTick(lastFile))); - for (int i = 0; i < 10; i++) { - lastFile = new File(db.getWorkTree(), "2." + i); - FileUtils.createNewFile(lastFile); - if (i % 4 == 0) - fsTick(lastFile); - } - FileTreeIteratorWithTimeControl fileIt = new FileTreeIteratorWithTimeControl( - db, modTimes); - try (NameConflictTreeWalk tw = new NameConflictTreeWalk(db)) { - tw.addTree(fileIt); - tw.setRecursive(true); - FileTreeIterator t; - long t0 = 0; - for (int i = 0; i < 10; i++) { - assertTrue(tw.next()); - t = tw.getTree(0, FileTreeIterator.class); - if (i == 0) { - t0 = t.getEntryLastModified(); - } else { - assertEquals(t0, t.getEntryLastModified()); - } - } - long t1 = 0; - for (int i = 0; i < 10; i++) { - assertTrue(tw.next()); - t = tw.getTree(0, FileTreeIterator.class); - if (i == 0) { - t1 = t.getEntryLastModified(); - assertTrue(t1 > t0); - } else { - assertEquals(t1, t.getEntryLastModified()); - } - } - long t2 = 0; - for (int i = 0; i < 10; i++) { - assertTrue(tw.next()); - t = tw.getTree(0, FileTreeIterator.class); - if (i == 0) { - t2 = t.getEntryLastModified(); - assertTrue(t2 > t1); - } else { - assertEquals(t2, t.getEntryLastModified()); - } - } - } - } @Test public void testRacyGitDetection() throws Exception { - TreeSet modTimes = new TreeSet<>(); - File lastFile; - // Reset to force creation of index file try (Git git = new Git(db)) { git.reset().call(); @@ -138,48 +72,60 @@ public class RacyGitTests extends RepositoryTestCase { // wait to ensure that modtimes of the file doesn't match last index // file modtime - modTimes.add(valueOf(fsTick(db.getIndexFile()))); + fsTick(db.getIndexFile()); // create two files - addToWorkDir("a", "a"); - lastFile = addToWorkDir("b", "b"); + File a = writeToWorkDir("a", "a"); + File b = writeToWorkDir("b", "b"); + TimeUtil.setLastModifiedOf(a.toPath(), b.toPath()); + TimeUtil.setLastModifiedOf(b.toPath(), b.toPath()); // wait to ensure that file-modTimes and therefore index entry modTime // doesn't match the modtime of index-file after next persistance - modTimes.add(valueOf(fsTick(lastFile))); + fsTick(b); // now add both files to the index. No racy git expected - resetIndex(new FileTreeIteratorWithTimeControl(db, modTimes)); + resetIndex(new FileTreeIterator(db)); assertEquals( - "[a, mode:100644, time:t0, length:1, content:a]" + - "[b, mode:100644, time:t0, length:1, content:b]", + "[a, mode:100644, time:t0, length:1, content:a]" + + "[b, mode:100644, time:t0, length:1, content:b]", indexState(SMUDGE | MOD_TIME | LENGTH | CONTENT)); - // Remember the last modTime of index file. All modifications times of - // further modification are translated to this value so it looks that - // files have been modified in the same time slot as the index file - long indexMod = db.getIndexFile().lastModified(); - modTimes.add(Long.valueOf(indexMod)); - - // modify one file - long aMod = addToWorkDir("a", "a2").lastModified(); - assumeTrue(aMod == indexMod); - - // now update the index the index. 'a' has to be racily clean -- because - // it's modification time is exactly the same as the previous index file - // mod time. - resetIndex(new FileTreeIteratorWithTimeControl(db, modTimes)); - - db.readDirCache(); - // although racily clean a should not be reported as being dirty + // wait to ensure the file 'a' is updated at t1. + fsTick(db.getIndexFile()); + + // Create a racy git situation. This is a situation that the index is + // updated and then a file is modified within the same tick of the + // filesystem timestamp resolution. By changing the index file + // artificially, we create a fake racy situation. + File updatedA = writeToWorkDir("a", "a2"); + Instant newLastModified = TimeUtil + .setLastModifiedWithOffset(updatedA.toPath(), 100L); + resetIndex(new FileTreeIterator(db)); + FS.DETECTED.setLastModified(db.getIndexFile().toPath(), + newLastModified); + + DirCache dc = db.readDirCache(); + // check index state: although racily clean a should not be reported as + // being dirty since we forcefully reset the index to match the working + // tree assertEquals( - "[a, mode:100644, time:t1, smudged, length:0, content:a2]" + - "[b, mode:100644, time:t0, length:1, content:b]", - indexState(SMUDGE|MOD_TIME|LENGTH|CONTENT)); + "[a, mode:100644, time:t1, smudged, length:0, content:a2]" + + "[b, mode:100644, time:t0, length:1, content:b]", + indexState(SMUDGE | MOD_TIME | LENGTH | CONTENT)); + + // compare state of files in working tree with index to check that + // FileTreeIterator.isModified() works as expected + FileTreeIterator f = new FileTreeIterator(db.getWorkTree(), db.getFS(), + db.getConfig().get(WorkingTreeOptions.KEY)); + assertTrue(f.findFile("a")); + try (ObjectReader reader = db.newObjectReader()) { + assertFalse(f.isModified(dc.getEntry("a"), false, reader)); + } } - private File addToWorkDir(String path, String content) throws IOException { + private File writeToWorkDir(String path, String content) throws IOException { File f = new File(db.getWorkTree(), path); try (FileOutputStream fos = new FileOutputStream(f)) { fos.write(content.getBytes(UTF_8)); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java index 8ca5d453f..fa02227a5 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java @@ -43,6 +43,7 @@ package org.eclipse.jgit.merge; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.time.Instant.EPOCH; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -54,6 +55,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.time.Instant; import java.util.Arrays; import java.util.Map; @@ -1090,13 +1092,13 @@ public class MergerTest extends RepositoryTestCase { @Theory public void checkForCorrectIndex(MergeStrategy strategy) throws Exception { File f; - long lastTs4, lastTsIndex; + Instant lastTs4, lastTsIndex; Git git = Git.wrap(db); File indexFile = db.getIndexFile(); // Create initial content and remember when the last file was written. f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig"); - lastTs4 = FS.DETECTED.lastModified(f); + lastTs4 = FS.DETECTED.lastModifiedInstant(f); // add all files, commit and check this doesn't update any working tree // files and that the index is in a new file system timer tick. Make @@ -1109,8 +1111,9 @@ public class MergerTest extends RepositoryTestCase { checkConsistentLastModified("0", "1", "2", "3", "4"); checkModificationTimeStampOrder("1", "2", "3", "4", "<.git/index"); assertEquals("Commit should not touch working tree file 4", lastTs4, - FS.DETECTED.lastModified(new File(db.getWorkTree(), "4"))); - lastTsIndex = FS.DETECTED.lastModified(indexFile); + FS.DETECTED + .lastModifiedInstant(new File(db.getWorkTree(), "4"))); + lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); // Do modifications on the master branch. Then add and commit. This // should touch only "0", "2 and "3" @@ -1124,7 +1127,7 @@ public class MergerTest extends RepositoryTestCase { checkConsistentLastModified("0", "1", "2", "3", "4"); checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*" + lastTsIndex, "<0", "2", "3", "<.git/index"); - lastTsIndex = FS.DETECTED.lastModified(indexFile); + lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); // Checkout a side branch. This should touch only "0", "2 and "3" fsTick(indexFile); @@ -1133,7 +1136,7 @@ public class MergerTest extends RepositoryTestCase { checkConsistentLastModified("0", "1", "2", "3", "4"); checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*" + lastTsIndex, "<0", "2", "3", ".git/index"); - lastTsIndex = FS.DETECTED.lastModified(indexFile); + lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); // This checkout may have populated worktree and index so fast that we // may have smudged entries now. Check that we have the right content @@ -1146,13 +1149,13 @@ public class MergerTest extends RepositoryTestCase { indexState(CONTENT)); fsTick(indexFile); f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig"); - lastTs4 = FS.DETECTED.lastModified(f); + lastTs4 = FS.DETECTED.lastModifiedInstant(f); fsTick(f); git.add().addFilepattern(".").call(); checkConsistentLastModified("0", "1", "2", "3", "4"); checkModificationTimeStampOrder("*" + lastTsIndex, "<0", "1", "2", "3", "4", "<.git/index"); - lastTsIndex = FS.DETECTED.lastModified(indexFile); + lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); // Do modifications on the side branch. Touch only "1", "2 and "3" fsTick(indexFile); @@ -1163,7 +1166,7 @@ public class MergerTest extends RepositoryTestCase { checkConsistentLastModified("0", "1", "2", "3", "4"); checkModificationTimeStampOrder("0", "4", "*" + lastTs4, "<*" + lastTsIndex, "<1", "2", "3", "<.git/index"); - lastTsIndex = FS.DETECTED.lastModified(indexFile); + lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); // merge master and side. Should only touch "0," "2" and "3" fsTick(indexFile); @@ -1330,9 +1333,10 @@ public class MergerTest extends RepositoryTestCase { assertEquals( "IndexEntry with path " + path - + " has lastmodified with is different from the worktree file", - FS.DETECTED.lastModified(new File(workTree, path)), dc.getEntry(path) - .getLastModified()); + + " has lastmodified which is different from the worktree file", + FS.DETECTED.lastModifiedInstant(new File(workTree, path)), + dc.getEntry(path) + .getLastModifiedInstant()); } // Assert that modification timestamps of working tree files are as @@ -1341,21 +1345,22 @@ public class MergerTest extends RepositoryTestCase { // then this file must be younger then file i. A path "*" // represents a file with a modification time of // E.g. ("a", "b", " lastMod); - else + curMod.compareTo(lastMod) > 0); + } else { assertTrue("path " + p + " is older than predecesssor", - curMod >= lastMod); + curMod.compareTo(lastMod) >= 0); + } } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java index b401d2bea..43648206f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java @@ -46,12 +46,13 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.util.FileUtils.pathToString; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.StringTokenizer; import org.eclipse.jgit.errors.ConfigInvalidException; @@ -85,42 +86,44 @@ public class FileBasedConfigTest { private static final String CONTENT3 = "[" + USER + "]\n\t" + NAME + " = " + ALICE + "\n" + "[" + USER + "]\n\t" + EMAIL + " = " + ALICE_EMAIL; - private File trash; + private Path trash; @Before public void setUp() throws Exception { - trash = File.createTempFile("tmp_", ""); - trash.delete(); - assertTrue("mkdir " + trash, trash.mkdir()); + trash = Files.createTempDirectory("tmp_"); + FS.getFileStoreAttributes(trash.getParent()); } @After public void tearDown() throws Exception { - FileUtils.delete(trash, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); + FileUtils.delete(trash.toFile(), + FileUtils.RECURSIVE | FileUtils.SKIP_MISSING | FileUtils.RETRY); } @Test public void testSystemEncoding() throws IOException, ConfigInvalidException { - final File file = createFile(CONTENT1.getBytes(UTF_8)); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(CONTENT1.getBytes(UTF_8)); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); config.setString(USER, null, NAME, BOB); config.save(); - assertArrayEquals(CONTENT2.getBytes(UTF_8), IO.readFully(file)); + assertArrayEquals(CONTENT2.getBytes(UTF_8), IO.readFully(file.toFile())); } @Test public void testUTF8withoutBOM() throws IOException, ConfigInvalidException { - final File file = createFile(CONTENT1.getBytes(UTF_8)); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(CONTENT1.getBytes(UTF_8)); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); config.setString(USER, null, NAME, BOB); config.save(); - assertArrayEquals(CONTENT2.getBytes(UTF_8), IO.readFully(file)); + assertArrayEquals(CONTENT2.getBytes(UTF_8), IO.readFully(file.toFile())); } @Test @@ -131,8 +134,9 @@ public class FileBasedConfigTest { bos1.write(0xBF); bos1.write(CONTENT1.getBytes(UTF_8)); - final File file = createFile(bos1.toByteArray()); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(bos1.toByteArray()); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); @@ -144,7 +148,7 @@ public class FileBasedConfigTest { bos2.write(0xBB); bos2.write(0xBF); bos2.write(CONTENT2.getBytes(UTF_8)); - assertArrayEquals(bos2.toByteArray(), IO.readFully(file)); + assertArrayEquals(bos2.toByteArray(), IO.readFully(file.toFile())); } @Test @@ -153,8 +157,9 @@ public class FileBasedConfigTest { bos1.write(" \n\t".getBytes(UTF_8)); bos1.write(CONTENT1.getBytes(UTF_8)); - final File file = createFile(bos1.toByteArray()); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(bos1.toByteArray()); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); @@ -164,19 +169,20 @@ public class FileBasedConfigTest { final ByteArrayOutputStream bos2 = new ByteArrayOutputStream(); bos2.write(" \n\t".getBytes(UTF_8)); bos2.write(CONTENT2.getBytes(UTF_8)); - assertArrayEquals(bos2.toByteArray(), IO.readFully(file)); + assertArrayEquals(bos2.toByteArray(), IO.readFully(file.toFile())); } @Test public void testIncludeAbsolute() throws IOException, ConfigInvalidException { - final File includedFile = createFile(CONTENT1.getBytes(UTF_8)); + final Path includedFile = createFile(CONTENT1.getBytes(UTF_8)); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write("[include]\npath=".getBytes(UTF_8)); - bos.write(pathToString(includedFile).getBytes(UTF_8)); + bos.write(pathToString(includedFile.toFile()).getBytes(UTF_8)); - final File file = createFile(bos.toByteArray()); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(bos.toByteArray()); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); } @@ -184,13 +190,14 @@ public class FileBasedConfigTest { @Test public void testIncludeRelativeDot() throws IOException, ConfigInvalidException { - final File includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1"); + final Path includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1"); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write("[include]\npath=".getBytes(UTF_8)); - bos.write(("./" + includedFile.getName()).getBytes(UTF_8)); + bos.write(("./" + includedFile.getFileName()).getBytes(UTF_8)); - final File file = createFile(bos.toByteArray(), "dir1"); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(bos.toByteArray(), "dir1"); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); } @@ -198,14 +205,15 @@ public class FileBasedConfigTest { @Test public void testIncludeRelativeDotDot() throws IOException, ConfigInvalidException { - final File includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1"); + final Path includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1"); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write("[include]\npath=".getBytes(UTF_8)); - bos.write(("../" + includedFile.getParentFile().getName() + "/" - + includedFile.getName()).getBytes(UTF_8)); + bos.write(("../" + includedFile.getParent().getFileName() + "/" + + includedFile.getFileName()).getBytes(UTF_8)); - final File file = createFile(bos.toByteArray(), "dir2"); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(bos.toByteArray(), "dir2"); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); } @@ -213,13 +221,14 @@ public class FileBasedConfigTest { @Test public void testIncludeRelativeDotDotNotFound() throws IOException, ConfigInvalidException { - final File includedFile = createFile(CONTENT1.getBytes(UTF_8)); + final Path includedFile = createFile(CONTENT1.getBytes(UTF_8)); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write("[include]\npath=".getBytes(UTF_8)); - bos.write(("../" + includedFile.getName()).getBytes(UTF_8)); + bos.write(("../" + includedFile.getFileName()).getBytes(UTF_8)); - final File file = createFile(bos.toByteArray()); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(bos.toByteArray()); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(null, config.getString(USER, null, NAME)); } @@ -227,16 +236,16 @@ public class FileBasedConfigTest { @Test public void testIncludeWithTilde() throws IOException, ConfigInvalidException { - final File includedFile = createFile(CONTENT1.getBytes(UTF_8), "home"); + final Path includedFile = createFile(CONTENT1.getBytes(UTF_8), "home"); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write("[include]\npath=".getBytes(UTF_8)); - bos.write(("~/" + includedFile.getName()).getBytes(UTF_8)); + bos.write(("~/" + includedFile.getFileName()).getBytes(UTF_8)); - final File file = createFile(bos.toByteArray(), "repo"); + final Path file = createFile(bos.toByteArray(), "repo"); final FS fs = FS.DETECTED.newInstance(); - fs.setUserHome(includedFile.getParentFile()); + fs.setUserHome(includedFile.getParent().toFile()); - final FileBasedConfig config = new FileBasedConfig(file, fs); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), fs); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); } @@ -246,13 +255,14 @@ public class FileBasedConfigTest { throws IOException, ConfigInvalidException { // use a content with multiple sections and multiple key/value pairs // because code for first line works different than for subsequent lines - final File includedFile = createFile(CONTENT3.getBytes(UTF_8), "dir1"); + final Path includedFile = createFile(CONTENT3.getBytes(UTF_8), "dir1"); - final File file = createFile(new byte[0], "dir2"); - FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(new byte[0], "dir2"); + FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.setString("include", null, "path", - ("../" + includedFile.getParentFile().getName() + "/" - + includedFile.getName())); + ("../" + includedFile.getParent().getFileName() + "/" + + includedFile.getFileName())); // just by setting the include.path, it won't be included assertEquals(null, config.getString(USER, null, NAME)); @@ -267,7 +277,7 @@ public class FileBasedConfigTest { assertEquals(2, new StringTokenizer(expectedText, "\n", false).countTokens()); - config = new FileBasedConfig(file, FS.DETECTED); + config = new FileBasedConfig(file.toFile(), FS.DETECTED); config.load(); String actualText = config.toText(); @@ -285,16 +295,17 @@ public class FileBasedConfigTest { assertEquals(ALICE_EMAIL, config.getString(USER, null, EMAIL)); } - private File createFile(byte[] content) throws IOException { + private Path createFile(byte[] content) throws IOException { return createFile(content, null); } - private File createFile(byte[] content, String subdir) throws IOException { - File dir = subdir != null ? new File(trash, subdir) : trash; - dir.mkdirs(); + private Path createFile(byte[] content, String subdir) throws IOException { + Path dir = subdir != null ? trash.resolve(subdir) : trash; + Files.createDirectories(dir); - File f = File.createTempFile(getClass().getName(), null, dir); - try (FileOutputStream os = new FileOutputStream(f, true)) { + Path f = Files.createTempFile(dir, getClass().getName(), null); + try (OutputStream os = Files.newOutputStream(f, + StandardOpenOption.APPEND)) { os.write(content); } return f; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java index 1a22e10f4..2e5027f7e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java @@ -56,10 +56,13 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; +import java.time.Instant; +import java.util.concurrent.TimeUnit; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.transport.OpenSshConfig.Host; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.SystemReader; import org.junit.Before; @@ -91,13 +94,19 @@ public class OpenSshConfigTest extends RepositoryTestCase { } private void config(String data) throws IOException { - long lastMtime = configFile.lastModified(); + FS fs = FS.DETECTED; + long resolution = FS.getFileStoreAttributes(configFile.toPath()) + .getFsTimestampResolution().toNanos(); + Instant lastMtime = fs.lastModifiedInstant(configFile); do { try (final OutputStreamWriter fw = new OutputStreamWriter( new FileOutputStream(configFile), UTF_8)) { fw.write(data); + TimeUnit.NANOSECONDS.sleep(resolution); + } catch (InterruptedException e) { + Thread.interrupted(); } - } while (lastMtime == configFile.lastModified()); + } while (lastMtime.equals(fs.lastModifiedInstant(configFile))); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java index 33e32cd81..9a0e7d2ea 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java @@ -52,6 +52,7 @@ import static org.junit.Assert.assertTrue; import java.io.File; import java.io.IOException; import java.security.MessageDigest; +import java.time.Instant; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ResetCommand.ResetType; @@ -86,7 +87,7 @@ import org.junit.Test; public class FileTreeIteratorTest extends RepositoryTestCase { private final String[] paths = { "a,", "a,b", "a/b", "a0b" }; - private long[] mtime; + private Instant[] mtime; @Override @Before @@ -99,11 +100,11 @@ public class FileTreeIteratorTest extends RepositoryTestCase { // This should stress the sorting code better than doing it in // the correct order. // - mtime = new long[paths.length]; + mtime = new Instant[paths.length]; for (int i = paths.length - 1; i >= 0; i--) { final String s = paths[i]; writeTrashFile(s, s); - mtime[i] = FS.DETECTED.lastModified(new File(trash, s)); + mtime[i] = db.getFS().lastModifiedInstant(new File(trash, s)); } } @@ -199,7 +200,7 @@ public class FileTreeIteratorTest extends RepositoryTestCase { assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode); assertEquals(paths[0], nameOf(top)); assertEquals(paths[0].length(), top.getEntryLength()); - assertEquals(mtime[0], top.getEntryLastModified()); + assertEquals(mtime[0], top.getEntryLastModifiedInstant()); top.next(1); assertFalse(top.first()); @@ -207,7 +208,7 @@ public class FileTreeIteratorTest extends RepositoryTestCase { assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode); assertEquals(paths[1], nameOf(top)); assertEquals(paths[1].length(), top.getEntryLength()); - assertEquals(mtime[1], top.getEntryLastModified()); + assertEquals(mtime[1], top.getEntryLastModifiedInstant()); top.next(1); assertFalse(top.first()); @@ -222,7 +223,7 @@ public class FileTreeIteratorTest extends RepositoryTestCase { assertFalse(sub.eof()); assertEquals(paths[2], nameOf(sub)); assertEquals(paths[2].length(), subfti.getEntryLength()); - assertEquals(mtime[2], subfti.getEntryLastModified()); + assertEquals(mtime[2], subfti.getEntryLastModifiedInstant()); sub.next(1); assertTrue(sub.eof()); @@ -233,7 +234,7 @@ public class FileTreeIteratorTest extends RepositoryTestCase { assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode); assertEquals(paths[3], nameOf(top)); assertEquals(paths[3].length(), top.getEntryLength()); - assertEquals(mtime[3], top.getEntryLastModified()); + assertEquals(mtime[3], top.getEntryLastModifiedInstant()); top.next(1); assertTrue(top.eof()); @@ -345,20 +346,21 @@ public class FileTreeIteratorTest extends RepositoryTestCase { @Test public void testIsModifiedFileSmudged() throws Exception { File f = writeTrashFile("file", "content"); + FS fs = db.getFS(); try (Git git = new Git(db)) { // The idea of this test is to check the smudged handling // Hopefully fsTick will make sure our entry gets smudged fsTick(f); writeTrashFile("file", "content"); - long lastModified = f.lastModified(); + Instant lastModified = fs.lastModifiedInstant(f); git.add().addFilepattern("file").call(); writeTrashFile("file", "conten2"); - f.setLastModified(lastModified); + fs.setLastModified(f.toPath(), lastModified); // We cannot trust this to go fast enough on // a system with less than one-second lastModified // resolution, so we force the index to have the // same timestamp as the file we look at. - db.getIndexFile().setLastModified(lastModified); + fs.setLastModified(db.getIndexFile().toPath(), lastModified); } DirCacheEntry dce = db.readDirCache().getEntry("file"); FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(), db diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorWithTimeControl.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorWithTimeControl.java deleted file mode 100644 index fc79d4586..000000000 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorWithTimeControl.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2010, Christian Halstrick - * and other copyright owners as documented in the project's IP log. - * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package org.eclipse.jgit.treewalk; - -import java.io.File; -import java.util.SortedSet; -import java.util.TreeSet; - -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.ObjectReader; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.util.FS; - -/** - * A {@link FileTreeIterator} used in tests which allows to specify explicitly - * what will be returned by {@link #getEntryLastModified()}. This allows to - * write tests where certain files have to have the same modification time. - *

- * This iterator is configured by a list of strictly increasing long values - * t(0), t(1), ..., t(n). For each file with a modification between t(x) and - * t(x+1) [ t(x) <= time < t(x+1) ] this iterator will report t(x). For - * files with a modification time smaller t(0) a modification time of 0 is - * returned. For files with a modification time greater or equal t(n) t(n) will - * be returned. - *

- * This class was written especially to test racy-git problems - */ -public class FileTreeIteratorWithTimeControl extends FileTreeIterator { - private TreeSet modTimes; - - public FileTreeIteratorWithTimeControl(FileTreeIterator p, Repository repo, - TreeSet modTimes) { - super(p, repo.getWorkTree(), repo.getFS()); - this.modTimes = modTimes; - } - - public FileTreeIteratorWithTimeControl(FileTreeIterator p, File f, FS fs, - TreeSet modTimes) { - super(p, f, fs); - this.modTimes = modTimes; - } - - public FileTreeIteratorWithTimeControl(Repository repo, - TreeSet modTimes) { - super(repo); - this.modTimes = modTimes; - } - - public FileTreeIteratorWithTimeControl(File f, FS fs, - TreeSet modTimes) { - super(f, fs, new Config().get(WorkingTreeOptions.KEY)); - this.modTimes = modTimes; - } - - @Override - public AbstractTreeIterator createSubtreeIterator(ObjectReader reader) { - return new FileTreeIteratorWithTimeControl(this, - ((FileEntry) current()).getFile(), fs, modTimes); - } - - @Override - public long getEntryLastModified() { - if (modTimes == null) - return 0; - Long cutOff = Long.valueOf(super.getEntryLastModified() + 1); - SortedSet head = modTimes.headSet(cutOff); - return head.isEmpty() ? 0 : head.last().longValue(); - } -} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java index 59c8e31c0..2054e1efa 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java @@ -43,6 +43,7 @@ package org.eclipse.jgit.util; +import static java.time.Instant.EPOCH; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -65,6 +66,7 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jgit.errors.CommandFailedException; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.RepositoryCache; import org.junit.After; import org.junit.Assume; import org.junit.Before; @@ -105,7 +107,7 @@ public class FSTest { assertTrue(fs.exists(link)); String targetName = fs.readSymLink(link); assertEquals("å", targetName); - assertTrue(fs.lastModified(link) > 0); + assertTrue(fs.lastModifiedInstant(link).compareTo(EPOCH) > 0); assertTrue(fs.exists(link)); assertFalse(fs.canExecute(link)); assertEquals(2, fs.length(link)); @@ -118,8 +120,9 @@ public class FSTest { // Now create the link target FileUtils.createNewFile(target); assertTrue(fs.exists(link)); - assertTrue(fs.lastModified(link) > 0); - assertTrue(fs.lastModified(target) > fs.lastModified(link)); + assertTrue(fs.lastModifiedInstant(link).compareTo(EPOCH) > 0); + assertTrue(fs.lastModifiedInstant(target) + .compareTo(fs.lastModifiedInstant(link)) > 0); assertFalse(fs.canExecute(link)); fs.setExecute(target, true); assertFalse(fs.canExecute(link)); @@ -200,7 +203,8 @@ public class FSTest { .ofPattern("uuuu-MMM-dd HH:mm:ss.nnnnnnnnn", Locale.ENGLISH) .withZone(ZoneId.systemDefault()); Path dir = Files.createTempDirectory("probe-filesystem"); - Duration resolution = FS.getFsTimerResolution(dir); + Duration resolution = FS.getFileStoreAttributes(dir) + .getFsTimestampResolution(); long resolutionNs = resolution.toNanos(); assertTrue(resolutionNs > 0); for (int i = 0; i < 10; i++) { @@ -223,4 +227,11 @@ public class FSTest { } } } + + // bug 548682 + @Test + public void testRepoCacheRelativePathUnbornRepo() { + assertFalse(RepositoryCache.FileKey + .isGitRepository(new File("repo.git"), FS.DETECTED)); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/SimpleLruCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/SimpleLruCacheTest.java new file mode 100644 index 000000000..5894f7dc5 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/SimpleLruCacheTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2019, Matthias Sohn + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class SimpleLruCacheTest { + + private Path trash; + + private SimpleLruCache cache; + + + @Before + public void setup() throws IOException { + trash = Files.createTempDirectory("tmp_"); + cache = new SimpleLruCache<>(100, 0.2f); + } + + @Before + @After + public void tearDown() throws Exception { + FileUtils.delete(trash.toFile(), + FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); + } + + @Test + public void testPutGet() { + cache.put("a", "A"); + cache.put("z", "Z"); + assertEquals("A", cache.get("a")); + assertEquals("Z", cache.get("z")); + } + + @Test(expected = IllegalArgumentException.class) + public void testPurgeFactorTooLarge() { + cache.configure(5, 1.01f); + } + + @Test(expected = IllegalArgumentException.class) + public void testPurgeFactorTooLarge2() { + cache.configure(5, 100); + } + + @Test(expected = IllegalArgumentException.class) + public void testPurgeFactorTooSmall() { + cache.configure(5, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testPurgeFactorTooSmall2() { + cache.configure(5, -100); + } + + @Test + public void testGetMissing() { + assertEquals(null, cache.get("a")); + } + + @Test + public void testPurge() { + for (int i = 0; i < 101; i++) { + cache.put("a" + i, "a" + i); + } + assertEquals(80, cache.size()); + assertNull(cache.get("a0")); + assertNull(cache.get("a20")); + assertNotNull(cache.get("a21")); + assertNotNull(cache.get("a99")); + } + + @Test + public void testConfigure() { + for (int i = 0; i < 100; i++) { + cache.put("a" + i, "a" + i); + } + assertEquals(100, cache.size()); + cache.configure(10, 0.3f); + assertEquals(7, cache.size()); + assertNull(cache.get("a0")); + assertNull(cache.get("a92")); + assertNotNull(cache.get("a93")); + assertNotNull(cache.get("a99")); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StatsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StatsTest.java new file mode 100644 index 000000000..8b253828c --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StatsTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2019, Matthias Sohn + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.eclipse.jgit.util.Stats; +import org.junit.Test; + +public class StatsTest { + @Test + public void testStatsTrivial() { + Stats s = new Stats(); + s.add(1); + s.add(1); + s.add(1); + assertEquals(3, s.count()); + assertEquals(1.0, s.min(), 1E-6); + assertEquals(1.0, s.max(), 1E-6); + assertEquals(1.0, s.avg(), 1E-6); + assertEquals(0.0, s.var(), 1E-6); + assertEquals(0.0, s.stddev(), 1E-6); + } + + @Test + public void testStats() { + Stats s = new Stats(); + s.add(1); + s.add(2); + s.add(3); + s.add(4); + assertEquals(4, s.count()); + assertEquals(1.0, s.min(), 1E-6); + assertEquals(4.0, s.max(), 1E-6); + assertEquals(2.5, s.avg(), 1E-6); + assertEquals(1.666667, s.var(), 1E-6); + assertEquals(1.290994, s.stddev(), 1E-6); + } + + @Test + /** + * see + * https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Example + */ + public void testStatsCancellationExample1() { + Stats s = new Stats(); + s.add(1E8 + 4); + s.add(1E8 + 7); + s.add(1E8 + 13); + s.add(1E8 + 16); + assertEquals(4, s.count()); + assertEquals(1E8 + 4, s.min(), 1E-6); + assertEquals(1E8 + 16, s.max(), 1E-6); + assertEquals(1E8 + 10, s.avg(), 1E-6); + assertEquals(30, s.var(), 1E-6); + assertEquals(5.477226, s.stddev(), 1E-6); + } + + @Test + /** + * see + * https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Example + */ + public void testStatsCancellationExample2() { + Stats s = new Stats(); + s.add(1E9 + 4); + s.add(1E9 + 7); + s.add(1E9 + 13); + s.add(1E9 + 16); + assertEquals(4, s.count()); + assertEquals(1E9 + 4, s.min(), 1E-6); + assertEquals(1E9 + 16, s.max(), 1E-6); + assertEquals(1E9 + 10, s.avg(), 1E-6); + assertEquals(30, s.var(), 1E-6); + assertEquals(5.477226, s.stddev(), 1E-6); + } + + @Test + public void testNoValues() { + Stats s = new Stats(); + assertTrue(Double.isNaN(s.var())); + assertTrue(Double.isNaN(s.stddev())); + assertTrue(Double.isNaN(s.avg())); + assertTrue(Double.isNaN(s.min())); + assertTrue(Double.isNaN(s.max())); + s.add(42.3); + assertTrue(Double.isNaN(s.var())); + assertTrue(Double.isNaN(s.stddev())); + assertEquals(42.3, s.avg(), 1E-6); + assertEquals(42.3, s.max(), 1E-6); + assertEquals(42.3, s.min(), 1E-6); + s.add(42.3); + assertEquals(0, s.var(), 1E-6); + assertEquals(0, s.stddev(), 1E-6); + } +} diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index 4131901d7..2c9b3fe42 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -8,6 +8,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -22,6 +42,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -92,6 +140,28 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -101,17 +171,73 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index b03ff4ac9..66701b0b3 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -86,7 +86,7 @@ Export-Package: org.eclipse.jgit.annotations;version="5.2.3", org.eclipse.jgit.pgm", org.eclipse.jgit.internal.storage.reftree;version="5.2.3";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", org.eclipse.jgit.internal.submodule;version="5.2.3";x-internal:=true, - org.eclipse.jgit.internal.transport.parser;version="5.2.3";x-friends:="org.eclipse.jgit.test", + org.eclipse.jgit.internal.transport.parser;version="5.2.3";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.server", org.eclipse.jgit.internal.transport.ssh;version="5.2.3";x-friends:="org.eclipse.jgit.ssh.apache", org.eclipse.jgit.lib;version="5.2.3"; uses:="org.eclipse.jgit.revwalk, diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml index c74e26f6d..c30d648c6 100644 --- a/org.eclipse.jgit/pom.xml +++ b/org.eclipse.jgit/pom.xml @@ -209,6 +209,7 @@ com.github.spotbugs spotbugs-maven-plugin + ${spotbugs-maven-plugin-version} findBugs/FindBugsExcludeFilter.xml diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 12fded836..35f3dabe5 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -104,6 +104,7 @@ cannotReadObjectsPath=Cannot read {0}/{1}: {2} cannotReadTree=Cannot read tree {0} cannotRebaseWithoutCurrentHead=Can not rebase without a current HEAD cannotResolveLocalTrackingRefForUpdating=Cannot resolve local tracking ref {0} for updating. +cannotSaveConfig=Cannot save config file ''{0}'' cannotSquashFixupWithoutPreviousCommit=Cannot {0} without previous commit. cannotStoreObjects=cannot store objects cannotResolveUniquelyAbbrevObjectId=Could not resolve uniquely the abbreviated object ID @@ -389,6 +390,7 @@ invalidPathContainsSeparator=Invalid path (contains separator ''{0}''): {1} invalidPathPeriodAtEndWindows=Invalid path (period at end is ignored by Windows): {0} invalidPathSpaceAtEndWindows=Invalid path (space at end is ignored by Windows): {0} invalidPathReservedOnWindows=Invalid path (''{0}'' is reserved on Windows): {1} +invalidPurgeFactor=Invalid purgeFactor {0}, values have to be in range between 0 and 1 invalidRedirectLocation=Invalid redirect location {0} -> {1} invalidRefAdvertisementLine=Invalid ref advertisement line: ''{1}'' invalidReflogRevision=Invalid reflog revision: {0} @@ -558,8 +560,10 @@ pushIsNotSupportedForBundleTransport=Push is not supported for bundle transport pushNotPermitted=push not permitted pushOptionsNotSupported=Push options not supported; received {0} rawLogMessageDoesNotParseAsLogEntry=Raw log message does not parse as log entry +readConfigFailed=Reading config file ''{0}'' failed readerIsRequired=Reader is required readingObjectsFromLocalRepositoryFailed=reading objects from local repository failed: {0} +readLastModifiedFailed=Reading lastModified of {0} failed readTimedOut=Read timed out after {0} ms receivePackObjectTooLarge1=Object too large, rejecting the pack. Max object size limit is {0} bytes. receivePackObjectTooLarge2=Object too large ({0} bytes), rejecting the pack. Max object size limit is {1} bytes. @@ -680,6 +684,7 @@ theFactoryMustNotBeNull=The factory must not be null threadInterruptedWhileRunning="Current thread interrupted while running {0}" timeIsUncertain=Time is uncertain timerAlreadyTerminated=Timer already terminated +timeoutMeasureFsTimestampResolution=measuring filesystem timestamp resolution for ''{0}'' timed out, fall back to resolution of 2 seconds tooManyCommands=Too many commands tooManyFilters=Too many "filter" lines in request tooManyIncludeRecursions=Too many recursions; circular includes in config file(s)? diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java index f0408ab3d..a2cd4aeb2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java @@ -50,6 +50,7 @@ import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; import java.io.IOException; import java.io.InputStream; +import java.time.Instant; import java.util.Collection; import java.util.LinkedList; @@ -228,7 +229,7 @@ public class AddCommand extends GitCommand { if (GITLINK != mode) { entry.setLength(f.getEntryLength()); - entry.setLastModified(f.getEntryLastModified()); + entry.setLastModified(f.getEntryLastModifiedInstant()); long len = f.getEntryContentLength(); // We read and filter the content multiple times. // f.getEntryContentLength() reads and filters the input and @@ -241,7 +242,7 @@ public class AddCommand extends GitCommand { } } else { entry.setLength(0); - entry.setLastModified(0); + entry.setLastModified(Instant.ofEpochSecond(0)); entry.setObjectId(f.getEntryObjectId()); } builder.add(entry); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java index 27bb5a90b..3f7306bf3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java @@ -56,13 +56,18 @@ import java.util.concurrent.ConcurrentMap; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; @@ -375,13 +380,15 @@ public class ArchiveCommand extends GitCommand { MutableObjectId idBuf = new MutableObjectId(); ObjectReader reader = walk.getObjectReader(); - walk.reset(rw.parseTree(tree)); - if (!paths.isEmpty()) + RevObject o = rw.peel(rw.parseAny(tree)); + walk.reset(getTree(o)); + if (!paths.isEmpty()) { walk.setFilter(PathFilterGroup.createFromStrings(paths)); + } // Put base directory into archive if (pfx.endsWith("/")) { //$NON-NLS-1$ - fmt.putEntry(outa, tree, pfx.replaceAll("[/]+$", "/"), //$NON-NLS-1$ //$NON-NLS-2$ + fmt.putEntry(outa, o, pfx.replaceAll("[/]+$", "/"), //$NON-NLS-1$ //$NON-NLS-2$ FileMode.TREE, null); } @@ -392,17 +399,18 @@ public class ArchiveCommand extends GitCommand { if (walk.isSubtree()) walk.enterSubtree(); - if (mode == FileMode.GITLINK) + if (mode == FileMode.GITLINK) { // TODO(jrn): Take a callback to recurse // into submodules. mode = FileMode.TREE; + } if (mode == FileMode.TREE) { - fmt.putEntry(outa, tree, name + "/", mode, null); //$NON-NLS-1$ + fmt.putEntry(outa, o, name + "/", mode, null); //$NON-NLS-1$ continue; } walk.getObjectId(idBuf, 0); - fmt.putEntry(outa, tree, name, mode, reader.open(idBuf)); + fmt.putEntry(outa, o, name, mode, reader.open(idBuf)); } return out; } finally { @@ -534,4 +542,19 @@ public class ArchiveCommand extends GitCommand { this.paths = Arrays.asList(paths); return this; } + + private RevTree getTree(RevObject o) + throws IncorrectObjectTypeException { + final RevTree t; + if (o instanceof RevCommit) { + t = ((RevCommit) o).getTree(); + } else if (!(o instanceof RevTree)) { + throw new IncorrectObjectTypeException(tree.toObjectId(), + Constants.TYPE_TREE); + } else { + t = (RevTree) o; + } + return t; + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index d07532c09..cea28fac1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -392,7 +392,7 @@ public class CommitCommand extends GitCommand { final DirCacheEntry dcEntry = new DirCacheEntry(path); long entryLength = fTree.getEntryLength(); dcEntry.setLength(entryLength); - dcEntry.setLastModified(fTree.getEntryLastModified()); + dcEntry.setLastModified(fTree.getEntryLastModifiedInstant()); dcEntry.setFileMode(fTree.getIndexFileMode(dcTree)); boolean objectExists = (dcTree != null diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java index 13ce4e769..d7c9ad5e0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java @@ -422,7 +422,7 @@ public class ResetCommand extends GitCommand { DirCacheIterator.class); if (dcIter != null && dcIter.idEqual(cIter)) { DirCacheEntry indexEntry = dcIter.getDirCacheEntry(); - entry.setLastModified(indexEntry.getLastModified()); + entry.setLastModified(indexEntry.getLastModifiedInstant()); entry.setLength(indexEntry.getLength()); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java index 01d070cbd..ff7c4c64b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java @@ -332,7 +332,7 @@ public class StashApplyCommand extends GitCommand { DirCacheIterator.class); if (dcIter != null && dcIter.idEqual(cIter)) { DirCacheEntry indexEntry = dcIter.getDirCacheEntry(); - entry.setLastModified(indexEntry.getLastModified()); + entry.setLastModified(indexEntry.getLastModifiedInstant()); entry.setLength(indexEntry.getLength()); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java index c32890d8a..0d010adb2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java @@ -300,7 +300,8 @@ public class StashCreateCommand extends GitCommand { final DirCacheEntry entry = new DirCacheEntry( treeWalk.getRawPath()); entry.setLength(wtIter.getEntryLength()); - entry.setLastModified(wtIter.getEntryLastModified()); + entry.setLastModified( + wtIter.getEntryLastModifiedInstant()); entry.setFileMode(wtIter.getEntryFileMode()); long contentLength = wtIter.getEntryContentLength(); try (InputStream in = wtIter.openEntryStream()) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java index 14653fe17..0cfd16b58 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java @@ -58,6 +58,7 @@ import java.io.OutputStream; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.text.MessageFormat; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -497,8 +498,7 @@ public class DirCache { throw new CorruptObjectException(JGitText.get().DIRCHasTooManyEntries); snapshot = FileSnapshot.save(liveFile); - int smudge_s = (int) (snapshot.lastModified() / 1000); - int smudge_ns = ((int) (snapshot.lastModified() % 1000)) * 1000000; + Instant smudge = snapshot.lastModifiedInstant(); // Load the individual file entries. // @@ -507,8 +507,9 @@ public class DirCache { sortedEntries = new DirCacheEntry[entryCnt]; final MutableInteger infoAt = new MutableInteger(); - for (int i = 0; i < entryCnt; i++) - sortedEntries[i] = new DirCacheEntry(infos, infoAt, in, md, smudge_s, smudge_ns); + for (int i = 0; i < entryCnt; i++) { + sortedEntries[i] = new DirCacheEntry(infos, infoAt, in, md, smudge); + } // After the file entries are index extensions, and then a footer. // @@ -679,8 +680,8 @@ public class DirCache { // so we use the current timestamp as a approximation. myLock.createCommitSnapshot(); snapshot = myLock.getCommitSnapshot(); - smudge_s = (int) (snapshot.lastModified() / 1000); - smudge_ns = ((int) (snapshot.lastModified() % 1000)) * 1000000; + smudge_s = (int) (snapshot.lastModifiedInstant().getEpochSecond()); + smudge_ns = snapshot.lastModifiedInstant().getNano(); } else { // Used in unit tests only smudge_ns = 0; @@ -1025,7 +1026,7 @@ public class DirCache { DirCacheEntry entry = iIter.getDirCacheEntry(); if (entry.isSmudged() && iIter.idEqual(fIter)) { entry.setLength(fIter.getEntryLength()); - entry.setLastModified(fIter.getEntryLastModified()); + entry.setLastModified(fIter.getEntryLastModifiedInstant()); } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java index db6073f89..8aa97df77 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -50,6 +50,7 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.file.StandardCopyOption; import java.text.MessageFormat; +import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -425,8 +426,10 @@ public class DirCacheCheckout { // update the timestamp of the index with the one from the // file if not set, as we are sure to be in sync here. DirCacheEntry entry = i.getDirCacheEntry(); - if (entry.getLastModified() == 0) - entry.setLastModified(f.getEntryLastModified()); + Instant mtime = entry.getLastModifiedInstant(); + if (mtime == null || mtime.equals(Instant.EPOCH)) { + entry.setLastModified(f.getEntryLastModifiedInstant()); + } keep(entry); } } else @@ -638,7 +641,7 @@ public class DirCacheCheckout { File gitlinkDir = new File(repo.getWorkTree(), path); FileUtils.mkdirs(gitlinkDir, true); FS fs = repo.getFS(); - entry.setLastModified(fs.lastModified(gitlinkDir)); + entry.setLastModified(fs.lastModifiedInstant(gitlinkDir)); } private static ArrayList filterOut(ArrayList strings, @@ -1460,7 +1463,7 @@ public class DirCacheCheckout { } fs.createSymLink(f, target); entry.setLength(bytes.length); - entry.setLastModified(fs.lastModified(f)); + entry.setLastModified(fs.lastModifiedInstant(f)); return; } @@ -1529,7 +1532,7 @@ public class DirCacheCheckout { FileUtils.delete(tmpFile); } } - entry.setLastModified(fs.lastModified(f)); + entry.setLastModified(fs.lastModifiedInstant(f)); } // Run an external filter command diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java index 6b1d4f4d8..d2a59c131 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java @@ -56,6 +56,7 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.text.MessageFormat; +import java.time.Instant; import java.util.Arrays; import org.eclipse.jgit.errors.CorruptObjectException; @@ -145,8 +146,8 @@ public class DirCacheEntry { private byte inCoreFlags; DirCacheEntry(final byte[] sharedInfo, final MutableInteger infoAt, - final InputStream in, final MessageDigest md, final int smudge_s, - final int smudge_ns) throws IOException { + final InputStream in, final MessageDigest md, final Instant smudge) + throws IOException { info = sharedInfo; infoOffset = infoAt.value; @@ -215,8 +216,9 @@ public class DirCacheEntry { md.update(nullpad, 0, padLen); } - if (mightBeRacilyClean(smudge_s, smudge_ns)) + if (mightBeRacilyClean(smudge)) { smudgeRacilyClean(); + } } /** @@ -344,8 +346,29 @@ public class DirCacheEntry { * @param smudge_ns * nanoseconds component of the index's last modified time. * @return true if extra careful checks should be used. + * @deprecated use {@link #mightBeRacilyClean(Instant)} instead */ + @Deprecated public final boolean mightBeRacilyClean(int smudge_s, int smudge_ns) { + return mightBeRacilyClean(Instant.ofEpochSecond(smudge_s, smudge_ns)); + } + + /** + * Is it possible for this entry to be accidentally assumed clean? + *

+ * The "racy git" problem happens when a work file can be updated faster + * than the filesystem records file modification timestamps. It is possible + * for an application to edit a work file, update the index, then edit it + * again before the filesystem will give the work file a new modification + * timestamp. This method tests to see if file was written out at the same + * time as the index. + * + * @param smudge + * index's last modified time. + * @return true if extra careful checks should be used. + * @since 5.1.9 + */ + public final boolean mightBeRacilyClean(Instant smudge) { // If the index has a modification time then it came from disk // and was not generated from scratch in memory. In such cases // the entry is 'racily clean' if the entry's cached modification @@ -355,8 +378,9 @@ public class DirCacheEntry { // final int base = infoOffset + P_MTIME; final int mtime = NB.decodeInt32(info, base); - if (smudge_s == mtime) - return smudge_ns <= NB.decodeInt32(info, base + 4); + if (smudge.getEpochSecond() == mtime) { + return smudge.getNano() <= NB.decodeInt32(info, base + 4); + } return false; } @@ -563,21 +587,50 @@ public class DirCacheEntry { * * @return last modification time of this file, in milliseconds since the * Java epoch (midnight Jan 1, 1970 UTC). + * @deprecated use {@link #getLastModifiedInstant()} instead */ + @Deprecated public long getLastModified() { return decodeTS(P_MTIME); } + /** + * Get the cached last modification date of this file. + *

+ * One of the indicators that the file has been modified by an application + * changing the working tree is if the last modification time for the file + * differs from the time stored in this entry. + * + * @return last modification time of this file. + * @since 5.1.9 + */ + public Instant getLastModifiedInstant() { + return decodeTSInstant(P_MTIME); + } + /** * Set the cached last modification date of this file, using milliseconds. * * @param when * new cached modification date of the file, in milliseconds. + * @deprecated use {@link #setLastModified(Instant)} instead */ + @Deprecated public void setLastModified(long when) { encodeTS(P_MTIME, when); } + /** + * Set the cached last modification date of this file. + * + * @param when + * new cached modification date of the file. + * @since 5.1.9 + */ + public void setLastModified(Instant when) { + encodeTS(P_MTIME, when); + } + /** * Get the cached size (mod 4 GB) (in bytes) of this file. *

@@ -692,7 +745,8 @@ public class DirCacheEntry { @SuppressWarnings("nls") @Override public String toString() { - return getFileMode() + " " + getLength() + " " + getLastModified() + return getFileMode() + " " + getLength() + " " + + getLastModifiedInstant() + " " + getObjectId() + " " + getStage() + " " + getPathString() + "\n"; } @@ -750,12 +804,25 @@ public class DirCacheEntry { return 1000L * sec + ms; } + private Instant decodeTSInstant(int pIdx) { + final int base = infoOffset + pIdx; + final int sec = NB.decodeInt32(info, base); + final int nano = NB.decodeInt32(info, base + 4); + return Instant.ofEpochSecond(sec, nano); + } + private void encodeTS(int pIdx, long when) { final int base = infoOffset + pIdx; NB.encodeInt32(info, base, (int) (when / 1000)); NB.encodeInt32(info, base + 4, ((int) (when % 1000)) * 1000000); } + private void encodeTS(int pIdx, Instant when) { + final int base = infoOffset + pIdx; + NB.encodeInt32(info, base, (int) when.getEpochSecond()); + NB.encodeInt32(info, base + 4, when.getNano()); + } + private int getExtendedFlags() { if (isExtended()) return NB.decodeUInt16(info, infoOffset + P_FLAGS2) << 16; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 8f884b44f..4914927ad 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -165,6 +165,7 @@ public class JGitText extends TranslationBundle { /***/ public String cannotReadTree; /***/ public String cannotRebaseWithoutCurrentHead; /***/ public String cannotResolveLocalTrackingRefForUpdating; + /***/ public String cannotSaveConfig; /***/ public String cannotSquashFixupWithoutPreviousCommit; /***/ public String cannotStoreObjects; /***/ public String cannotResolveUniquelyAbbrevObjectId; @@ -450,6 +451,7 @@ public class JGitText extends TranslationBundle { /***/ public String invalidPathPeriodAtEndWindows; /***/ public String invalidPathSpaceAtEndWindows; /***/ public String invalidPathReservedOnWindows; + /***/ public String invalidPurgeFactor; /***/ public String invalidRedirectLocation; /***/ public String invalidRefAdvertisementLine; /***/ public String invalidReflogRevision; @@ -619,8 +621,10 @@ public class JGitText extends TranslationBundle { /***/ public String pushNotPermitted; /***/ public String pushOptionsNotSupported; /***/ public String rawLogMessageDoesNotParseAsLogEntry; + /***/ public String readConfigFailed; /***/ public String readerIsRequired; /***/ public String readingObjectsFromLocalRepositoryFailed; + /***/ public String readLastModifiedFailed; /***/ public String readTimedOut; /***/ public String receivePackObjectTooLarge1; /***/ public String receivePackObjectTooLarge2; @@ -736,6 +740,7 @@ public class JGitText extends TranslationBundle { /***/ public String tagAlreadyExists; /***/ public String tagNameInvalid; /***/ public String tagOnRepoWithoutHEADCurrentlyNotSupported; + /***/ public String timeoutMeasureFsTimestampResolution; /***/ public String transactionAborted; /***/ public String theFactoryMustNotBeNull; /***/ public String threadInterruptedWhileRunning; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java index 1de313500..976f946e5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java @@ -43,19 +43,24 @@ package org.eclipse.jgit.internal.storage.file; +import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_FILESTORE_ATTRIBUTES; +import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_TIMESTAMP_RESOLUTION; import java.io.File; import java.io.IOException; import java.nio.file.attribute.BasicFileAttributes; -import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.time.Duration; -import java.util.Date; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.Objects; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.FileStoreAttributes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Caches when a file was last read, making it possible to detect future edits. @@ -74,6 +79,8 @@ import org.eclipse.jgit.util.FS; * file is less than 3 seconds ago. */ public class FileSnapshot { + private static final Logger LOG = LoggerFactory + .getLogger(FileSnapshot.class); /** * An unknown file size. * @@ -81,8 +88,14 @@ public class FileSnapshot { */ public static final long UNKNOWN_SIZE = -1; + private static final Instant UNKNOWN_TIME = Instant.ofEpochMilli(-1); + private static final Object MISSING_FILEKEY = new Object(); + private static final DateTimeFormatter dateFmt = DateTimeFormatter + .ofPattern("yyyy-MM-dd HH:mm:ss.nnnnnnnnn") //$NON-NLS-1$ + .withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault()); + /** * A FileSnapshot that is considered to always be modified. *

@@ -90,8 +103,8 @@ public class FileSnapshot { * file, but only after {@link #isModified(File)} gets invoked. The returned * snapshot contains only invalid status information. */ - public static final FileSnapshot DIRTY = new FileSnapshot(-1, -1, - UNKNOWN_SIZE, Duration.ZERO, MISSING_FILEKEY); + public static final FileSnapshot DIRTY = new FileSnapshot(UNKNOWN_TIME, + UNKNOWN_TIME, UNKNOWN_SIZE, Duration.ZERO, MISSING_FILEKEY); /** * A FileSnapshot that is clean if the file does not exist. @@ -100,8 +113,8 @@ public class FileSnapshot { * file to be clean. {@link #isModified(File)} will return false if the file * path does not exist. */ - public static final FileSnapshot MISSING_FILE = new FileSnapshot(0, 0, 0, - Duration.ZERO, MISSING_FILEKEY) { + public static final FileSnapshot MISSING_FILE = new FileSnapshot( + Instant.EPOCH, Instant.EPOCH, 0, Duration.ZERO, MISSING_FILEKEY) { @Override public boolean isModified(File path) { return FS.DETECTED.exists(path); @@ -122,6 +135,22 @@ public class FileSnapshot { return new FileSnapshot(path); } + /** + * Record a snapshot for a specific file path without using config file to + * get filesystem timestamp resolution. + *

+ * This method should be invoked before the file is accessed. It is used by + * FileBasedConfig to avoid endless recursion. + * + * @param path + * the path to later remember. The path's current status + * information is saved. + * @return the snapshot. + */ + public static FileSnapshot saveNoConfig(File path) { + return new FileSnapshot(path, false); + } + private static Object getFileKey(BasicFileAttributes fileAttributes) { Object fileKey = fileAttributes.fileKey(); return fileKey == null ? MISSING_FILEKEY : fileKey; @@ -141,18 +170,41 @@ public class FileSnapshot { * @param modified * the last modification time of the file * @return the snapshot. + * @deprecated use {@link #save(Instant)} instead. */ + @Deprecated public static FileSnapshot save(long modified) { - final long read = System.currentTimeMillis(); - return new FileSnapshot(read, modified, -1, Duration.ZERO, - MISSING_FILEKEY); + final Instant read = Instant.now(); + return new FileSnapshot(read, Instant.ofEpochMilli(modified), + UNKNOWN_SIZE, FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY); + } + + /** + * Record a snapshot for a file for which the last modification time is + * already known. + *

+ * This method should be invoked before the file is accessed. + *

+ * Note that this method cannot rely on measuring file timestamp resolution + * to avoid racy git issues caused by finite file timestamp resolution since + * it's unknown in which filesystem the file is located. Hence the worst + * case fallback for timestamp resolution is used. + * + * @param modified + * the last modification time of the file + * @return the snapshot. + */ + public static FileSnapshot save(Instant modified) { + final Instant read = Instant.now(); + return new FileSnapshot(read, modified, UNKNOWN_SIZE, + FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY); } /** Last observed modification time of the path. */ - private final long lastModified; + private final Instant lastModified; /** Last wall-clock time the path was read. */ - private volatile long lastRead; + private volatile Instant lastRead; /** True once {@link #lastRead} is far later than {@link #lastModified}. */ private boolean cannotBeRacilyClean; @@ -162,8 +214,8 @@ public class FileSnapshot { * When set to {@link #UNKNOWN_SIZE} the size is not considered for modification checks. */ private final long size; - /** measured filesystem timestamp resolution */ - private Duration fsTimestampResolution; + /** measured FileStore attributes */ + private FileStoreAttributes fileStoreAttributeCache; /** * Object that uniquely identifies the given file, or {@code @@ -171,31 +223,57 @@ public class FileSnapshot { */ private final Object fileKey; + private final File file; + /** * Record a snapshot for a specific file path. *

* This method should be invoked before the file is accessed. * - * @param path - * the path to later remember. The path's current status + * @param file + * the path to remember meta data for. The path's current status * information is saved. */ - protected FileSnapshot(File path) { - this.lastRead = System.currentTimeMillis(); - this.fsTimestampResolution = FS - .getFsTimerResolution(path.toPath().getParent()); + protected FileSnapshot(File file) { + this(file, true); + } + + /** + * Record a snapshot for a specific file path. + *

+ * This method should be invoked before the file is accessed. + * + * @param file + * the path to remember meta data for. The path's current status + * information is saved. + * @param useConfig + * if {@code true} read filesystem time resolution from + * configuration file otherwise use fallback resolution + */ + protected FileSnapshot(File file, boolean useConfig) { + this.file = file; + this.lastRead = Instant.now(); + this.fileStoreAttributeCache = useConfig + ? FS.getFileStoreAttributes(file.toPath().getParent()) + : FALLBACK_FILESTORE_ATTRIBUTES; BasicFileAttributes fileAttributes = null; try { - fileAttributes = FS.DETECTED.fileAttributes(path); + fileAttributes = FS.DETECTED.fileAttributes(file); } catch (IOException e) { - this.lastModified = path.lastModified(); - this.size = path.length(); + this.lastModified = Instant.ofEpochMilli(file.lastModified()); + this.size = file.length(); this.fileKey = MISSING_FILEKEY; return; } - this.lastModified = fileAttributes.lastModifiedTime().toMillis(); + this.lastModified = fileAttributes.lastModifiedTime().toInstant(); this.size = fileAttributes.size(); this.fileKey = getFileKey(fileAttributes); + if (LOG.isDebugEnabled()) { + LOG.debug("file={}, create new FileSnapshot: lastRead={}, lastModified={}, size={}, fileKey={}", //$NON-NLS-1$ + file, dateFmt.format(lastRead), + dateFmt.format(lastModified), Long.valueOf(size), + fileKey.toString()); + } } private boolean sizeChanged; @@ -206,11 +284,17 @@ public class FileSnapshot { private boolean wasRacyClean; - private FileSnapshot(long read, long modified, long size, + private long delta; + + private long racyThreshold; + + private FileSnapshot(Instant read, Instant modified, long size, @NonNull Duration fsTimestampResolution, @NonNull Object fileKey) { + this.file = null; this.lastRead = read; this.lastModified = modified; - this.fsTimestampResolution = fsTimestampResolution; + this.fileStoreAttributeCache = new FileStoreAttributes( + fsTimestampResolution); this.size = size; this.fileKey = fileKey; } @@ -219,8 +303,19 @@ public class FileSnapshot { * Get time of last snapshot update * * @return time of last snapshot update + * @deprecated use {@link #lastModifiedInstant()} instead */ + @Deprecated public long lastModified() { + return lastModified.toEpochMilli(); + } + + /** + * Get time of last snapshot update + * + * @return time of last snapshot update + */ + public Instant lastModifiedInstant() { return lastModified; } @@ -239,16 +334,16 @@ public class FileSnapshot { * @return true if the path needs to be read again. */ public boolean isModified(File path) { - long currLastModified; + Instant currLastModified; long currSize; Object currFileKey; try { BasicFileAttributes fileAttributes = FS.DETECTED.fileAttributes(path); - currLastModified = fileAttributes.lastModifiedTime().toMillis(); + currLastModified = fileAttributes.lastModifiedTime().toInstant(); currSize = fileAttributes.size(); currFileKey = getFileKey(fileAttributes); } catch (IOException e) { - currLastModified = path.lastModified(); + currLastModified = Instant.ofEpochMilli(path.lastModified()); currSize = path.length(); currFileKey = MISSING_FILEKEY; } @@ -290,7 +385,7 @@ public class FileSnapshot { * the other snapshot. */ public void setClean(FileSnapshot other) { - final long now = other.lastRead; + final Instant now = other.lastRead; if (!isRacyClean(now)) { cannotBeRacilyClean = true; } @@ -304,9 +399,10 @@ public class FileSnapshot { * if sleep was interrupted */ public void waitUntilNotRacy() throws InterruptedException { - while (isRacyClean(System.currentTimeMillis())) { - TimeUnit.NANOSECONDS - .sleep((fsTimestampResolution.toNanos() + 1) * 11 / 10); + long timestampResolution = fileStoreAttributeCache + .getFsTimestampResolution().toNanos(); + while (isRacyClean(Instant.now())) { + TimeUnit.NANOSECONDS.sleep(timestampResolution); } } @@ -318,7 +414,8 @@ public class FileSnapshot { * @return true if the two snapshots share the same information. */ public boolean equals(FileSnapshot other) { - return lastModified == other.lastModified && size == other.size + boolean sizeEq = size == UNKNOWN_SIZE || other.size == UNKNOWN_SIZE || size == other.size; + return lastModified.equals(other.lastModified) && sizeEq && Objects.equals(fileKey, other.fileKey); } @@ -341,8 +438,7 @@ public class FileSnapshot { /** {@inheritDoc} */ @Override public int hashCode() { - return Objects.hash(Long.valueOf(lastModified), Long.valueOf(size), - fileKey); + return Objects.hash(lastModified, Long.valueOf(size), fileKey); } /** @@ -377,6 +473,22 @@ public class FileSnapshot { return wasRacyClean; } + /** + * @return the delta in nanoseconds between lastModified and lastRead during + * last racy check + */ + public long lastDelta() { + return delta; + } + + /** + * @return the racyLimitNanos threshold in nanoseconds during last racy + * check + */ + public long lastRacyThreshold() { + return racyThreshold; + } + /** {@inheritDoc} */ @SuppressWarnings("nls") @Override @@ -387,24 +499,46 @@ public class FileSnapshot { if (this == MISSING_FILE) { return "MISSING_FILE"; } - DateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", - Locale.US); - return "FileSnapshot[modified: " + f.format(new Date(lastModified)) - + ", read: " + f.format(new Date(lastRead)) + ", size:" + size + return "FileSnapshot[modified: " + dateFmt.format(lastModified) + + ", read: " + dateFmt.format(lastRead) + ", size:" + size + ", fileKey: " + fileKey + "]"; } - private boolean isRacyClean(long read) { - // add a 10% safety margin - long racyNanos = (fsTimestampResolution.toNanos() + 1) * 11 / 10; - return wasRacyClean = (read - lastModified) * 1_000_000 <= racyNanos; + private boolean isRacyClean(Instant read) { + racyThreshold = getEffectiveRacyThreshold(); + delta = Duration.between(lastModified, read).toNanos(); + wasRacyClean = delta <= racyThreshold; + if (LOG.isDebugEnabled()) { + LOG.debug( + "file={}, isRacyClean={}, read={}, lastModified={}, delta={} ns, racy<={} ns", //$NON-NLS-1$ + file, Boolean.valueOf(wasRacyClean), dateFmt.format(read), + dateFmt.format(lastModified), Long.valueOf(delta), + Long.valueOf(racyThreshold)); + } + return wasRacyClean; + } + + private long getEffectiveRacyThreshold() { + long timestampResolution = fileStoreAttributeCache + .getFsTimestampResolution().toNanos(); + long minRacyInterval = fileStoreAttributeCache.getMinimalRacyInterval() + .toNanos(); + long max = Math.max(timestampResolution, minRacyInterval); + // safety margin: factor 2.5 below 100ms otherwise 1.25 + return max < 100_000_000L ? max * 5 / 2 : max * 5 / 4; } - private boolean isModified(long currLastModified) { + private boolean isModified(Instant currLastModified) { // Any difference indicates the path was modified. - lastModifiedChanged = lastModified != currLastModified; + lastModifiedChanged = !lastModified.equals(currLastModified); if (lastModifiedChanged) { + if (LOG.isDebugEnabled()) { + LOG.debug( + "file={}, lastModified changed from {} to {}", //$NON-NLS-1$ + file, dateFmt.format(lastModified), + dateFmt.format(currLastModified)); + } return true; } @@ -412,26 +546,40 @@ public class FileSnapshot { // after the last modification that any new modifications // are certain to change the last modified time. if (cannotBeRacilyClean) { + LOG.debug("file={}, cannot be racily clean", file); //$NON-NLS-1$ return false; } if (!isRacyClean(lastRead)) { // Our last read should have marked cannotBeRacilyClean, // but this thread may not have seen the change. The read // of the volatile field lastRead should have fixed that. + LOG.debug("file={}, is unmodified", file); //$NON-NLS-1$ return false; } // We last read this path too close to its last observed // modification time. We may have missed a modification. // Scan again, to ensure we still see the same state. + LOG.debug("file={}, is racily clean", file); //$NON-NLS-1$ return true; } private boolean isFileKeyChanged(Object currFileKey) { - return currFileKey != MISSING_FILEKEY && !currFileKey.equals(fileKey); + boolean changed = currFileKey != MISSING_FILEKEY + && !currFileKey.equals(fileKey); + if (changed) { + LOG.debug("file={}, FileKey changed from {} to {}", //$NON-NLS-1$ + file, fileKey, currFileKey); + } + return changed; } private boolean isSizeChanged(long currSize) { - return currSize != UNKNOWN_SIZE && currSize != size; + boolean changed = (currSize != UNKNOWN_SIZE) && (currSize != size); + if (changed) { + LOG.debug("file={}, size changed from {} to {} bytes", //$NON-NLS-1$ + file, Long.valueOf(size), Long.valueOf(currSize)); + } + return changed; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java index 3c830e88c..791a10828 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java @@ -372,8 +372,9 @@ public class GC { continue oldPackLoop; if (!oldPack.shouldBeKept() - && repo.getFS().lastModified( - oldPack.getPackFile()) < packExpireDate) { + && repo.getFS() + .lastModifiedInstant(oldPack.getPackFile()) + .toEpochMilli() < packExpireDate) { oldPack.close(); if (shouldLoosen) { loosen(inserter, reader, oldPack, ids); @@ -561,8 +562,10 @@ public class GC { String fName = f.getName(); if (fName.length() != Constants.OBJECT_ID_STRING_LENGTH - 2) continue; - if (repo.getFS().lastModified(f) >= expireDate) + if (repo.getFS().lastModifiedInstant(f) + .toEpochMilli() >= expireDate) { continue; + } try { ObjectId id = ObjectId.fromString(d + fName); if (objectsToKeep.contains(id)) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java index b80c58ca9..420e73754 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java @@ -56,8 +56,12 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; +import java.nio.file.Files; import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; import java.text.MessageFormat; +import java.time.Instant; +import java.util.concurrent.TimeUnit; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; @@ -422,9 +426,16 @@ public class LockFile { public void waitForStatChange() throws InterruptedException { FileSnapshot o = FileSnapshot.save(ref); FileSnapshot n = FileSnapshot.save(lck); + long fsTimeResolution = FS.getFileStoreAttributes(lck.toPath()) + .getFsTimestampResolution().toNanos(); while (o.equals(n)) { - Thread.sleep(25 /* milliseconds */); - lck.setLastModified(System.currentTimeMillis()); + TimeUnit.NANOSECONDS.sleep(fsTimeResolution); + try { + Files.setLastModifiedTime(lck.toPath(), + FileTime.from(Instant.now())); + } catch (IOException e) { + n.waitUntilNotRacy(); + } n = FileSnapshot.save(lck); } } @@ -474,11 +485,22 @@ public class LockFile { * Get the modification time of the output file when it was committed. * * @return modification time of the lock file right before we committed it. + * @deprecated use {@link #getCommitLastModifiedInstant()} instead */ + @Deprecated public long getCommitLastModified() { return commitSnapshot.lastModified(); } + /** + * Get the modification time of the output file when it was committed. + * + * @return modification time of the lock file right before we committed it. + */ + public Instant getCommitLastModifiedInstant() { + return commitSnapshot.lastModifiedInstant(); + } + /** * Get the {@link FileSnapshot} just before commit. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java index 73ad38c95..88e05af41 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java @@ -60,6 +60,7 @@ import java.nio.channels.FileChannel.MapMode; import java.nio.file.AccessDeniedException; import java.nio.file.NoSuchFileException; import java.text.MessageFormat; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; @@ -107,7 +108,7 @@ public class PackFile implements Iterable { public static final Comparator SORT = new Comparator() { @Override public int compare(PackFile a, PackFile b) { - return b.packLastModified - a.packLastModified; + return b.packLastModified.compareTo(a.packLastModified); } }; @@ -132,7 +133,7 @@ public class PackFile implements Iterable { private int activeCopyRawData; - int packLastModified; + Instant packLastModified; private PackFileSnapshot fileSnapshot; @@ -172,7 +173,7 @@ public class PackFile implements Iterable { public PackFile(File packFile, int extensions) { this.packFile = packFile; this.fileSnapshot = PackFileSnapshot.save(packFile); - this.packLastModified = (int) (fileSnapshot.lastModified() >> 10); + this.packLastModified = fileSnapshot.lastModifiedInstant(); this.extensions = extensions; // Multiply by 31 here so we can more directly combine with another diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java index e8a6ba730..c1e94a0a3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java @@ -50,6 +50,7 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -65,6 +66,7 @@ import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.errors.InvalidPatternException; import org.eclipse.jgit.fnmatch.FileNameMatcher; import org.eclipse.jgit.transport.SshConstants; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.SystemReader; @@ -129,7 +131,7 @@ public class OpenSshConfigFile { private final String localUserName; /** Modification time of {@link #configFile} when it was last loaded. */ - private long lastModified; + private Instant lastModified; /** * Encapsulates entries read out of the configuration file, and a cache of @@ -223,8 +225,8 @@ public class OpenSshConfigFile { } private synchronized State refresh() { - final long mtime = configFile.lastModified(); - if (mtime != lastModified) { + final Instant mtime = FS.DETECTED.lastModifiedInstant(configFile); + if (!mtime.equals(lastModified)) { State newState = new State(); try (BufferedReader br = Files .newBufferedReader(configFile.toPath(), UTF_8)) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index 9763fb72f..5ae9d41db 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -476,4 +476,23 @@ public final class ConfigConstants { * @since 5.2 */ public static final String CONFIG_KEY_LOG_OUTPUT_ENCODING = "logOutputEncoding"; + + /** + * The "filesystem" section + * @since 5.1.9 + */ + public static final String CONFIG_FILESYSTEM_SECTION = "filesystem"; + + /** + * The "timestampResolution" key + * @since 5.1.9 + */ + public static final String CONFIG_KEY_TIMESTAMP_RESOLUTION = "timestampResolution"; + + /** + * The "minRacyThreshold" key + * + * @since 5.1.9 + */ + public static final String CONFIG_KEY_MIN_RACY_THRESHOLD = "minRacyThreshold"; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java index 6a66cf682..fb239399e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java @@ -226,6 +226,14 @@ public class DefaultTypedConfigGetter implements TypedConfigGetter { inputUnit = wantUnit; inputMul = 1; + } else if (match(unitName, "ns", "nanoseconds")) { //$NON-NLS-1$ //$NON-NLS-2$ + inputUnit = TimeUnit.NANOSECONDS; + inputMul = 1; + + } else if (match(unitName, "us", "microseconds")) { //$NON-NLS-1$ //$NON-NLS-2$ + inputUnit = TimeUnit.MICROSECONDS; + inputMul = 1; + } else if (match(unitName, "ms", "milliseconds")) { //$NON-NLS-1$ //$NON-NLS-2$ inputUnit = TimeUnit.MILLISECONDS; inputMul = 1; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java index 412d9bba7..909f3b15d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -47,6 +47,7 @@ package org.eclipse.jgit.merge; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.time.Instant.EPOCH; import static org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm.HISTOGRAM; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_ALGORITHM; @@ -59,6 +60,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -464,7 +466,7 @@ public class ResolveMerger extends ThreeWayMerger { * @return the entry which was added to the index */ private DirCacheEntry add(byte[] path, CanonicalTreeParser p, int stage, - long lastMod, long len) { + Instant lastMod, long len) { if (p != null && !p.getEntryFileMode().equals(FileMode.TREE)) { DirCacheEntry e = new DirCacheEntry(path, stage); e.setFileMode(p.getEntryFileMode()); @@ -491,7 +493,7 @@ public class ResolveMerger extends ThreeWayMerger { e.getStage()); newEntry.setFileMode(e.getFileMode()); newEntry.setObjectId(e.getObjectId()); - newEntry.setLastModified(e.getLastModified()); + newEntry.setLastModified(e.getLastModifiedInstant()); newEntry.setLength(e.getLength()); builder.add(newEntry); return newEntry; @@ -667,16 +669,17 @@ public class ResolveMerger extends ThreeWayMerger { // we know about length and lastMod only after we have written the new content. // This will happen later. Set these values to 0 for know. DirCacheEntry e = add(tw.getRawPath(), theirs, - DirCacheEntry.STAGE_0, 0, 0); + DirCacheEntry.STAGE_0, EPOCH, 0); addToCheckout(tw.getPathString(), e, attributes); } return true; } else { // FileModes are not mergeable. We found a conflict on modes. // For conflicting entries we don't know lastModified and length. - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); - add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0); - add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0); + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); + add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, + 0); unmergedPaths.add(tw.getPathString()); mergeResults.put( tw.getPathString(), @@ -708,7 +711,7 @@ public class ResolveMerger extends ThreeWayMerger { // the new content. // This will happen later. Set these values to 0 for know. DirCacheEntry e = add(tw.getRawPath(), theirs, - DirCacheEntry.STAGE_0, 0, 0); + DirCacheEntry.STAGE_0, EPOCH, 0); if (e != null) { addToCheckout(tw.getPathString(), e, attributes); } @@ -737,16 +740,16 @@ public class ResolveMerger extends ThreeWayMerger { // detected later if (nonTree(modeO) && !nonTree(modeT)) { if (nonTree(modeB)) - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); - add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0); + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); unmergedPaths.add(tw.getPathString()); enterSubtree = false; return true; } if (nonTree(modeT) && !nonTree(modeO)) { if (nonTree(modeB)) - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); - add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0); + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); + add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); unmergedPaths.add(tw.getPathString()); enterSubtree = false; return true; @@ -773,9 +776,9 @@ public class ResolveMerger extends ThreeWayMerger { boolean gitlinkConflict = isGitLink(modeO) || isGitLink(modeT); // Don't attempt to resolve submodule link conflicts if (gitlinkConflict || !attributes.canBeContentMerged()) { - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); - add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0); - add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0); + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); + add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); if (gitlinkConflict) { MergeResult result = new MergeResult<>( @@ -822,10 +825,10 @@ public class ResolveMerger extends ThreeWayMerger { MergeResult result = contentMerge(base, ours, theirs, attributes); - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); - add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0); + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); DirCacheEntry e = add(tw.getRawPath(), theirs, - DirCacheEntry.STAGE_3, 0, 0); + DirCacheEntry.STAGE_3, EPOCH, 0); // OURS was deleted checkout THEIRS if (modeO == 0) { @@ -957,9 +960,9 @@ public class ResolveMerger extends ThreeWayMerger { // A conflict occurred, the file will contain conflict markers // the index will be populated with the three stages and the // workdir (if used) contains the halfway merged content. - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); - add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0); - add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0); + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); + add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); mergeResults.put(tw.getPathString(), result); return; } @@ -976,7 +979,7 @@ public class ResolveMerger extends ThreeWayMerger { ? FileMode.REGULAR_FILE : FileMode.fromBits(newMode)); if (mergedFile != null) { dce.setLastModified( - nonNullRepo().getFS().lastModified(mergedFile)); + nonNullRepo().getFS().lastModifiedInstant(mergedFile)); dce.setLength((int) mergedFile.length()); } dce.setObjectId(insertMergeResult(rawMerged, attributes)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java index 2b31ebd8e..84cd6adb8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java @@ -148,11 +148,37 @@ public class FileBasedConfig extends StoredConfig { */ @Override public void load() throws IOException, ConfigInvalidException { + load(true); + } + + /** + * Load the configuration as a Git text style configuration file. + *

+ * If the file does not exist, this configuration is cleared, and thus + * behaves the same as though the file exists, but is empty. + * + * @param useFileSnapshotWithConfig + * if {@code true} use the FileSnapshot with config, otherwise + * use it without config + * @throws IOException + * if IO failed + * @throws ConfigInvalidException + * if config is invalid + * @since 5.1.9 + */ + public void load(boolean useFileSnapshotWithConfig) + throws IOException, ConfigInvalidException { final int maxStaleRetries = 5; int retries = 0; while (true) { final FileSnapshot oldSnapshot = snapshot; - final FileSnapshot newSnapshot = FileSnapshot.save(getFile()); + final FileSnapshot newSnapshot; + if (useFileSnapshotWithConfig) { + newSnapshot = FileSnapshot.save(getFile()); + } else { + // don't use config in this snapshot to avoid endless recursion + newSnapshot = FileSnapshot.saveNoConfig(getFile()); + } try { final byte[] in = IO.readFully(getFile()); final ObjectId newHash = hash(in); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java index 8562376aa..7dd019ba2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java @@ -49,6 +49,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.time.Instant; import java.util.Collection; import java.util.HashMap; import java.util.Locale; @@ -126,7 +127,7 @@ public class NetRC { private File netrc; - private long lastModified; + private Instant lastModified; private Map hosts = new HashMap<>(); @@ -190,8 +191,10 @@ public class NetRC { if (netrc == null) return null; - if (this.lastModified != this.netrc.lastModified()) + if (!this.lastModified + .equals(FS.DETECTED.lastModifiedInstant(this.netrc))) { parse(); + } NetRCEntry entry = this.hosts.get(host); @@ -212,7 +215,7 @@ public class NetRC { private void parse() { this.hosts.clear(); - this.lastModified = this.netrc.lastModified(); + this.lastModified = FS.DETECTED.lastModifiedInstant(this.netrc); try (BufferedReader r = new BufferedReader( new InputStreamReader(new FileInputStream(netrc), UTF_8))) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java index 3d25c2314..d432c9445 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java @@ -53,6 +53,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.time.Instant; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -406,8 +407,14 @@ public class FileTreeIterator extends WorkingTreeIterator { } @Override + @Deprecated public long getLastModified() { - return attributes.getLastModifiedTime(); + return attributes.getLastModifiedInstant().toEpochMilli(); + } + + @Override + public Instant getLastModifiedInstant() { + return attributes.getLastModifiedInstant(); } @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java index 1fa1db584..ddf916f41 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -59,6 +59,7 @@ import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetEncoder; import java.text.MessageFormat; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; @@ -645,11 +646,23 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { * * @return last modified time of this file, in milliseconds since the epoch * (Jan 1, 1970 UTC). + * @deprecated use {@link #getEntryLastModifiedInstant()} instead */ + @Deprecated public long getEntryLastModified() { return current().getLastModified(); } + /** + * Get the last modified time of this entry. + * + * @return last modified time of this file + * @since 5.1.9 + */ + public Instant getEntryLastModifiedInstant() { + return current().getLastModifiedInstant(); + } + /** * Obtain an input stream to read the file content. *

@@ -924,30 +937,28 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { // Git under windows only stores seconds so we round the timestamp // Java gives us if it looks like the timestamp in index is seconds - // only. Otherwise we compare the timestamp at millisecond precision, + // only. Otherwise we compare the timestamp at nanosecond precision, // unless core.checkstat is set to "minimal", in which case we only // compare the whole second part. - long cacheLastModified = entry.getLastModified(); - long fileLastModified = getEntryLastModified(); - long lastModifiedMillis = fileLastModified % 1000; - long cacheMillis = cacheLastModified % 1000; - if (getOptions().getCheckStat() == CheckStat.MINIMAL) { - fileLastModified = fileLastModified - lastModifiedMillis; - cacheLastModified = cacheLastModified - cacheMillis; - } else if (cacheMillis == 0) - fileLastModified = fileLastModified - lastModifiedMillis; - // Some Java version on Linux return whole seconds only even when - // the file systems supports more precision. - else if (lastModifiedMillis == 0) - cacheLastModified = cacheLastModified - cacheMillis; - - if (fileLastModified != cacheLastModified) + Instant cacheLastModified = entry.getLastModifiedInstant(); + Instant fileLastModified = getEntryLastModifiedInstant(); + if ((getOptions().getCheckStat() == CheckStat.MINIMAL) + || (cacheLastModified.getNano() == 0) + // Some Java version on Linux return whole seconds only even + // when the file systems supports more precision. + || (fileLastModified.getNano() == 0)) { + if (fileLastModified.getEpochSecond() != cacheLastModified + .getEpochSecond()) { + return MetadataDiff.DIFFER_BY_TIMESTAMP; + } + } + if (!fileLastModified.equals(cacheLastModified)) { return MetadataDiff.DIFFER_BY_TIMESTAMP; - else if (!entry.isSmudged()) - // The file is clean when you look at timestamps. - return MetadataDiff.EQUAL; - else + } else if (entry.isSmudged()) { return MetadataDiff.SMUDGED; + } + // The file is clean when when comparing timestamps + return MetadataDiff.EQUAL; } /** @@ -1274,9 +1285,25 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { * instance member instead. * * @return time since the epoch (in ms) of the last change. + * @deprecated use {@link #getLastModifiedInstant()} instead */ + @Deprecated public abstract long getLastModified(); + /** + * Get the last modified time of this entry. + *

+ * Note: Efficient implementation required. + *

+ * The implementation of this method must be efficient. If a subclass + * needs to compute the value they should cache the reference within an + * instance member instead. + * + * @return time of the last change. + * @since 5.1.9 + */ + public abstract Instant getLastModifiedInstant(); + /** * Get the name of this entry within its directory. *

diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java index fb958872a..10a991939 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java @@ -44,6 +44,7 @@ package org.eclipse.jgit.util; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.time.Instant.EPOCH; import java.io.BufferedReader; import java.io.ByteArrayInputStream; @@ -53,7 +54,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.io.PrintStream; +import java.io.Writer; import java.nio.charset.Charset; import java.nio.file.AccessDeniedException; import java.nio.file.FileStore; @@ -65,27 +68,39 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.text.MessageFormat; import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.errors.CommandFailedException; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.LockFailedException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.file.FileSnapshot; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry; import org.eclipse.jgit.treewalk.FileTreeIterator.FileModeStrategy; import org.eclipse.jgit.treewalk.WorkingTreeIterator.Entry; @@ -188,81 +203,478 @@ public abstract class FS { } } - private static final class FileStoreAttributeCache { + /** + * Attributes of FileStores on this system + * + * @since 5.1.9 + */ + public final static class FileStoreAttributes { + + private static final Duration UNDEFINED_DURATION = Duration + .ofNanos(Long.MAX_VALUE); + /** - * The last modified time granularity of FAT filesystems is 2 seconds. + * Fallback filesystem timestamp resolution. The worst case timestamp + * resolution on FAT filesystems is 2 seconds. */ - private static final Duration FALLBACK_TIMESTAMP_RESOLUTION = Duration + public static final Duration FALLBACK_TIMESTAMP_RESOLUTION = Duration .ofMillis(2000); - private static final Map attributeCache = new ConcurrentHashMap<>(); + /** + * Fallback FileStore attributes used when we can't measure the + * filesystem timestamp resolution. The last modified time granularity + * of FAT filesystems is 2 seconds. + */ + public static final FileStoreAttributes FALLBACK_FILESTORE_ATTRIBUTES = new FileStoreAttributes( + FALLBACK_TIMESTAMP_RESOLUTION); + + private static final Map attributeCache = new ConcurrentHashMap<>(); + + private static final SimpleLruCache attrCacheByPath = new SimpleLruCache<>( + 100, 0.2f); + + private static AtomicBoolean background = new AtomicBoolean(); + + private static Map locks = new ConcurrentHashMap<>(); + + private static void setBackground(boolean async) { + background.set(async); + } + + private static final String javaVersionPrefix = System + .getProperty("java.vendor") + '|' //$NON-NLS-1$ + + System.getProperty("java.version") + '|'; //$NON-NLS-1$ + + private static final Duration FALLBACK_MIN_RACY_INTERVAL = Duration + .ofMillis(10); + + /** + * Configures size and purge factor of the path-based cache for file + * system attributes. Caching of file system attributes avoids recurring + * lookup of @{code FileStore} of files which may be expensive on some + * platforms. + * + * @param maxSize + * maximum size of the cache, default is 100 + * @param purgeFactor + * when the size of the map reaches maxSize the oldest + * entries will be purged to free up some space for new + * entries, {@code purgeFactor} is the fraction of + * {@code maxSize} to purge when this happens + * @since 5.1.9 + */ + public static void configureAttributesPathCache(int maxSize, + float purgeFactor) { + FileStoreAttributes.attrCacheByPath.configure(maxSize, purgeFactor); + } + + /** + * Get the FileStoreAttributes for the given FileStore + * + * @param path + * file residing in the FileStore to get attributes for + * @return FileStoreAttributes for the given path. + */ + public static FileStoreAttributes get(Path path) { + path = path.toAbsolutePath(); + Path dir = Files.isDirectory(path) ? path : path.getParent(); + FileStoreAttributes cached = attrCacheByPath.get(dir); + if (cached != null) { + return cached; + } + FileStoreAttributes attrs = getFileStoreAttributes(dir); + attrCacheByPath.put(dir, attrs); + return attrs; + } - static Duration getFsTimestampResolution(Path file) { + private static FileStoreAttributes getFileStoreAttributes(Path dir) { + FileStore s; try { - Path dir = Files.isDirectory(file) ? file : file.getParent(); - if (!dir.toFile().canWrite()) { - // can not determine FileStore of an unborn directory or in - // a read-only directory - return FALLBACK_TIMESTAMP_RESOLUTION; - } - FileStore s = Files.getFileStore(dir); - FileStoreAttributeCache c = attributeCache.get(s); - if (c == null) { - c = new FileStoreAttributeCache(dir); - attributeCache.put(s, c); - if (LOG.isDebugEnabled()) { - LOG.debug(c.toString()); + if (Files.exists(dir)) { + s = Files.getFileStore(dir); + FileStoreAttributes c = attributeCache.get(s); + if (c != null) { + return c; + } + if (!Files.isWritable(dir)) { + // cannot measure resolution in a read-only directory + LOG.debug( + "{}: cannot measure timestamp resolution in read-only directory {}", //$NON-NLS-1$ + Thread.currentThread(), dir); + return FALLBACK_FILESTORE_ATTRIBUTES; } + } else { + // cannot determine FileStore of an unborn directory + LOG.debug( + "{}: cannot measure timestamp resolution of unborn directory {}", //$NON-NLS-1$ + Thread.currentThread(), dir); + return FALLBACK_FILESTORE_ATTRIBUTES; } - return c.getFsTimestampResolution(); - } catch (IOException | InterruptedException e) { - LOG.warn(e.getMessage(), e); - return FALLBACK_TIMESTAMP_RESOLUTION; + CompletableFuture> f = CompletableFuture + .supplyAsync(() -> { + Lock lock = locks.computeIfAbsent(s, + l -> new ReentrantLock()); + if (!lock.tryLock()) { + LOG.debug( + "{}: couldn't get lock to measure timestamp resolution in {}", //$NON-NLS-1$ + Thread.currentThread(), dir); + return Optional.empty(); + } + Optional attributes = Optional + .empty(); + try { + // Some earlier future might have set the value + // and removed itself since we checked for the + // value above. Hence check cache again. + FileStoreAttributes c = attributeCache + .get(s); + if (c != null) { + return Optional.of(c); + } + attributes = readFromConfig(s); + if (attributes.isPresent()) { + attributeCache.put(s, attributes.get()); + return attributes; + } + + Optional resolution = measureFsTimestampResolution( + s, dir); + if (resolution.isPresent()) { + c = new FileStoreAttributes( + resolution.get()); + attributeCache.put(s, c); + // for high timestamp resolution measure + // minimal racy interval + if (c.fsTimestampResolution + .toNanos() < 100_000_000L) { + c.minimalRacyInterval = measureMinimalRacyInterval( + dir); + } + if (LOG.isDebugEnabled()) { + LOG.debug(c.toString()); + } + saveToConfig(s, c); + } + attributes = Optional.of(c); + } finally { + lock.unlock(); + locks.remove(s); + } + return attributes; + }); + f.exceptionally(e -> { + LOG.error(e.getLocalizedMessage(), e); + return Optional.empty(); + }); + // even if measuring in background wait a little - if the result + // arrives, it's better than returning the large fallback + Optional d = background.get() ? f.get( + 100, TimeUnit.MILLISECONDS) : f.get(); + if (d.isPresent()) { + return d.get(); + } + // return fallback until measurement is finished + } catch (IOException | InterruptedException + | ExecutionException | CancellationException e) { + LOG.error(e.getMessage(), e); + } catch (TimeoutException | SecurityException e) { + // use fallback } + LOG.debug("{}: use fallback timestamp resolution for directory {}", //$NON-NLS-1$ + Thread.currentThread(), dir); + return FALLBACK_FILESTORE_ATTRIBUTES; } - private Duration fsTimestampResolution; + @SuppressWarnings("boxing") + private static Duration measureMinimalRacyInterval(Path dir) { + LOG.debug("{}: start measure minimal racy interval in {}", //$NON-NLS-1$ + Thread.currentThread(), dir); + int n = 0; + int failures = 0; + long racyNanos = 0; + ArrayList deltas = new ArrayList<>(); + Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$ + Instant end = Instant.now().plusSeconds(3); + try { + Files.createFile(probe); + do { + n++; + write(probe, "a"); //$NON-NLS-1$ + FileSnapshot snapshot = FileSnapshot.save(probe.toFile()); + read(probe); + write(probe, "b"); //$NON-NLS-1$ + if (!snapshot.isModified(probe.toFile())) { + deltas.add(Long.valueOf(snapshot.lastDelta())); + racyNanos = snapshot.lastRacyThreshold(); + failures++; + } + } while (Instant.now().compareTo(end) < 0); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + return FALLBACK_MIN_RACY_INTERVAL; + } finally { + deleteProbe(probe); + } + if (failures > 0) { + Stats stats = new Stats(); + for (Long d : deltas) { + stats.add(d); + } + LOG.debug( + "delta [ns] since modification FileSnapshot failed to detect\n" //$NON-NLS-1$ + + "count, failures, racy limit [ns], delta min [ns]," //$NON-NLS-1$ + + " delta max [ns], delta avg [ns]," //$NON-NLS-1$ + + " delta stddev [ns]\n" //$NON-NLS-1$ + + "{}, {}, {}, {}, {}, {}, {}", //$NON-NLS-1$ + n, failures, racyNanos, stats.min(), stats.max(), + stats.avg(), stats.stddev()); + return Duration + .ofNanos(Double.valueOf(stats.max()).longValue()); + } + // since no failures occurred using the measured filesystem + // timestamp resolution there is no need for minimal racy interval + LOG.debug("{}: no failures when measuring minimal racy interval", //$NON-NLS-1$ + Thread.currentThread()); + return Duration.ZERO; + } - Duration getFsTimestampResolution() { - return fsTimestampResolution; + private static void write(Path p, String body) throws IOException { + FileUtils.mkdirs(p.getParent().toFile(), true); + try (Writer w = new OutputStreamWriter(Files.newOutputStream(p), + UTF_8)) { + w.write(body); + } + } + + private static String read(Path p) throws IOException { + final byte[] body = IO.readFully(p.toFile()); + return new String(body, 0, body.length, UTF_8); } - private FileStoreAttributeCache(Path dir) - throws IOException, InterruptedException { + private static Optional measureFsTimestampResolution( + FileStore s, Path dir) { + LOG.debug("{}: start measure timestamp resolution {} in {}", //$NON-NLS-1$ + Thread.currentThread(), s, dir); Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$ - Files.createFile(probe); try { - FileTime startTime = Files.getLastModifiedTime(probe); - FileTime actTime = startTime; - long sleepTime = 512; - while (actTime.compareTo(startTime) <= 0) { - TimeUnit.NANOSECONDS.sleep(sleepTime); - FileUtils.touch(probe); - actTime = Files.getLastModifiedTime(probe); - // limit sleep time to max. 100ms - if (sleepTime < 100_000_000L) { - sleepTime = sleepTime * 2; - } + Files.createFile(probe); + FileTime t1 = Files.getLastModifiedTime(probe); + FileTime t2 = t1; + Instant t1i = t1.toInstant(); + for (long i = 1; t2.compareTo(t1) <= 0; i += 1 + i / 20) { + Files.setLastModifiedTime(probe, + FileTime.from(t1i.plusNanos(i * 1000))); + t2 = Files.getLastModifiedTime(probe); } - fsTimestampResolution = Duration.between(startTime.toInstant(), - actTime.toInstant()); + Duration fsResolution = Duration.between(t1.toInstant(), t2.toInstant()); + Duration clockResolution = measureClockResolution(); + fsResolution = fsResolution.plus(clockResolution); + LOG.debug("{}: end measure timestamp resolution {} in {}", //$NON-NLS-1$ + Thread.currentThread(), s, dir); + return Optional.of(fsResolution); } catch (AccessDeniedException e) { + LOG.warn(e.getLocalizedMessage(), e); // see bug 548648 + } catch (IOException e) { LOG.error(e.getLocalizedMessage(), e); } finally { - Files.delete(probe); + deleteProbe(probe); } + return Optional.empty(); } - @SuppressWarnings("nls") + private static Duration measureClockResolution() { + Duration clockResolution = Duration.ZERO; + for (int i = 0; i < 10; i++) { + Instant t1 = Instant.now(); + Instant t2 = t1; + while (t2.compareTo(t1) <= 0) { + t2 = Instant.now(); + } + Duration r = Duration.between(t1, t2); + if (r.compareTo(clockResolution) > 0) { + clockResolution = r; + } + } + return clockResolution; + } + + private static void deleteProbe(Path probe) { + try { + FileUtils.delete(probe.toFile(), + FileUtils.SKIP_MISSING | FileUtils.RETRY); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + } + + private static Optional readFromConfig( + FileStore s) { + FileBasedConfig userConfig = SystemReader.getInstance() + .openUserConfig(null, FS.DETECTED); + try { + userConfig.load(false); + } catch (IOException e) { + LOG.error(MessageFormat.format(JGitText.get().readConfigFailed, + userConfig.getFile().getAbsolutePath()), e); + } catch (ConfigInvalidException e) { + LOG.error(MessageFormat.format( + JGitText.get().repositoryConfigFileInvalid, + userConfig.getFile().getAbsolutePath(), + e.getMessage())); + } + String key = getConfigKey(s); + Duration resolution = Duration.ofNanos(userConfig.getTimeUnit( + ConfigConstants.CONFIG_FILESYSTEM_SECTION, key, + ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION, + UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS)); + if (UNDEFINED_DURATION.equals(resolution)) { + return Optional.empty(); + } + Duration minRacyThreshold = Duration.ofNanos(userConfig.getTimeUnit( + ConfigConstants.CONFIG_FILESYSTEM_SECTION, key, + ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD, + UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS)); + FileStoreAttributes c = new FileStoreAttributes(resolution); + if (!UNDEFINED_DURATION.equals(minRacyThreshold)) { + c.minimalRacyInterval = minRacyThreshold; + } + return Optional.of(c); + } + + private static void saveToConfig(FileStore s, + FileStoreAttributes c) { + FileBasedConfig userConfig = SystemReader.getInstance() + .openUserConfig(null, FS.DETECTED); + long resolution = c.getFsTimestampResolution().toNanos(); + TimeUnit resolutionUnit = getUnit(resolution); + long resolutionValue = resolutionUnit.convert(resolution, + TimeUnit.NANOSECONDS); + + long minRacyThreshold = c.getMinimalRacyInterval().toNanos(); + TimeUnit minRacyThresholdUnit = getUnit(minRacyThreshold); + long minRacyThresholdValue = minRacyThresholdUnit + .convert(minRacyThreshold, TimeUnit.NANOSECONDS); + + final int max_retries = 5; + int retries = 0; + boolean succeeded = false; + String key = getConfigKey(s); + while (!succeeded && retries < max_retries) { + try { + userConfig.load(false); + userConfig.setString( + ConfigConstants.CONFIG_FILESYSTEM_SECTION, key, + ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION, + String.format("%d %s", //$NON-NLS-1$ + Long.valueOf(resolutionValue), + resolutionUnit.name().toLowerCase())); + userConfig.setString( + ConfigConstants.CONFIG_FILESYSTEM_SECTION, key, + ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD, + String.format("%d %s", //$NON-NLS-1$ + Long.valueOf(minRacyThresholdValue), + minRacyThresholdUnit.name().toLowerCase())); + userConfig.save(); + succeeded = true; + } catch (LockFailedException e) { + // race with another thread, wait a bit and try again + try { + LOG.warn(MessageFormat.format(JGitText.get().cannotLock, + userConfig.getFile().getAbsolutePath())); + retries++; + Thread.sleep(20); + } catch (InterruptedException e1) { + Thread.interrupted(); + } + } catch (IOException e) { + LOG.error(MessageFormat.format( + JGitText.get().cannotSaveConfig, + userConfig.getFile().getAbsolutePath()), e); + } catch (ConfigInvalidException e) { + LOG.error(MessageFormat.format( + JGitText.get().repositoryConfigFileInvalid, + userConfig.getFile().getAbsolutePath(), + e.getMessage())); + } + } + } + + private static String getConfigKey(FileStore s) { + final String storeKey; + if (SystemReader.getInstance().isWindows()) { + Object attribute = null; + try { + attribute = s.getAttribute("volume:vsn"); //$NON-NLS-1$ + } catch (IOException ignored) { + // ignore + } + if (attribute instanceof Integer) { + storeKey = attribute.toString(); + } else { + storeKey = s.name(); + } + } else { + storeKey = s.name(); + } + return javaVersionPrefix + storeKey; + } + + private static TimeUnit getUnit(long nanos) { + TimeUnit unit; + if (nanos < 200_000L) { + unit = TimeUnit.NANOSECONDS; + } else if (nanos < 200_000_000L) { + unit = TimeUnit.MICROSECONDS; + } else { + unit = TimeUnit.MILLISECONDS; + } + return unit; + } + + private final @NonNull Duration fsTimestampResolution; + + private Duration minimalRacyInterval; + + /** + * @return the measured minimal interval after a file has been modified + * in which we cannot rely on lastModified to detect + * modifications + */ + public Duration getMinimalRacyInterval() { + return minimalRacyInterval; + } + + /** + * @return the measured filesystem timestamp resolution + */ + @NonNull + public Duration getFsTimestampResolution() { + return fsTimestampResolution; + } + + /** + * Construct a FileStoreAttributeCache entry for the given filesystem + * timestamp resolution + * + * @param fsTimestampResolution + */ + public FileStoreAttributes( + @NonNull Duration fsTimestampResolution) { + this.fsTimestampResolution = fsTimestampResolution; + this.minimalRacyInterval = Duration.ZERO; + } + + @SuppressWarnings({ "nls", "boxing" }) @Override public String toString() { - return "FileStoreAttributeCache[" + attributeCache.keySet() - .stream() - .map(key -> "FileStore[" + key + "]: fsTimestampResolution=" - + attributeCache.get(key).getFsTimestampResolution()) - .collect(Collectors.joining(",\n")) + "]"; + return String.format( + "FileStoreAttributes[fsTimestampResolution=%,d µs, " + + "minimalRacyInterval=%,d µs]", + fsTimestampResolution.toNanos() / 1000, + minimalRacyInterval.toNanos() / 1000); } + } /** The auto-detected implementation selected for this operating system and JRE. */ @@ -279,6 +691,19 @@ public abstract class FS { return detect(null); } + /** + * Whether FileStore attributes should be determined asynchronously + * + * @param asynch + * whether FileStore attributes should be determined + * asynchronously. If false access to cached attributes may block + * for some seconds for the first call per FileStore + * @since 5.1.9 + */ + public static void setAsyncFileStoreAttributes(boolean asynch) { + FileStoreAttributes.setBackground(asynch); + } + /** * Auto-detect the appropriate file system abstraction, taking into account * the presence of a Cygwin installation on the system. Using jgit in @@ -307,18 +732,18 @@ public abstract class FS { } /** - * Get an estimate for the filesystem timestamp resolution from a cache of - * timestamp resolution per FileStore, if not yet available it is measured - * for a probe file under the given directory. + * Get cached FileStore attributes, if not yet available measure them using + * a probe file under the given directory. * * @param dir * the directory under which the probe file will be created to * measure the timer resolution. * @return measured filesystem timestamp resolution - * @since 5.2.3 + * @since 5.1.9 */ - public static Duration getFsTimerResolution(@NonNull Path dir) { - return FileStoreAttributeCache.getFsTimestampResolution(dir); + public static FileStoreAttributes getFileStoreAttributes( + @NonNull Path dir) { + return FileStoreAttributes.get(dir); } private volatile Holder userHome; @@ -432,11 +857,41 @@ public abstract class FS { * @return last modified time of f * @throws java.io.IOException * @since 3.0 + * @deprecated use {@link #lastModifiedInstant(Path)} instead */ + @Deprecated public long lastModified(File f) throws IOException { return FileUtils.lastModified(f); } + /** + * Get the last modified time of a file system object. If the OS/JRE support + * symbolic links, the modification time of the link is returned, rather + * than that of the link target. + * + * @param p + * a {@link Path} object. + * @return last modified time of p + * @since 5.1.9 + */ + public Instant lastModifiedInstant(Path p) { + return FileUtils.lastModifiedInstant(p); + } + + /** + * Get the last modified time of a file system object. If the OS/JRE support + * symbolic links, the modification time of the link is returned, rather + * than that of the link target. + * + * @param f + * a {@link File} object. + * @return last modified time of p + * @since 5.1.9 + */ + public Instant lastModifiedInstant(File f) { + return FileUtils.lastModifiedInstant(f.toPath()); + } + /** * Set the last modified time of a file system object. If the OS/JRE support * symbolic links, the link is modified, not the target, @@ -447,11 +902,28 @@ public abstract class FS { * last modified time * @throws java.io.IOException * @since 3.0 + * @deprecated use {@link #setLastModified(Path, Instant)} instead */ + @Deprecated public void setLastModified(File f, long time) throws IOException { FileUtils.setLastModified(f, time); } + /** + * Set the last modified time of a file system object. If the OS/JRE support + * symbolic links, the link is modified, not the target, + * + * @param p + * a {@link Path} object. + * @param time + * last modified time + * @throws java.io.IOException + * @since 5.1.9 + */ + public void setLastModified(Path p, Instant time) throws IOException { + FileUtils.setLastModified(p, time); + } + /** * Get the length of a file or link, If the OS/JRE supports symbolic links * it's the length of the link, else the length of the target. @@ -1526,9 +1998,19 @@ public abstract class FS { /** * @return the time (milliseconds since 1970-01-01) when this object was * last modified + * @deprecated use getLastModifiedInstant instead */ + @Deprecated public long getLastModifiedTime() { - return lastModifiedTime; + return lastModifiedInstant.toEpochMilli(); + } + + /** + * @return the time when this object was last modified + * @since 5.1.9 + */ + public Instant getLastModifiedInstant() { + return lastModifiedInstant; } private final boolean isDirectory; @@ -1539,7 +2021,7 @@ public abstract class FS { private final long creationTime; - private final long lastModifiedTime; + private final Instant lastModifiedInstant; private final boolean isExecutable; @@ -1557,7 +2039,7 @@ public abstract class FS { Attributes(FS fs, File file, boolean exists, boolean isDirectory, boolean isExecutable, boolean isSymbolicLink, boolean isRegularFile, long creationTime, - long lastModifiedTime, long length) { + Instant lastModifiedInstant, long length) { this.fs = fs; this.file = file; this.exists = exists; @@ -1566,7 +2048,7 @@ public abstract class FS { this.isSymbolicLink = isSymbolicLink; this.isRegularFile = isRegularFile; this.creationTime = creationTime; - this.lastModifiedTime = lastModifiedTime; + this.lastModifiedInstant = lastModifiedInstant; this.length = length; } @@ -1578,7 +2060,7 @@ public abstract class FS { * @param path */ public Attributes(File path, FS fs) { - this(fs, path, false, false, false, false, false, 0L, 0L, 0L); + this(fs, path, false, false, false, false, false, 0L, EPOCH, 0L); } /** @@ -1624,7 +2106,7 @@ public abstract class FS { boolean exists = isDirectory || isFile; boolean canExecute = exists && !isDirectory && canExecute(path); boolean isSymlink = false; - long lastModified = exists ? path.lastModified() : 0L; + Instant lastModified = exists ? lastModifiedInstant(path) : EPOCH; long createTime = 0L; return new Attributes(this, path, exists, isDirectory, canExecute, isSymlink, isFile, createTime, lastModified, -1); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java index 98797dc64..7c0727036 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java @@ -149,7 +149,7 @@ public class FS_Win32 extends FS { attrs.isSymbolicLink(), attrs.isRegularFile(), attrs.creationTime().toMillis(), - attrs.lastModifiedTime().toMillis(), + attrs.lastModifiedTime().toInstant(), attrs.size()); result.add(new FileEntry(f, fs, attributes, fileModeStrategy)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java index 9bba6ca8a..80f188cb2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java @@ -49,7 +49,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.File; import java.io.IOException; -import java.io.OutputStream; +import java.nio.channels.FileChannel; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.CopyOption; import java.nio.file.Files; @@ -57,6 +57,7 @@ import java.nio.file.InvalidPathException; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; @@ -66,6 +67,7 @@ import java.nio.file.attribute.PosixFilePermission; import java.text.MessageFormat; import java.text.Normalizer; import java.text.Normalizer.Form; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -74,11 +76,14 @@ import java.util.regex.Pattern; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.util.FS.Attributes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * File Utilities */ public class FileUtils { + private static final Logger LOG = LoggerFactory.getLogger(FileUtils.class); /** * Option to delete given {@code File} @@ -654,12 +659,31 @@ public class FileUtils { * @return lastModified attribute for given file, not following symbolic * links * @throws IOException + * @deprecated use {@link #lastModifiedInstant(Path)} instead which returns + * FileTime */ + @Deprecated static long lastModified(File file) throws IOException { return Files.getLastModifiedTime(toPath(file), LinkOption.NOFOLLOW_LINKS) .toMillis(); } + /** + * @param path + * @return lastModified attribute for given file, not following symbolic + * links + */ + static Instant lastModifiedInstant(Path path) { + try { + return Files.getLastModifiedTime(path, LinkOption.NOFOLLOW_LINKS) + .toInstant(); + } catch (IOException e) { + LOG.error(MessageFormat + .format(JGitText.get().readLastModifiedFailed, path)); + return Instant.ofEpochMilli(path.toFile().lastModified()); + } + } + /** * Return all the attributes of a file, without following symbolic links. * @@ -678,10 +702,21 @@ public class FileUtils { * @param time * @throws IOException */ + @Deprecated static void setLastModified(File file, long time) throws IOException { Files.setLastModifiedTime(toPath(file), FileTime.fromMillis(time)); } + /** + * @param path + * @param time + * @throws IOException + */ + static void setLastModified(Path path, Instant time) + throws IOException { + Files.setLastModifiedTime(path, FileTime.from(time)); + } + /** * @param file * @return {@code true} if the given file exists, not following symbolic @@ -786,7 +821,7 @@ public class FileUtils { readAttributes.isSymbolicLink(), readAttributes.isRegularFile(), // readAttributes.creationTime().toMillis(), // - readAttributes.lastModifiedTime().toMillis(), + readAttributes.lastModifiedTime().toInstant(), readAttributes.isSymbolicLink() ? Constants .encode(readSymLink(file)).length : readAttributes.size()); @@ -825,7 +860,7 @@ public class FileUtils { readAttributes.isSymbolicLink(), readAttributes.isRegularFile(), // readAttributes.creationTime().toMillis(), // - readAttributes.lastModifiedTime().toMillis(), + readAttributes.lastModifiedTime().toInstant(), readAttributes.size()); return attributes; } catch (IOException e) { @@ -916,11 +951,13 @@ public class FileUtils { * @param f * the file to touch * @throws IOException - * @since 5.2.3 + * @since 5.1.8 */ public static void touch(Path f) throws IOException { - try (OutputStream fos = Files.newOutputStream(f)) { - // touch the file + try (FileChannel fc = FileChannel.open(f, StandardOpenOption.CREATE, + StandardOpenOption.APPEND, StandardOpenOption.SYNC)) { + // touch } + Files.setLastModifiedTime(f, FileTime.from(Instant.now())); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SimpleLruCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SimpleLruCache.java new file mode 100644 index 000000000..709d9ee73 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SimpleLruCache.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2019, Marc Strapetz + * Copyright (C) 2019, Matthias Sohn + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.util; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.internal.JGitText; + +/** + * Simple limited size cache based on ConcurrentHashMap purging entries in LRU + * order when reaching size limit + * + * @param + * the type of keys maintained by this cache + * @param + * the type of mapped values + * + * @since 5.1.9 + */ +public class SimpleLruCache { + + private static class Entry { + + private final K key; + + private final V value; + + // pseudo clock timestamp of the last access to this entry + private volatile long lastAccessed; + + private long lastAccessedSorting; + + Entry(K key, V value, long lastAccessed) { + this.key = key; + this.value = value; + this.lastAccessed = lastAccessed; + } + + void copyAccessTime() { + lastAccessedSorting = lastAccessed; + } + + @SuppressWarnings("nls") + @Override + public String toString() { + return "Entry [lastAccessed=" + lastAccessed + ", key=" + key + + ", value=" + value + "]"; + } + } + + private Lock lock = new ReentrantLock(); + + private Map> map = new ConcurrentHashMap<>(); + + private volatile int maximumSize; + + private int purgeSize; + + // pseudo clock to implement LRU order of access to entries + private volatile long time = 0L; + + private static void checkPurgeFactor(float purgeFactor) { + if (purgeFactor <= 0 || purgeFactor >= 1) { + throw new IllegalArgumentException( + MessageFormat.format(JGitText.get().invalidPurgeFactor, + Float.valueOf(purgeFactor))); + } + } + + private static int purgeSize(int maxSize, float purgeFactor) { + return (int) ((1 - purgeFactor) * maxSize); + } + + /** + * Create a new cache + * + * @param maxSize + * maximum size of the cache, to reduce need for synchronization + * this is not a hard limit. The real size of the cache could be + * slightly above this maximum if multiple threads put new values + * concurrently + * @param purgeFactor + * when the size of the map reaches maxSize the oldest entries + * will be purged to free up some space for new entries, + * {@code purgeFactor} is the fraction of {@code maxSize} to + * purge when this happens + */ + public SimpleLruCache(int maxSize, float purgeFactor) { + checkPurgeFactor(purgeFactor); + this.maximumSize = maxSize; + this.purgeSize = purgeSize(maxSize, purgeFactor); + } + + /** + * Returns the value to which the specified key is mapped, or {@code null} + * if this map contains no mapping for the key. + * + *

+ * More formally, if this cache contains a mapping from a key {@code k} to a + * value {@code v} such that {@code key.equals(k)}, then this method returns + * {@code v}; otherwise it returns {@code null}. (There can be at most one + * such mapping.) + * + * @param key + * the key + * + * @throws NullPointerException + * if the specified key is null + * + * @return value mapped for this key, or {@code null} if no value is mapped + */ + public V get(Object key) { + Entry entry = map.get(key); + if (entry != null) { + entry.lastAccessed = ++time; + return entry.value; + } + return null; + } + + /** + * Maps the specified key to the specified value in this cache. Neither the + * key nor the value can be null. + * + *

+ * The value can be retrieved by calling the {@code get} method with a key + * that is equal to the original key. + * + * @param key + * key with which the specified value is to be associated + * @param value + * value to be associated with the specified key + * @return the previous value associated with {@code key}, or {@code null} + * if there was no mapping for {@code key} + * @throws NullPointerException + * if the specified key or value is null + */ + public V put(@NonNull K key, @NonNull V value) { + map.put(key, new Entry<>(key, value, ++time)); + if (map.size() > maximumSize) { + purge(); + } + return value; + } + + /** + * Returns the current size of this cache + * + * @return the number of key-value mappings in this cache + */ + public int size() { + return map.size(); + } + + /** + * Reconfigures the cache. If {@code maxSize} is reduced some entries will + * be purged. + * + * @param maxSize + * maximum size of the cache + * + * @param purgeFactor + * when the size of the map reaches maxSize the oldest entries + * will be purged to free up some space for new entries, + * {@code purgeFactor} is the fraction of {@code maxSize} to + * purge when this happens + */ + public void configure(int maxSize, float purgeFactor) { + lock.lock(); + try { + checkPurgeFactor(purgeFactor); + this.maximumSize = maxSize; + this.purgeSize = purgeSize(maxSize, purgeFactor); + if (map.size() >= maximumSize) { + purge(); + } + } finally { + lock.unlock(); + } + } + + private void purge() { + // don't try to compete if another thread already has the lock + if (lock.tryLock()) { + try { + List entriesToPurge = new ArrayList<>(map.values()); + // copy access times to avoid other threads interfere with + // sorting + for (Entry e : entriesToPurge) { + e.copyAccessTime(); + } + Collections.sort(entriesToPurge, + Comparator.comparingLong(o -> -o.lastAccessedSorting)); + for (int index = purgeSize; index < entriesToPurge + .size(); index++) { + map.remove(entriesToPurge.get(index).key); + } + } finally { + lock.unlock(); + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java new file mode 100644 index 000000000..e9307d342 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2019, Matthias Sohn + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.util; + +/** + * Simple double statistics, computed incrementally, variance and standard + * deviation using Welford's online algorithm, see + * https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm + * + * @since 5.1.9 + */ +public class Stats { + private int n = 0; + + private double avg = 0.0; + + private double min = 0.0; + + private double max = 0.0; + + private double sum = 0.0; + + /** + * Add a value + * + * @param x + * value + */ + public void add(double x) { + n++; + min = n == 1 ? x : Math.min(min, x); + max = n == 1 ? x : Math.max(max, x); + double d = x - avg; + avg += d / n; + sum += d * d * (n - 1) / n; + } + + /** + * @return number of the added values + */ + public int count() { + return n; + } + + /** + * @return minimum of the added values + */ + public double min() { + if (n < 1) { + return Double.NaN; + } + return min; + } + + /** + * @return maximum of the added values + */ + public double max() { + if (n < 1) { + return Double.NaN; + } + return max; + } + + /** + * @return average of the added values + */ + + public double avg() { + if (n < 1) { + return Double.NaN; + } + return avg; + } + + /** + * @return variance of the added values + */ + public double var() { + if (n < 2) { + return Double.NaN; + } + return sum / (n - 1); + } + + /** + * @return standard deviation of the added values + */ + public double stddev() { + return Math.sqrt(this.var()); + } +} diff --git a/pom.xml b/pom.xml index 25fa24d61..a7d8c8014 100644 --- a/pom.xml +++ b/pom.xml @@ -194,19 +194,20 @@ 4.3.1 3.1.0 9.4.11.v20180605 - 0.13.0 + 0.14.1 4.5.5 4.4.9 1.7.2 1.2.15 - 3.0.1 - 1.2.0 + 3.1.0 + 1.3.0 2.8.2 - 3.1.8 - 2.22.1 - 3.8.0 3.0.0 3.0.0 + 3.1.12 + 3.0.0-M3 + ${maven-surefire-plugin-version} + 3.8.1 jacoco @@ -238,7 +239,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.1.0 + 3.1.2 @@ -281,7 +282,7 @@ org.apache.maven.plugins maven-source-plugin - 3.0.1 + 3.1.0 @@ -293,7 +294,7 @@ org.apache.maven.plugins maven-surefire-plugin - ${maven-surefire-version} + ${maven-surefire-plugin-version} ${test-fork-count} true @@ -326,7 +327,7 @@ org.apache.maven.plugins maven-pmd-plugin - 3.11.0 + 3.12.0 utf-8 100 @@ -364,7 +365,7 @@ org.jacoco jacoco-maven-plugin - 0.8.2 + 0.8.4 org.apache.maven.plugins @@ -381,7 +382,7 @@ org.apache.maven.plugins maven-surefire-report-plugin - ${maven-surefire-version} + ${maven-surefire-report-plugin-version} org.apache.maven.plugins @@ -393,6 +394,26 @@ maven-project-info-reports-plugin ${maven-project-info-reports-plugin-version} + + org.apache.maven.plugins + maven-deploy-plugin + 3.0.0-M1 + + + org.apache.maven.plugins + maven-install-plugin + 3.0.0-M1 + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin-version} + + + org.apache.maven.plugins + maven-resources-plugin + 3.1.0 + @@ -778,19 +799,19 @@ org.codehaus.plexus plexus-compiler-javac - 2.8.4 + 2.8.5 org.codehaus.plexus plexus-compiler-javac-errorprone - 2.8.4 + 2.8.5 com.google.errorprone error_prone_core - 2.3.2 + 2.3.3 @@ -823,12 +844,12 @@ org.codehaus.plexus plexus-compiler-eclipse - 2.8.4 + 2.8.5 org.eclipse.jdt ecj - 3.15.0 + 3.17.0