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/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java index 12b216950..50e6f2da5 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,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.Collections; import java.util.HashMap; @@ -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 + * 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.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/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index f469173aa..89ce34dbf 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -40,6 +40,7 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", org.eclipse.jgit.internal.storage.reftable;version="[5.1.9,5.2.0)", org.eclipse.jgit.internal.storage.reftree;version="[5.1.9,5.2.0)", org.eclipse.jgit.junit;version="[5.1.9,5.2.0)", + org.eclipse.jgit.junit.time;version="[5.1.9,5.2.0)", org.eclipse.jgit.lfs;version="[5.1.9,5.2.0)", org.eclipse.jgit.lib;version="[5.1.9,5.2.0)", org.eclipse.jgit.merge;version="[5.1.9,5.2.0)", @@ -62,12 +63,12 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", org.eclipse.jgit.util;version="[5.1.9,5.2.0)", org.eclipse.jgit.util.io;version="[5.1.9,5.2.0)", org.eclipse.jgit.util.sha1;version="[5.1.9,5.2.0)", - org.tukaani.xz;version="[1.6.0,2.0)", org.junit;version="[4.12,5.0.0)", org.junit.experimental.theories;version="[4.12,5.0.0)", org.junit.rules;version="[4.12,5.0.0)", org.junit.runner;version="[4.12,5.0.0)", org.junit.runners;version="[4.12,5.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)" 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 911f65914..c67c86a93 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 @@ -1266,7 +1266,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/CheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java index 71df59e1f..08ad7b8bc 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; @@ -360,14 +366,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()); @@ -375,10 +381,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()); @@ -386,7 +394,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 2b97b307b..588387d3e 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; @@ -246,13 +250,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); @@ -262,12 +266,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()); @@ -276,13 +280,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 d12f302da..d9a420377 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; @@ -98,7 +99,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; { @@ -116,7 +117,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); @@ -138,7 +139,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()); } @@ -148,7 +149,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; { @@ -166,7 +167,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); @@ -186,7 +187,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()); } @@ -203,7 +204,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 6e458fbbf..467f6956b 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 @@ -51,9 +51,11 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.FileTime; +import java.time.Instant; import java.util.ArrayList; import java.util.List; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.SystemReader; import org.junit.After; @@ -80,11 +82,12 @@ public class FileSnapshotTest { FileUtils.delete(trash, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); } - private static void waitNextSec(File f) { - long initialLastModified = f.lastModified(); + private static void waitNextTick(File f) throws IOException { + Instant initialLastModified = FS.DETECTED.lastModifiedInstant(f); do { - f.setLastModified(System.currentTimeMillis()); - } while (f.lastModified() == initialLastModified); + FS.DETECTED.setLastModified(f.toPath(), Instant.now()); + } while (FS.DETECTED.lastModifiedInstant(f) + .equals(initialLastModified)); } /** @@ -95,10 +98,10 @@ public class FileSnapshotTest { @Test public void testActuallyIsModifiedTrivial() throws Exception { File f1 = createFile("simple"); - waitNextSec(f1); + waitNextTick(f1); FileSnapshot save = FileSnapshot.save(f1); append(f1, (byte) 'x'); - waitNextSec(f1); + waitNextTick(f1); assertTrue(save.isModified(f1)); } @@ -113,7 +116,7 @@ public class FileSnapshotTest { @Test public void testNewFileWithWait() throws Exception { File f1 = createFile("newfile"); - waitNextSec(f1); + waitNextTick(f1); FileSnapshot save = FileSnapshot.save(f1); Thread.sleep(1500); assertTrue(save.isModified(f1)); @@ -146,8 +149,8 @@ public class FileSnapshotTest { File 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); + waitNextTick(f2); + waitNextTick(f2); FileTime timestamp = Files.getLastModifiedTime(f1.toPath()); FileSnapshot save = FileSnapshot.save(f1); Files.move(f2.toPath(), f1.toPath(), // Now "file" is inode x @@ -185,7 +188,7 @@ public class FileSnapshotTest { // 0 sized FileSnapshot. FileSnapshot fs1 = FileSnapshot.MISSING_FILE; // UNKNOWN_SIZE FileSnapshot. - FileSnapshot fs2 = FileSnapshot.save(fs1.lastModified()); + FileSnapshot fs2 = FileSnapshot.save(fs1.lastModifiedInstant()); assertTrue(fs1.equals(fs2)); assertTrue(fs2.equals(fs1)); 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 cbb73bb08..8cc06d93f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java @@ -65,6 +65,7 @@ import org.eclipse.jgit.lib.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; @@ -157,13 +158,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() { @@ -173,7 +175,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..ac0a34ded 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; @@ -78,6 +79,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; @@ -1319,10 +1321,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 02073226d..a4509695d 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/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 d044e65cf..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 @@ -50,12 +50,15 @@ import static org.junit.Assert.assertTrue; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +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.WorkingTreeOptions; +import org.eclipse.jgit.util.FS; import org.junit.Test; public class RacyGitTests extends RepositoryTestCase { @@ -74,8 +77,8 @@ public class RacyGitTests extends RepositoryTestCase { // create two files File a = writeToWorkDir("a", "a"); File b = writeToWorkDir("b", "b"); - assertTrue(a.setLastModified(b.lastModified())); - assertTrue(b.setLastModified(b.lastModified())); + 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 @@ -97,10 +100,11 @@ public class RacyGitTests extends RepositoryTestCase { // filesystem timestamp resolution. By changing the index file // artificially, we create a fake racy situation. File updatedA = writeToWorkDir("a", "a2"); - long newLastModified = updatedA.lastModified() + 100; - assertTrue(updatedA.setLastModified(newLastModified)); + Instant newLastModified = TimeUtil + .setLastModifiedWithOffset(updatedA.toPath(), 100L); resetIndex(new FileTreeIterator(db)); - assertTrue(db.getIndexFile().setLastModified(newLastModified)); + FS.DETECTED.setLastModified(db.getIndexFile().toPath(), + newLastModified); DirCache dc = db.readDirCache(); // check index state: although racily clean a should not be reported as 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 7f5dba697..f22b7d6ad 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/transport/OpenSshConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java index 19fcbfd7a..7777a3c99 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 @@ -57,10 +57,12 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; +import java.time.Instant; 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 +93,14 @@ public class OpenSshConfigTest extends RepositoryTestCase { } private void config(String data) throws IOException { - long lastMtime = configFile.lastModified(); + FS fs = db.getFS(); + Instant lastMtime = fs.lastModifiedInstant(configFile); do { try (final OutputStreamWriter fw = new OutputStreamWriter( new FileOutputStream(configFile), UTF_8)) { fw.write(data); } - } 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/util/FSTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java index 6b3d58bae..bde8a8a6b 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; @@ -106,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)); @@ -119,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)); diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index 5f58e7e45..951a5e30f 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -8,6 +8,20 @@ + + + + + + + + + + + + + + @@ -130,6 +144,28 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -137,12 +173,30 @@ + + + + + + + + + + + + + + + + + + @@ -150,6 +204,14 @@ + + + + + + + + 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 9af6cce59..6ac6955f0 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -561,6 +561,7 @@ 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. 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/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..340214b06 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java @@ -497,8 +497,9 @@ 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; + // TODO (ms) combine smudge_s and smudge_ns into Duration + int smudge_s = (int) (snapshot.lastModifiedInstant().getEpochSecond()); + int smudge_ns = snapshot.lastModifiedInstant().getNano(); // Load the individual file entries. // @@ -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 ca1e3ab27..5110d77dc 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 @@ -611,7 +614,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, @@ -1433,7 +1436,7 @@ public class DirCacheCheckout { } fs.createSymLink(f, target); entry.setLength(bytes.length); - entry.setLastModified(fs.lastModified(f)); + entry.setLastModified(fs.lastModifiedInstant(f)); return; } @@ -1502,7 +1505,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..4b36f6b96 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; @@ -144,6 +145,7 @@ public class DirCacheEntry { /** Flags which are never stored to disk. */ private byte inCoreFlags; + // TODO (ms): use Instant to combine smudge_s and smudge_ns DirCacheEntry(final byte[] sharedInfo, final MutableInteger infoAt, final InputStream in, final MessageDigest md, final int smudge_s, final int smudge_ns) throws IOException { @@ -563,21 +565,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 +723,8 @@ public class DirCacheEntry { @SuppressWarnings("nls") @Override public String toString() { - return getFileMode() + " " + getLength() + " " + getLastModified() + return getFileMode() + " " + getLength() + " " + + getLastModifiedInstant() + " " + getObjectId() + " " + getStage() + " " + getPathString() + "\n"; } @@ -750,12 +782,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 79133d203..38c063d07 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -622,6 +622,7 @@ public class JGitText extends TranslationBundle { /***/ public String readConfigFailed; /***/ public String readerIsRequired; /***/ public String readingObjectsFromLocalRepositoryFailed; + /***/ public String readLastModifiedFailed; /***/ public String readTimedOut; /***/ public String receivePackObjectTooLarge1; /***/ public String receivePackObjectTooLarge2; 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 1b13ccf76..fe3703719 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 @@ -48,10 +48,10 @@ import static org.eclipse.jgit.lib.Constants.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; @@ -87,8 +87,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. *

