Add filesystem metrics

Collect metrics from filesystem:
- number of keep files
- number of directories
- number of empty directories
- number of total files

Bug: Issue 16223
Change-Id: I37a142a166c9beb164e72c6ac9e57e70b677a39a
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/Module.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/Module.java
index 1ffb65c..f612533 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/Module.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.inject.Scopes;
+import com.googlesource.gerrit.plugins.gitrepometrics.collectors.FSMetricsCollector;
 import com.googlesource.gerrit.plugins.gitrepometrics.collectors.GitStatsMetricsCollector;
 import com.googlesource.gerrit.plugins.gitrepometrics.collectors.MetricsCollector;
 import java.util.concurrent.ExecutorService;
@@ -34,6 +35,7 @@
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(GitRepoUpdateListener.class);
     DynamicSet.setOf(binder(), MetricsCollector.class);
     DynamicSet.bind(binder(), MetricsCollector.class).to(GitStatsMetricsCollector.class);
+    DynamicSet.bind(binder(), MetricsCollector.class).to(FSMetricsCollector.class);
     listener().to(PluginStartup.class);
     install(new UpdateGitMetricsTaskModule());
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/FSMetricsCollector.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/FSMetricsCollector.java
new file mode 100644
index 0000000..87e2b0e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/FSMetricsCollector.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.gitrepometrics.collectors;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+
+public class FSMetricsCollector implements MetricsCollector {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public static String numberOfKeepFiles = "numberOfKeepFiles";
+  public static String numberOfEmptyDirectories = "numberOfEmptyDirectories";
+  public static String numberOfDirectories = "numberOfDirectories";
+  public static String numberOfFiles = "numberOfFiles";
+
+  @Override
+  public HashMap<String, Long> collect(FileRepository repository, String projectName) {
+    HashMap<String, Long> metrics = new HashMap<>();
+
+    HashMap<String, AtomicInteger> partialMetrics =
+        filesAndDirectoriesCount(repository, projectName);
+    putMetric(
+        projectName,
+        metrics,
+        numberOfEmptyDirectories,
+        partialMetrics.get(numberOfEmptyDirectories).longValue());
+    putMetric(
+        projectName,
+        metrics,
+        numberOfDirectories,
+        partialMetrics.get(numberOfDirectories).longValue());
+    putMetric(projectName, metrics, numberOfFiles, partialMetrics.get(numberOfFiles).longValue());
+    putMetric(
+        projectName, metrics, numberOfKeepFiles, partialMetrics.get(numberOfKeepFiles).longValue());
+
+    return metrics;
+  }
+
+  private HashMap<String, AtomicInteger> filesAndDirectoriesCount(
+      FileRepository repository, String projectName) {
+    HashMap<String, AtomicInteger> counter =
+        new HashMap<String, AtomicInteger>() {
+          {
+            put(numberOfFiles, new AtomicInteger(0));
+            put(numberOfDirectories, new AtomicInteger(0));
+            put(numberOfEmptyDirectories, new AtomicInteger(0));
+            put(numberOfKeepFiles, new AtomicInteger(0));
+          }
+        };
+    try {
+      Files.walk(repository.getObjectsDirectory().toPath())
+          .parallel()
+          .forEach(
+              path -> {
+                if (path.toFile().isFile()) {
+                  counter.get(numberOfFiles).updateAndGet(metricCounter -> metricCounter + 1);
+                  if (path.toFile().getName().endsWith(".keep")) {
+                    counter.get(numberOfKeepFiles).updateAndGet(metricCounter -> metricCounter + 1);
+                  }
+                }
+                if (path.toFile().isDirectory()) {
+                  counter.get(numberOfDirectories).updateAndGet(metricCounter -> metricCounter + 1);
+                  if (Objects.requireNonNull(path.toFile().listFiles()).length == 0) {
+                    counter
+                        .get(numberOfEmptyDirectories)
+                        .updateAndGet(metricCounter -> metricCounter + 1);
+                  }
+                }
+              });
+    } catch (IOException e) {
+      logger.atSevere().withCause(e).log("Can't open object directory for project " + projectName);
+    }
+
+    return counter;
+  }
+
+  @Override
+  public String getMetricsCollectorName() {
+    return "filesystem-statistics";
+  }
+
+  @Override
+  public ImmutableList<GitRepoMetric> availableMetrics() {
+    return ImmutableList.of(
+        new GitRepoMetric(numberOfKeepFiles, "Number of keep files on filesystem", "Count"),
+        new GitRepoMetric(
+            numberOfEmptyDirectories, "Number of empty directories on filesystem", "Count"),
+        new GitRepoMetric(numberOfDirectories, "Number of directories on filesystem", "Count"),
+        new GitRepoMetric(numberOfFiles, "Number of directories on filesystem", "Count"));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitStatsMetricsCollector.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitStatsMetricsCollector.java
index 00182e4..aa01d1a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitStatsMetricsCollector.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitStatsMetricsCollector.java
@@ -16,7 +16,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
-import com.googlesource.gerrit.plugins.gitrepometrics.GitRepoMetricsCache;
 import java.io.IOException;
 import java.util.HashMap;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
@@ -69,11 +68,6 @@
         new GitRepoMetric(numberOfBitmaps, "Number of bitmaps", "Count"));
   }
 
