Cache refreshed loose ref dirs in SnapshottingRefDirectory

Update SnapshottingRefDirectory to have a cache of dirs refreshed for
loose refs. This should help improve performance when 'after_open'
setting is used for 'trustLooseRefStat' as duplicate refreshes are
avoided when a snapshot of the ref database is used in a request scope.

Change-Id: I8f66e7cee572e477d29abe2d9db69e97bca3ee4c
Signed-off-by: Nasser Grainawi <quic_nasserg@quicinc.com>
Co-authored-by: Martin Fick <quic_mfick@quicinc.com>
Co-authored-by: Kaushik Lingarkar <quic_kaushikl@quicinc.com>
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectory.java
index 46607f6..1dc5776 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectory.java
@@ -16,15 +16,21 @@
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.revwalk.RevWalk;
 
+import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Snapshotting write-through cache of a {@link RefDirectory}.
  * <p>
  * This is intended to be short-term write-through snapshot based cache used in
- * a request scope to avoid re-reading packed-refs on each read. A future
- * improvement could also snapshot loose refs.
+ * a request scope to avoid re-reading packed-refs on each read and to avoid
+ * refreshing paths to a loose ref that has already been refreshed.
  * <p>
  * Only use this class when concurrent writes from other requests (not using the
  * same instance of SnapshottingRefDirectory) generally need not be visible to
@@ -34,6 +40,7 @@
  */
 class SnapshottingRefDirectory extends RefDirectory {
 	final RefDirectory refDb;
+	private final Set<File> refreshedLooseRefDirs = ConcurrentHashMap.newKeySet();
 
 	private volatile boolean isValid;
 
@@ -67,6 +74,22 @@ PackedRefList getPackedRefs() throws IOException {
 	}
 
 	@Override
+	void refreshPathToLooseRef(Path refPath) {
+		for (int i = 1; i < refPath.getNameCount(); i++) {
+			File dir = fileFor(refPath.subpath(0, i).toString());
+			if (!refreshedLooseRefDirs.contains(dir)) {
+				try (InputStream stream = Files.newInputStream(dir.toPath())) {
+					// open the dir to refresh attributes (on some NFS clients)
+				} catch (IOException e) {
+					break; // loose ref may not exist
+				} finally {
+					refreshedLooseRefDirs.add(dir);
+				}
+			}
+		}
+	}
+
+	@Override
 	void delete(RefDirectoryUpdate update) throws IOException {
 		refreshSnapshot();
 		super.delete(update);
@@ -107,6 +130,7 @@ RefDirectoryRename createRefDirectoryRename(RefDirectoryUpdate from,
 	}
 
 	synchronized void invalidateSnapshot() {
+		refreshedLooseRefDirs.clear();
 		isValid = false;
 	}