summaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.junit/src
diff options
context:
space:
mode:
authorMatthias Sohn <matthias.sohn@sap.com>2019-08-08 12:30:47 +0200
committerMatthias Sohn <matthias.sohn@sap.com>2019-08-08 13:04:38 +0200
commit718555518ded4fbcafe0c1203f0035d3251884e6 (patch)
tree3e49529cd0ebc21fcea581efe0fa15fdcdc8b81c /org.eclipse.jgit.junit/src
parentfcfe1299c37ae8308e9d930201823b3ac340dd2b (diff)
parent0046b2a8fefcbfecc10a7b198a075eb2775d3e7a (diff)
downloadjgit-718555518ded4fbcafe0c1203f0035d3251884e6.tar.gz
jgit-718555518ded4fbcafe0c1203f0035d3251884e6.zip
Merge branch 'stable-5.2' into stable-5.3
* stable-5.2: Fix OpenSshConfigTest#config FileSnapshot: fix bug with timestamp thresholding In LockFile#waitForStatChange wait in units of file time resolution Cache FileStoreAttributeCache per directory Fix FileSnapshot#save(long) and FileSnapshot#save(Instant) Persist minimal racy threshold and allow manual configuration Measure minimum racy interval to auto-configure FileSnapshot Reuse FileUtils to recursively delete files created by tests Fix FileAttributeCache.toString() Add test for racy git detection in FileSnapshot Repeat RefDirectoryTest.testGetRef_DiscoversModifiedLoose 100 times Fix org.eclipse.jdt.core.prefs of org.eclipse.jgit.junit Add missing javadoc in org.eclipse.jgit.junit Enhance RepeatRule to report number of failures at the end Fix FileSnapshotTests for filesystem with high timestamp resolution Retry deleting test files in FileBasedConfigTest Measure filesystem timestamp resolution already in test setup Refactor FileSnapshotTest to use NIO APIs Measure stored timestamp resolution instead of time to touch file Handle CancellationException in FileStoreAttributeCache Fix FileSnapshot#saveNoConfig Use Instant for smudge time in DirCache and DirCacheEntry Use Instant instead of milliseconds for filesystem timestamp handling Workaround SecurityException in FS#getFsTimestampResolution Fix NPE in FS$FileStoreAttributeCache.getFsTimestampResolution FS: ignore AccessDeniedException when measuring timestamp resolution Add debug trace for FileSnapshot Use FileChannel.open to touch file and set mtime to now Persist filesystem timestamp resolution and allow manual configuration Increase bazel timeout for long running tests Bazel: Fix lint warning flagged by buildifier Update bazlets to latest version Bazel: Add missing dependencies for ArchiveCommandTest Bazel: Remove FileTreeIteratorWithTimeControl from BUILD file Add support for nanoseconds and microseconds for Config#getTimeUnit Optionally measure filesystem timestamp resolution asynchronously Delete unused FileTreeIteratorWithTimeControl FileSnapshot#equals: consider UNKNOWN_SIZE Timeout measuring file timestamp resolution after 2 seconds Fix RacyGitTests#testRacyGitDetection Change RacyGitTests to create a racy git situation in a stable way Deprecate Constants.CHARACTER_ENCODING in favor of StandardCharsets.UTF_8 Fix non-deterministic hash of archives created by ArchiveCommand Update Maven plugins ecj, plexus, error-prone Update Maven plugins and cleanup Maven warnings Make inner classes static where possible Fix API problem filters Change-Id: I238adfd3080a5fed9d64c3c757297da6ea893918 Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Diffstat (limited to 'org.eclipse.jgit.junit/src')
-rw-r--r--org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java57
-rw-r--r--org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java9
-rw-r--r--org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java46
-rw-r--r--org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java20
-rw-r--r--org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java139
-rw-r--r--org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java99
6 files changed, 332 insertions, 38 deletions
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 4b2eadf418..838537f3ae 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
@@ -51,6 +51,8 @@ import static org.junit.Assert.fail;
import java.io.File;
import java.io.IOException;
+import java.io.PrintStream;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -126,6 +128,10 @@ public abstract class LocalDiskRepositoryTestCase {
if (!tmp.delete() || !tmp.mkdir())
throw new IOException("Cannot create " + tmp);
+ // measure timer resolution before the test to avoid time critical tests
+ // are affected by time needed for measurement
+ FS.getFileStoreAttributes(tmp.toPath().getParent());
+
mockSystemReader = new MockSystemReader();
mockSystemReader.userGitConfig = new FileBasedConfig(new File(tmp,
"usergitconfig"), FS.DETECTED);
@@ -232,35 +238,30 @@ public abstract class LocalDiskRepositoryTestCase {
private static boolean recursiveDelete(final File dir,
boolean silent, boolean failOnError) {
assert !(silent && failOnError);
- if (!dir.exists())
- return silent;
- final File[] ls = dir.listFiles();
- if (ls != null)
- for (int k = 0; k < ls.length; k++) {
- final File e = ls[k];
- if (e.isDirectory())
- silent = recursiveDelete(e, silent, failOnError);
- else if (!e.delete()) {
- if (!silent)
- reportDeleteFailure(failOnError, e);
- silent = !failOnError;
- }
- }
- if (!dir.delete()) {
- if (!silent)
- reportDeleteFailure(failOnError, dir);
- silent = !failOnError;
+ int options = FileUtils.RECURSIVE | FileUtils.RETRY
+ | FileUtils.SKIP_MISSING;
+ if (silent) {
+ options |= FileUtils.IGNORE_ERRORS;
}
- return silent;
+ try {
+ FileUtils.delete(dir, options);
+ } catch (IOException e) {
+ reportDeleteFailure(failOnError, dir, e);
+ return !failOnError;
+ }
+ return true;
}
- private static void reportDeleteFailure(boolean failOnError, File e) {
+ private static void reportDeleteFailure(boolean failOnError, File f,
+ Exception cause) {
String severity = failOnError ? "ERROR" : "WARNING";
- String msg = severity + ": Failed to delete " + e;
- if (failOnError)
+ String msg = severity + ": Failed to delete " + f;
+ if (failOnError) {
fail(msg);
- else
+ } else {
System.err.println(msg);
+ }
+ cause.printStackTrace(new PrintStream(System.err));
}
/** Constant <code>MOD_TIME=1</code> */
@@ -322,12 +323,13 @@ public abstract class LocalDiskRepositoryTestCase {
throws IllegalStateException, IOException {
DirCache dc = repo.readDirCache();
StringBuilder sb = new StringBuilder();
- TreeSet<Long> timeStamps = new TreeSet<>();
+ TreeSet<Instant> timeStamps = new TreeSet<>();
// iterate once over the dircache just to collect all time stamps
if (0 != (includedOptions & MOD_TIME)) {
- for (int i=0; i<dc.getEntryCount(); ++i)
- timeStamps.add(Long.valueOf(dc.getEntry(i).getLastModified()));
+ for (int i = 0; i < dc.getEntryCount(); ++i) {
+ timeStamps.add(dc.getEntry(i).getLastModifiedInstant());
+ }
}
// iterate again, now produce the result string
@@ -339,7 +341,8 @@ public abstract class LocalDiskRepositoryTestCase {
sb.append(", stage:" + stage);
if (0 != (includedOptions & MOD_TIME)) {
sb.append(", time:t"+
- timeStamps.headSet(Long.valueOf(entry.getLastModified())).size());
+ timeStamps.headSet(entry.getLastModifiedInstant())
+ .size());
}
if (0 != (includedOptions & SMUDGE))
if (entry.isSmudged())
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java
index 08220ce245..94df554aec 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java
@@ -56,4 +56,13 @@ public @interface Repeat {
* Number of repetitions
*/
public abstract int n();
+
+ /**
+ * Whether to abort execution on first test failure
+ *
+ * @return {@code true} if execution should be aborted on the first failure,
+ * otherwise count failures and continue execution
+ * @since 5.1.9
+ */
+ public boolean abortOnFailure() default true;
}
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java
index 8165738ed8..8636f2a0aa 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java
@@ -81,9 +81,31 @@ public class RepeatRule implements TestRule {
private static Logger LOG = Logger
.getLogger(RepeatRule.class.getName());
+ /**
+ * Exception thrown if repeated execution of a test annotated with
+ * {@code @Repeat} failed.
+ */
public static class RepeatedTestException extends RuntimeException {
private static final long serialVersionUID = 1L;
+ /**
+ * Constructor
+ *
+ * @param message
+ * the error message
+ */
+ public RepeatedTestException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param message
+ * the error message
+ * @param cause
+ * exception causing this exception
+ */
public RepeatedTestException(String message, Throwable cause) {
super(message, cause);
}
@@ -93,28 +115,45 @@ public class RepeatRule implements TestRule {
private final int repetitions;
+ private boolean abortOnFailure;
+
private final Statement statement;
- private RepeatStatement(int repetitions, Statement statement) {
+ private RepeatStatement(int repetitions, boolean abortOnFailure,
+ Statement statement) {
this.repetitions = repetitions;
+ this.abortOnFailure = abortOnFailure;
this.statement = statement;
}
@Override
public void evaluate() throws Throwable {
+ int failures = 0;
for (int i = 0; i < repetitions; i++) {
try {
statement.evaluate();
} catch (Throwable e) {
+ failures += 1;
RepeatedTestException ex = new RepeatedTestException(
MessageFormat.format(
"Repeated test failed when run for the {0}. time",
Integer.valueOf(i + 1)),
e);
LOG.log(Level.SEVERE, ex.getMessage(), ex);
- throw ex;
+ if (abortOnFailure) {
+ throw ex;
+ }
}
}
+ if (failures > 0) {
+ RepeatedTestException e = new RepeatedTestException(
+ MessageFormat.format(
+ "Test failed {0} times out of {1} repeated executions",
+ Integer.valueOf(failures),
+ Integer.valueOf(repetitions)));
+ LOG.log(Level.SEVERE, e.getMessage(), e);
+ throw e;
+ }
}
}
@@ -125,7 +164,8 @@ public class RepeatRule implements TestRule {
Repeat repeat = description.getAnnotation(Repeat.class);
if (repeat != null) {
int n = repeat.n();
- result = new RepeatStatement(n, statement);
+ boolean abortOnFailure = repeat.abortOnFailure();
+ result = new RepeatStatement(n, abortOnFailure, statement);
}
return result;
}
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
index 987f923b0e..5aacbbadec 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
@@ -57,7 +57,9 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.file.Path;
+import java.time.Instant;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
@@ -284,7 +286,7 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase {
dce = new DirCacheEntry(treeItr.getEntryPathString());
dce.setFileMode(treeItr.getEntryFileMode());
- dce.setLastModified(treeItr.getEntryLastModified());
+ dce.setLastModified(treeItr.getEntryLastModifiedInstant());
dce.setLength((int) len);
try (FileInputStream in = new FileInputStream(
treeItr.getEntryFile())) {
@@ -361,7 +363,8 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase {
* @throws InterruptedException
* @throws IOException
*/
- public static long fsTick(File lastFile) throws InterruptedException,
+ public static Instant fsTick(File lastFile)
+ throws InterruptedException,
IOException {
File tmp;
FS fs = FS.DETECTED;
@@ -375,15 +378,16 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase {
tmp = File.createTempFile("fsTickTmpFile", null,
lastFile.getParentFile());
}
- long res = FS.getFsTimerResolution(tmp.toPath()).toMillis();
+ long res = FS.getFileStoreAttributes(tmp.toPath())
+ .getFsTimestampResolution().toNanos();
long sleepTime = res / 10;
try {
- long startTime = fs.lastModified(lastFile);
- long actTime = fs.lastModified(tmp);
- while (actTime <= startTime) {
- Thread.sleep(sleepTime);
+ Instant startTime = fs.lastModifiedInstant(lastFile);
+ Instant actTime = fs.lastModifiedInstant(tmp);
+ while (actTime.compareTo(startTime) <= 0) {
+ TimeUnit.NANOSECONDS.sleep(sleepTime);
FileUtils.touch(tmp.toPath());
- actTime = fs.lastModified(tmp);
+ actTime = fs.lastModifiedInstant(tmp);
}
return actTime;
} finally {
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
index 55a7766f60..02ffe4f409 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
@@ -1076,6 +1076,14 @@ public class TestRepository<R extends Repository> implements AutoCloseable {
parents.add(prior.create());
}
+ /**
+ * set parent commit
+ *
+ * @param p
+ * parent commit
+ * @return this commit builder
+ * @throws Exception
+ */
public CommitBuilder parent(RevCommit p) throws Exception {
if (parents.isEmpty()) {
DirCacheBuilder b = tree.builder();
@@ -1088,29 +1096,71 @@ public class TestRepository<R extends Repository> implements AutoCloseable {
return this;
}
+ /**
+ * Get parent commits
+ *
+ * @return parent commits
+ */
public List<RevCommit> parents() {
return Collections.unmodifiableList(parents);
}
+ /**
+ * Remove parent commits
+ *
+ * @return this commit builder
+ */
public CommitBuilder noParents() {
parents.clear();
return this;
}
+ /**
+ * Remove files
+ *
+ * @return this commit builder
+ */
public CommitBuilder noFiles() {
tree.clear();
return this;
}
+ /**
+ * Set top level tree
+ *
+ * @param treeId
+ * the top level tree
+ * @return this commit builder
+ */
public CommitBuilder setTopLevelTree(ObjectId treeId) {
topLevelTree = treeId;
return this;
}
+ /**
+ * Add file with given content
+ *
+ * @param path
+ * path of the file
+ * @param content
+ * the file content
+ * @return this commit builder
+ * @throws Exception
+ */
public CommitBuilder add(String path, String content) throws Exception {
return add(path, blob(content));
}
+ /**
+ * Add file with given path and blob
+ *
+ * @param path
+ * path of the file
+ * @param id
+ * blob for this file
+ * @return this commit builder
+ * @throws Exception
+ */
public CommitBuilder add(String path, RevBlob id)
throws Exception {
return edit(new PathEdit(path) {
@@ -1122,6 +1172,13 @@ public class TestRepository<R extends Repository> implements AutoCloseable {
});
}
+ /**
+ * Edit the index
+ *
+ * @param edit
+ * the index record update
+ * @return this commit builder
+ */
public CommitBuilder edit(PathEdit edit) {
DirCacheEditor e = tree.editor();
e.add(edit);
@@ -1129,6 +1186,13 @@ public class TestRepository<R extends Repository> implements AutoCloseable {
return this;
}
+ /**
+ * Remove a file
+ *
+ * @param path
+ * path of the file
+ * @return this commit builder
+ */
public CommitBuilder rm(String path) {
DirCacheEditor e = tree.editor();
e.add(new DeletePath(path));
@@ -1137,49 +1201,111 @@ public class TestRepository<R extends Repository> implements AutoCloseable {
return this;
}
+ /**
+ * Set commit message
+ *
+ * @param m
+ * the message
+ * @return this commit builder
+ */
public CommitBuilder message(String m) {
message = m;
return this;
}
+ /**
+ * Get the commit message
+ *
+ * @return the commit message
+ */
public String message() {
return message;
}
+ /**
+ * Tick the clock
+ *
+ * @param secs
+ * number of seconds
+ * @return this commit builder
+ */
public CommitBuilder tick(int secs) {
tick = secs;
return this;
}
+ /**
+ * Set author and committer identity
+ *
+ * @param ident
+ * identity to set
+ * @return this commit builder
+ */
public CommitBuilder ident(PersonIdent ident) {
author = ident;
committer = ident;
return this;
}
+ /**
+ * Set the author identity
+ *
+ * @param a
+ * the author's identity
+ * @return this commit builder
+ */
public CommitBuilder author(PersonIdent a) {
author = a;
return this;
}
+ /**
+ * Get the author identity
+ *
+ * @return the author identity
+ */
public PersonIdent author() {
return author;
}
+ /**
+ * Set the committer identity
+ *
+ * @param c
+ * the committer identity
+ * @return this commit builder
+ */
public CommitBuilder committer(PersonIdent c) {
committer = c;
return this;
}
+ /**
+ * Get the committer identity
+ *
+ * @return the committer identity
+ */
public PersonIdent committer() {
return committer;
}
+ /**
+ * Insert changeId
+ *
+ * @return this commit builder
+ */
public CommitBuilder insertChangeId() {
changeId = "";
return this;
}
+ /**
+ * Insert given changeId
+ *
+ * @param c
+ * changeId
+ * @return this commit builder
+ */
public CommitBuilder insertChangeId(String c) {
// Validate, but store as a string so we can use "" as a sentinel.
ObjectId.fromString(c);
@@ -1187,6 +1313,13 @@ public class TestRepository<R extends Repository> implements AutoCloseable {
return this;
}
+ /**
+ * Create the commit
+ *
+ * @return the new commit
+ * @throws Exception
+ * if creation failed
+ */
public RevCommit create() throws Exception {
if (self == null) {
TestRepository.this.tick(tick);
@@ -1247,6 +1380,12 @@ public class TestRepository<R extends Repository> implements AutoCloseable {
+ cid.getName() + "\n"); //$NON-NLS-1$
}
+ /**
+ * Create child commit builder
+ *
+ * @return child commit builder
+ * @throws Exception
+ */
public CommitBuilder child() throws Exception {
return new CommitBuilder(this);
}
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java
new file mode 100644
index 0000000000..1f8070d919
--- /dev/null
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.junit.time;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
+
+import org.eclipse.jgit.util.FS;
+
+/**
+ * Utility methods for handling timestamps
+ */
+public class TimeUtil {
+ /**
+ * Set the lastModified time of a given file by adding a given offset to the
+ * current lastModified time
+ *
+ * @param path
+ * path of a file to set last modified
+ * @param offsetMillis
+ * offset in milliseconds, if negative the new lastModified time
+ * is offset before the original lastModified time, otherwise
+ * after the original time
+ * @return the new lastModified time
+ */
+ public static Instant setLastModifiedWithOffset(Path path,
+ long offsetMillis) {
+ Instant mTime = FS.DETECTED.lastModifiedInstant(path)
+ .plusMillis(offsetMillis);
+ try {
+ Files.setLastModifiedTime(path, FileTime.from(mTime));
+ return mTime;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /**
+ * Set the lastModified time of file a to the one from file b
+ *
+ * @param a
+ * file to set lastModified time
+ * @param b
+ * file to read lastModified time from
+ */
+ public static void setLastModifiedOf(Path a, Path b) {
+ Instant mTime = FS.DETECTED.lastModifiedInstant(b);
+ try {
+ Files.setLastModifiedTime(a, FileTime.from(mTime));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+}