-  private void putMetric(
-      String projectName, HashMap<String, Long> metrics, String metricName, long value) {
-    metrics.put(GitRepoMetricsCache.getMetricName(metricName, projectName), value);
-  }
-
   @Override
   public String getMetricsCollectorName() {
     return "git-statistics";
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/MetricsCollector.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/MetricsCollector.java
index 311a707..bfe06f4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/MetricsCollector.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/MetricsCollector.java
@@ -14,6 +14,8 @@
 
 package com.googlesource.gerrit.plugins.gitrepometrics.collectors;
 
+import static com.googlesource.gerrit.plugins.gitrepometrics.GitRepoMetricsCache.getMetricName;
+
 import com.google.common.collect.ImmutableList;
 import java.util.HashMap;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
@@ -45,4 +47,9 @@
    *     metric collector implementation.
    */
   ImmutableList<GitRepoMetric> availableMetrics();
+
+  default void putMetric(
+      String projectName, HashMap<String, Long> metrics, String metricName, long value) {
+    metrics.put(getMetricName(metricName, projectName), value);
+  }
 }
diff --git a/src/resources/Documentation/config.md b/src/resources/Documentation/config.md
index 26d162e..963f413 100644
--- a/src/resources/Documentation/config.md
+++ b/src/resources/Documentation/config.md
@@ -15,6 +15,10 @@
 plugins_git_repo_metrics_numberofpackfiles_<repo_name>
 plugins_git_repo_metrics_sizeoflooseobjects_<repo_name>
 plugins_git_repo_metrics_sizeofpackedobjects_<repo_name>
+plugins_git_repo_metrics_numberofkeepfiles_<repo_name>
+plugins_git_repo_metrics_numberoffiles_<repo_name>
+plugins_git_repo_metrics_numberofdirectories_<repo_name>
+plugins_git_repo_metrics_numberofemptydirectories_<repo_name>
 ```
 
 Settings
diff --git a/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheIT.java b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheIT.java
index 97031c3..f9693ac 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.config.GlobalPluginConfig;
 import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.gitrepometrics.collectors.FSMetricsCollector;
 import com.googlesource.gerrit.plugins.gitrepometrics.collectors.GitStatsMetricsCollector;
 import java.util.Arrays;
 import java.util.List;
@@ -48,7 +49,10 @@
             .filter(metricName -> metricName.contains("git-repo-metrics"))
             .collect(Collectors.toList());
 
-    int expectedMetricsCount = new GitStatsMetricsCollector().availableMetrics().size();
+    int expectedMetricsCount =
+        new GitStatsMetricsCollector().availableMetrics().size()
+            + new FSMetricsCollector().availableMetrics().size();
+
     assertThat(repoMetricsCount.size()).isEqualTo(availableProjects.size() * expectedMetricsCount);
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/FSMetricsCollectorTest.java b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/FSMetricsCollectorTest.java
new file mode 100644
index 0000000..f330af3
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/FSMetricsCollectorTest.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.gitrepometrics.collectors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.HashMap;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class FSMetricsCollectorTest {
+
+  @Rule public TemporaryFolder dir = new TemporaryFolder();
+
+  private FileRepository repository;
+
+  @Before
+  public void setUp() throws Exception {
+    repository = createRepository("someRepo.git");
+  }
+
+  @Test
+  public void testCorrectMetricsCollection() throws IOException {
+    File objectDirectory = repository.getObjectsDirectory();
+    Files.createFile(new File(objectDirectory, "pack/keep1.keep").toPath());
+
+    HashMap<String, Long> metrics = new FSMetricsCollector().collect(repository, "testRepo");
+
+    // This is the FS structure, from the "objects" directory, metrics are collected from:
+    //  .
+    //  ├── info
+    //  └── pack
+    //      └── keep1.keep
+    assertThat(metrics.get("numberofkeepfiles_testrepo")).isEqualTo(1); // keep1.keep
+    assertThat(metrics.get("numberoffiles_testrepo")).isEqualTo(1); // keep1.keep
+    assertThat(metrics.get("numberofdirectories_testrepo")).isEqualTo(3); // info, pack and .
+    assertThat(metrics.get("numberofemptydirectories_testrepo")).isEqualTo(1); // info
+  }
+
+  private FileRepository createRepository(String repoName) throws Exception {
+    File repo = dir.newFolder(repoName);
+    try (Git git = Git.init().setDirectory(repo).call()) {
+      return (FileRepository) git.getRepository();
+    }
+  }
+}