@@ -96,8 +102,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. @@ -106,8 +112,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); @@ -163,18 +169,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(); + final Instant read = Instant.now(); + return new FileSnapshot(read, Instant.ofEpochMilli(modified), + UNKNOWN_SIZE, Duration.ZERO, 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, Duration.ZERO, 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; @@ -222,7 +251,7 @@ public class FileSnapshot { */ protected FileSnapshot(File file, boolean useConfig) { this.file = file; - this.lastRead = System.currentTimeMillis(); + this.lastRead = Instant.now(); this.fsTimestampResolution = useConfig ? FS.getFsTimerResolution(file.toPath().getParent()) : FALLBACK_TIMESTAMP_RESOLUTION; @@ -230,18 +259,20 @@ public class FileSnapshot { try { fileAttributes = FS.DETECTED.fileAttributes(file); } catch (IOException e) { - this.lastModified = file.lastModified(); + 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); - LOG.debug("file={}, create new FileSnapshot: lastRead={} ms" //$NON-NLS-1$ - + ", lastModified={} ms, size={}, fileKey={}", //$NON-NLS-1$ - file, Long.valueOf(lastRead), Long.valueOf(lastModified), - Long.valueOf(size), fileKey); + 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; @@ -252,7 +283,7 @@ public class FileSnapshot { private boolean wasRacyClean; - private FileSnapshot(long read, long modified, long size, + private FileSnapshot(Instant read, Instant modified, long size, @NonNull Duration fsTimestampResolution, @NonNull Object fileKey) { this.file = null; this.lastRead = read; @@ -266,8 +297,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; } @@ -286,16 +328,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; } @@ -337,7 +379,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; } @@ -351,7 +393,7 @@ public class FileSnapshot { * if sleep was interrupted */ public void waitUntilNotRacy() throws InterruptedException { - while (isRacyClean(System.currentTimeMillis())) { + while (isRacyClean(Instant.now())) { TimeUnit.NANOSECONDS .sleep((fsTimestampResolution.toNanos() + 1) * 11 / 10); } @@ -366,7 +408,7 @@ public class FileSnapshot { */ public boolean equals(FileSnapshot other) { boolean sizeEq = size == UNKNOWN_SIZE || other.size == UNKNOWN_SIZE || size == other.size; - return lastModified == other.lastModified && sizeEq + return lastModified.equals(other.lastModified) && sizeEq && Objects.equals(fileKey, other.fileKey); } @@ -389,8 +431,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); } /** @@ -435,34 +476,37 @@ 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) { + private boolean isRacyClean(Instant read) { // add a 10% safety margin long racyNanos = (fsTimestampResolution.toNanos() + 1) * 11 / 10; - long delta = (read - lastModified) * 1_000_000; + long delta = Duration.between(lastModified, read).toNanos(); wasRacyClean = delta <= racyNanos; - LOG.debug("file={}, isRacyClean={}, read={} ms, lastModified={} ms," //$NON-NLS-1$ - + " delta={} ns, racy<={} ns", //$NON-NLS-1$ - file, Boolean.valueOf(wasRacyClean), Long.valueOf(read), - Long.valueOf(lastModified), Long.valueOf(delta), - Long.valueOf(racyNanos)); + 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(racyNanos)); + } return wasRacyClean; } - 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) { - LOG.debug("file={}, lastModified changed from {} to {}", //$NON-NLS-1$ - file, Long.valueOf(lastModified), - Long.valueOf(currLastModified)); + if (LOG.isDebugEnabled()) { + LOG.debug( + "file={}, lastModified changed from {} to {}", //$NON-NLS-1$ + file, dateFmt.format(lastModified), + dateFmt.format(currLastModified)); + } return true; } 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..ccca0279a 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,11 @@ 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 org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; @@ -424,7 +427,12 @@ public class LockFile { FileSnapshot n = FileSnapshot.save(lck); while (o.equals(n)) { Thread.sleep(25 /* milliseconds */); - lck.setLastModified(System.currentTimeMillis()); + try { + Files.setLastModifiedTime(lck.toPath(), + FileTime.from(Instant.now())); + } catch (IOException e) { + n.waitUntilNotRacy(); + } n = FileSnapshot.save(lck); } } @@ -474,11 +482,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/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java index f60c95f64..d4282e0b8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -46,6 +46,7 @@ */ package org.eclipse.jgit.merge; +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/transport/NetRC.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java index e688f6340..4f1eba66d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java @@ -46,6 +46,7 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.time.Instant; import java.util.Collection; import java.util.HashMap; import java.util.Locale; @@ -123,7 +124,7 @@ public class NetRC { private File netrc; - private long lastModified; + private Instant lastModified; private Map hosts = new HashMap<>(); @@ -187,8 +188,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); @@ -209,7 +212,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 FileReader(netrc))) { String line = null; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java index 480055c2e..4dd5df9cd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java @@ -51,6 +51,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.security.AccessController; import java.security.PrivilegedAction; +import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -166,7 +167,7 @@ public class OpenSshConfig implements ConfigRepository { private final File configFile; /** 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 @@ -224,8 +225,8 @@ public class OpenSshConfig implements ConfigRepository { } 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 (FileInputStream in = new FileInputStream(configFile)) { newState.entries = parse(in); 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 7c6bfb9d6..299f07fb0 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 6e3e336e9..e8570d741 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 static org.eclipse.jgit.lib.Constants.FALLBACK_TIMESTAMP_RESOLUTION; import java.io.BufferedReader; @@ -66,6 +67,7 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.text.MessageFormat; import java.time.Duration; +import java.time.Instant; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -625,11 +627,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, @@ -640,11 +672,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. @@ -1712,9 +1761,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; @@ -1725,7 +1784,7 @@ public abstract class FS { private final long creationTime; - private final long lastModifiedTime; + private final Instant lastModifiedInstant; private final boolean isExecutable; @@ -1743,7 +1802,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; @@ -1752,7 +1811,7 @@ public abstract class FS { this.isSymbolicLink = isSymbolicLink; this.isRegularFile = isRegularFile; this.creationTime = creationTime; - this.lastModifiedTime = lastModifiedTime; + this.lastModifiedInstant = lastModifiedInstant; this.length = length; } @@ -1764,7 +1823,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); } /** @@ -1810,7 +1869,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 9650602fe..80f188cb2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java @@ -76,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} @@ -656,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. * @@ -680,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 @@ -788,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()); @@ -827,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) {