Initial plugin prototype

The initial version of the plugin only contains a single metric
collector.

At startup numberOfMetrics * numberOFProjects metrics are statically registered.
It would be better to dynamically register them when sample come in.

Change-Id: I529c42d5cda9602a421ecba6dc18b3449716a70c
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..543a692
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..1052170
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,52 @@
+load("@rules_java//java:defs.bzl", "java_binary", "java_library")
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+    "//tools/bzl:plugin.bzl",
+    "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
+    "gerrit_plugin",
+)
+
+gerrit_plugin(
+    name = "git-repo-metrics",
+    srcs = glob(
+        ["src/main/java/**/*.java"],
+    ),
+    manifest_entries = [
+        "Gerrit-PluginName: git-repo-metrics",
+        "Gerrit-Module: com.googlesource.gerrit.plugins.gitrepometrics.Module",
+        "Implementation-Title: git-repo-metrics plugin",
+        "Implementation-URL: https://review.gerrithub.io/admin/repos/GerritForge/git-repo-metrics",
+        "Implementation-Vendor: GerritForge",
+    ],
+    resources = glob(
+        ["src/main/resources/**/*"],
+    )
+)
+
+junit_tests(
+    name = "git-repo-metrics_tests",
+    srcs = glob(["src/test/java/**/*.java"]),
+    resources = glob(["src/test/resources/**/*"]),
+    tags = [
+        "git-repo-metrics",
+    ],
+    deps = [
+        ":git-repo-metrics__plugin_test_deps",
+    ],
+)
+
+java_library(
+    name = "git-repo-metrics__plugin_test_deps",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+        ":git-repo-metrics__plugin",
+    ],
+)
+
+java_library(
+    name = "git-repo-metrics__plugin_deps",
+    visibility = ["//visibility:public"],
+    exports = PLUGIN_DEPS
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..780f1c0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,34 @@
+# Plugin to collect Git repository metrics
+
+This plugin allows a systematic collection of repository metrics.
+Metrics are updated upon a `ref-update` receive.
+
+## How to build
+
+Clone or link this plugin to the plugins directory of Gerrit's source tree, and then run bazel build
+on the plugin's directory.
+
+Example:
+
+```
+git clone --recursive https://gerrit.googlesource.com/gerrit
+git clone https://gerrit.googlesource.com/plugins/git-repo-metrics
+pushd gerrit/plugins && ln -s ../../git-repo-metrics . && popd
+cd gerrit && bazel build plugins/git-repo-metrics
+```
+
+The output plugin jar is created in:
+
+```
+bazel-genfiles/plugins/git-repo-metrics/git-repo-metrics.jar
+```
+
+## How to install
+
+Copy the git-repo-metrics.jar into the Gerrit's /plugins directory and wait for the plugin to be automatically
+loaded.
+
+## Configuration
+
+More information about the plugin configuration can be found in the [config.md](resources/Documentation/config.md)
+file.
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheModule.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheModule.java
new file mode 100644
index 0000000..bd39eee
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheModule.java
@@ -0,0 +1,80 @@
+// 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;
+
+import com.google.common.base.Supplier;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.gitrepometrics.collectors.GitRepoMetric;
+import com.googlesource.gerrit.plugins.gitrepometrics.collectors.GitStats;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+@Singleton
+public class GitRepoMetricsCacheModule {
+  public static List<GitRepoMetric> metricsNames = new ArrayList<>(GitStats.availableMetrics());
+  public List<String> projects;
+
+  public static Map<String, Long> metrics = new HashMap<>(Collections.emptyMap());;
+
+  public final MetricMaker metricMaker;
+  public final GitRepoMetricsConfig config;
+
+  @Inject
+  GitRepoMetricsCacheModule(MetricMaker metricMaker, GitRepoMetricsConfig config) {
+    this.metricMaker = metricMaker;
+    this.config = config;
+    this.projects = config.getRepositoryNames();
+  }
+
+  public void initCache() {
+    metricsNames.forEach(
+        gitRepoMetric -> {
+          projects.forEach(
+              projectName -> {
+                String name =
+                    GitRepoMetricsCacheModule.getMetricName(gitRepoMetric.getName(), projectName);
+                Supplier<Long> supplier =
+                    new Supplier<Long>() {
+                      public Long get() {
+                        // TODO Blaah! Initializing all the values to zero!? Would be better
+                        // registering
+                        //     dynamically the metrics
+                        // TODO add grace period!!
+                        return GitRepoMetricsCacheModule.metrics.getOrDefault(name, 0L);
+                      }
+                    };
+
+                metricMaker.newCallbackMetric(
+                    name,
+                    Long.class,
+                    new Description(gitRepoMetric.getDescription())
+                        .setRate()
+                        .setUnit(gitRepoMetric.getUnit()),
+                    supplier);
+              });
+        });
+  }
+
+  public static String getMetricName(String metricName, String projectName) {
+    return String.format("%s_%s", metricName, projectName).toLowerCase(Locale.ROOT);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsConfig.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsConfig.java
new file mode 100644
index 0000000..cabe96a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsConfig.java
@@ -0,0 +1,41 @@
+// 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;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Arrays;
+import java.util.List;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class GitRepoMetricsConfig {
+
+  private final Config config;
+
+  @Inject
+  public GitRepoMetricsConfig(PluginConfigFactory configFactory, @PluginName String pluginName) {
+    config = configFactory.getGlobalPluginConfig(pluginName);
+  }
+
+  public List<String> getRepositoryNames() {
+    return Arrays.stream(config.getStringList("git-repo-metrics", null, "project"))
+        .collect(toList());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoUpdateListener.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoUpdateListener.java
new file mode 100644
index 0000000..fc4307c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoUpdateListener.java
@@ -0,0 +1,55 @@
+// 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;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
+public class GitRepoUpdateListener implements GitReferenceUpdatedListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private final GitRepositoryManager repoManager;
+  private final ExecutorService executor;
+
+  @Inject
+  GitRepoUpdateListener(
+      GitRepositoryManager repoManager, @UpdateGitMetricsExecutor ExecutorService executor) {
+    this.repoManager = repoManager;
+    this.executor = executor;
+  }
+
+  @Override
+  public void onGitReferenceUpdated(Event event) {
+    String projectName = event.getProjectName();
+    Project.NameKey projectNameKey = Project.nameKey(projectName);
+    logger.atFine().log("Got an update for project %s", projectName);
+    try (Repository repository = repoManager.openRepository(projectNameKey)) {
+      UpdateGitMetricsTask updateGitMetricsTask =
+          new UpdateGitMetricsTask(repository, Project.builder(projectNameKey).build());
+      executor.execute(updateGitMetricsTask);
+    } catch (RepositoryNotFoundException e) {
+      logger.atSevere().withCause(e).log("Cannot find repository for %s", projectName);
+    } catch (IOException e) {
+      logger.atSevere().withCause(e).log(
+          "Something went wrong when reading from the repository for %s", projectName);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/Module.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/Module.java
new file mode 100644
index 0000000..e9df0e2
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/Module.java
@@ -0,0 +1,33 @@
+// 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;
+
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import java.util.concurrent.ExecutorService;
+
+public class Module extends LifecycleModule {
+
+  @Override
+  protected void configure() {
+    bind(ExecutorService.class)
+        .annotatedWith(UpdateGitMetricsExecutor.class)
+        .toProvider(UpdateGitMetricsExecutorProvider.class);
+    bind(GitRepoUpdateListener.class);
+    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(GitRepoUpdateListener.class);
+    listener().to(PluginStartup.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/PluginStartup.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/PluginStartup.java
new file mode 100644
index 0000000..8c1cc60
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/PluginStartup.java
@@ -0,0 +1,38 @@
+// 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;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.inject.Inject;
+
+public class PluginStartup implements LifecycleListener {
+  public final MetricMaker metricMaker;
+  public final GitRepoMetricsConfig config;
+
+  @Inject
+  public PluginStartup(MetricMaker metricMaker, GitRepoMetricsConfig config) {
+    this.metricMaker = metricMaker;
+    this.config = config;
+  }
+
+  @Override
+  public void start() {
+    new GitRepoMetricsCacheModule(metricMaker, config).initCache();
+  }
+
+  @Override
+  public void stop() {}
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsExecutor.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsExecutor.java
new file mode 100644
index 0000000..82e7585
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsExecutor.java
@@ -0,0 +1,24 @@
+// 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;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface UpdateGitMetricsExecutor {}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsExecutorProvider.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsExecutorProvider.java
new file mode 100644
index 0000000..6e74c88
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsExecutorProvider.java
@@ -0,0 +1,38 @@
+// 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;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.concurrent.ExecutorService;
+
+@Singleton
+public class UpdateGitMetricsExecutorProvider implements Provider<ExecutorService> {
+  private ExecutorService executor;
+
+  @Inject
+  UpdateGitMetricsExecutorProvider(WorkQueue workQueue, @PluginName String pluginName) {
+    // TODO Make pool size configurable
+    executor = workQueue.createQueue(1, "[" + pluginName + " plugin]");
+  }
+
+  @Override
+  public ExecutorService get() {
+    return executor;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsTask.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsTask.java
new file mode 100644
index 0000000..f2b9fd3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsTask.java
@@ -0,0 +1,62 @@
+// 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;
+
+import static com.googlesource.gerrit.plugins.gitrepometrics.GitRepoMetricsCacheModule.metrics;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.googlesource.gerrit.plugins.gitrepometrics.collectors.GitStats;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.Repository;
+
+public class UpdateGitMetricsTask implements Runnable {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final Repository repository;
+  private final Project project;
+
+  UpdateGitMetricsTask(Repository repository, Project project) {
+    this.repository = repository;
+    this.project = project;
+  }
+
+  @Override
+  public void run() {
+    // TODO Might be a noop
+    logger.atInfo().log(
+        "Running task to collect stats: repo %s, project %s",
+        repository.getIdentifier(), project.getName());
+    // TODO Loop through all the collectors
+    GitStats gitStats = new GitStats((FileRepository) repository, project);
+    Map<String, Long> newMetrics = gitStats.get();
+    logger.atInfo().log(
+        "Here all the metrics for %s - %s", project.getName(), getStringFromMap(newMetrics));
+    metrics.putAll(newMetrics);
+  }
+
+  String getStringFromMap(Map<String, Long> m) {
+    return m.keySet().stream()
+        .map(key -> key + "=" + m.get(key))
+        .collect(Collectors.joining(", ", "{", "}"));
+  }
+
+  @Override
+  public String toString() {
+    return String.join(" - ", repository.toString(), project.getName());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitRepoMetric.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitRepoMetric.java
new file mode 100644
index 0000000..b016fbc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitRepoMetric.java
@@ -0,0 +1,39 @@
+// 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;
+
+public class GitRepoMetric {
+  private final String name;
+  private final String description;
+  private final String unit;
+
+  public GitRepoMetric(String name, String description, String unit) {
+    this.name = name;
+    this.description = description;
+    this.unit = unit;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public String getUnit() {
+    return unit;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitStats.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitStats.java
new file mode 100644
index 0000000..9e2150d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitStats.java
@@ -0,0 +1,90 @@
+// 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.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.googlesource.gerrit.plugins.gitrepometrics.GitRepoMetricsCacheModule;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.internal.storage.file.GC;
+
+// TODO Add an interface
+// TODO implement multiple collectors
+public class GitStats {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final FileRepository repository;
+  private final Project p;
+
+  public static String numberOfPackedObjects = "numberOfPackedObjects";
+  public static String numberOfPackFiles = "numberOfPackFiles";
+  public static String numberOfLooseObjects = "numberOfLooseObjects";
+  public static String numberOfLooseRefs = "numberOfLooseRefs";
+  public static String numberOfPackedRefs = "numberOfPackedRefs";
+  public static String sizeOfLooseObjects = "sizeOfLooseObjects";
+  public static String sizeOfPackedObjects = "sizeOfPackedObjects";
+  public static String numberOfBitmaps = "numberOfBitmaps";
+
+  public GitStats(FileRepository repository, Project project) {
+    this.repository = repository;
+    this.p = project;
+  }
+
+  public Map<String, Long> get() {
+    Map<String, Long> metrics = new java.util.HashMap<>(Collections.emptyMap());
+    try {
+      GC.RepoStatistics statistics = new GC(repository).getStatistics();
+      putMetric(metrics, numberOfPackedObjects, s -> s.numberOfPackedObjects, statistics);
+      putMetric(metrics, numberOfPackFiles, s -> s.numberOfPackFiles, statistics);
+      putMetric(metrics, numberOfLooseObjects, s -> s.numberOfLooseObjects, statistics);
+      putMetric(metrics, numberOfLooseRefs, s -> s.numberOfLooseRefs, statistics);
+      putMetric(metrics, numberOfPackedRefs, s -> s.numberOfPackedRefs, statistics);
+      putMetric(metrics, sizeOfLooseObjects, s -> s.sizeOfLooseObjects, statistics);
+      putMetric(metrics, sizeOfPackedObjects, s -> s.sizeOfPackedObjects, statistics);
+      putMetric(metrics, numberOfBitmaps, s -> s.numberOfBitmaps, statistics);
+      logger.atInfo().log("New Git Statistics metrics collected: %s", statistics.toString());
+    } catch (IOException e) {
+      logger.atSevere().log("Something went wrong: %s", e.getMessage());
+    }
+    return metrics;
+  }
+
+  public static List<GitRepoMetric> availableMetrics() {
+    return Arrays.asList(
+        new GitRepoMetric(numberOfPackedObjects, "Number of packed objects", "Count"),
+        new GitRepoMetric(numberOfPackFiles, "Number of pack files", "Count"),
+        new GitRepoMetric(numberOfLooseObjects, "Number of loose objects", "Count"),
+        new GitRepoMetric(numberOfLooseRefs, "Number of loose refs", "Count"),
+        new GitRepoMetric(numberOfPackedRefs, "Number of packed refs", "Count"),
+        new GitRepoMetric(sizeOfLooseObjects, "Size of loose objects", "Count"),
+        new GitRepoMetric(sizeOfPackedObjects, "Size of packed objects", "Count"),
+        new GitRepoMetric(numberOfBitmaps, "Number of bitmaps", "Count"));
+  }
+
+  private void putMetric(
+      Map<String, Long> metrics,
+      String metricName,
+      Function<GC.RepoStatistics, Long> fn,
+      GC.RepoStatistics statistics) {
+    metrics.put(
+        GitRepoMetricsCacheModule.getMetricName(metricName, p.getName()), fn.apply(statistics));
+  }
+}
diff --git a/src/resources/Documentation/config.md b/src/resources/Documentation/config.md
new file mode 100644
index 0000000..3ba47a9
--- /dev/null
+++ b/src/resources/Documentation/config.md
@@ -0,0 +1,33 @@
+@PLUGIN@ configuration
+======================
+
+The @PLUGIN@ allows a systematic collection of repository metrics.
+Metrics are updated upon a `ref-update` receive.
+
+Currently, the metrics exposed are the following:
+
+```bash
+plugins_git_repo_metrics_numberofbitmaps_<repo_name>
+plugins_git_repo_metrics_numberoflooseobjects_<repo_name>
+plugins_git_repo_metrics_numberoflooserefs_<repo_name>
+plugins_git_repo_metrics_numberofpackedobjects_<repo_name>
+plugins_git_repo_metrics_numberofpackedrefs_<repo_name>
+plugins_git_repo_metrics_numberofpackfiles_<repo_name>
+plugins_git_repo_metrics_sizeoflooseobjects_<repo_name>
+plugins_git_repo_metrics_sizeofpackedobjects_<repo_name>
+```
+
+Settings
+--------
+
+The plugin allows to customize its behaviour through a specific
+`git-repo-metrics.config` file in the `$GERRIT_SITE/etc` directory.
+
+The metrics are not collected for all the projects, otherwise there might be an explosion of metrics
+exported, but only the one listed in the configuration file, i.e.:
+
+```
+[git-repo-metrics]
+  project = test-project
+  project = another-repo
+```
\ No newline at end of file
diff --git a/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheModuleTest.java b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheModuleTest.java
new file mode 100644
index 0000000..ee6e5f1
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheModuleTest.java
@@ -0,0 +1,74 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import com.google.gerrit.metrics.CallbackMetric0;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.googlesource.gerrit.plugins.gitrepometrics.collectors.GitStats;
+import java.util.Collections;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class GitRepoMetricsCacheModuleTest {
+
+  PluginConfigFactory pluginConfigFactory = mock(PluginConfigFactory.class);
+
+  GitRepoMetricsCacheModule gitRepoMetricsCacheModule;
+  GitRepoMetricsConfig gitRepoMetricsConfig;
+  FakeMetricMaker fakeMetricMaker;
+
+  @Test
+  public void shouldRegisterMetrics() {
+    Config c = new Config();
+    c.setStringList("git-repo-metrics", null, "project", Collections.singletonList("repo1"));
+    doReturn(c).when(pluginConfigFactory).getGlobalPluginConfig(any());
+    gitRepoMetricsConfig = new GitRepoMetricsConfig(pluginConfigFactory, "git-repo-metrics");
+    fakeMetricMaker = new FakeMetricMaker();
+    gitRepoMetricsCacheModule =
+        new GitRepoMetricsCacheModule(fakeMetricMaker, gitRepoMetricsConfig);
+    gitRepoMetricsCacheModule.initCache();
+    assertThat(fakeMetricMaker.callsCounter).isEqualTo(GitStats.availableMetrics().size());
+  }
+
+  private class FakeMetricMaker extends DisabledMetricMaker {
+    Integer callsCounter;
+
+    FakeMetricMaker() {
+      callsCounter = 0;
+    }
+
+    @Override
+    public <V> CallbackMetric0<V> newCallbackMetric(
+        String name, Class<V> valueClass, Description desc) {
+
+      callsCounter += 1;
+      return new CallbackMetric0<V>() {
+
+        @Override
+        public void set(V value) {}
+
+        @Override
+        public void remove() {}
+      };
+    }
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitUpdateListenerTest.java b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitUpdateListenerTest.java
new file mode 100644
index 0000000..8e7f2e3
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitUpdateListenerTest.java
@@ -0,0 +1,122 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class GitUpdateListenerTest {
+
+  private final GitRepositoryManager repoManager = new InMemoryRepositoryManager();
+  private final ExecutorService mockedExecutorService = mock(ExecutorService.class);
+  private GitRepoUpdateListener gitRepoUpdateListener;
+  private final String testProject = "testProject";
+  private final Project.NameKey testProjectNameKey = Project.nameKey("testProject");
+  private Repository repository;
+  ArgumentCaptor<UpdateGitMetricsTask> valueCapture =
+      ArgumentCaptor.forClass(UpdateGitMetricsTask.class);
+
+  @Before
+  public void setupRepo() throws IOException {
+    reset(mockedExecutorService);
+    gitRepoUpdateListener = new GitRepoUpdateListener(repoManager, mockedExecutorService);
+    repository = repoManager.createRepository(testProjectNameKey);
+  }
+
+  @Test
+  public void shouldUpdateMetrics() {
+    doNothing().when(mockedExecutorService).execute(valueCapture.capture());
+    gitRepoUpdateListener.onGitReferenceUpdated(new TestEvent(testProject));
+    UpdateGitMetricsTask expectedUpdateGitMetricsTask =
+        new UpdateGitMetricsTask(repository, Project.builder(testProjectNameKey).build());
+    assertThat(valueCapture.getValue().toString())
+        .isEqualTo(expectedUpdateGitMetricsTask.toString());
+  }
+
+  @Test
+  public void shouldNotUpdateMetricsWhenRepoDoesntExists() {
+    gitRepoUpdateListener.onGitReferenceUpdated(new TestEvent("InvalidProject"));
+    verifyNoInteractions(mockedExecutorService);
+  }
+
+  public static class TestEvent implements GitReferenceUpdatedListener.Event {
+    private final String projectName;
+
+    protected TestEvent(String projectName) {
+      this.projectName = projectName;
+    }
+
+    @Override
+    public String getProjectName() {
+      return projectName;
+    }
+
+    @Override
+    public NotifyHandling getNotify() {
+      return null;
+    }
+
+    @Override
+    public String getRefName() {
+      return null;
+    }
+
+    @Override
+    public String getOldObjectId() {
+      return null;
+    }
+
+    @Override
+    public String getNewObjectId() {
+      return null;
+    }
+
+    @Override
+    public boolean isCreate() {
+      return false;
+    }
+
+    @Override
+    public boolean isDelete() {
+      return false;
+    }
+
+    @Override
+    public boolean isNonFastForward() {
+      return false;
+    }
+
+    @Override
+    public AccountInfo getUpdater() {
+      return null;
+    }
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsTaskTest.java b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsTaskTest.java
new file mode 100644
index 0000000..2a8aeb9
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsTaskTest.java
@@ -0,0 +1,56 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.gitrepometrics.GitRepoMetricsCacheModule.metrics;
+import static java.nio.file.Files.delete;
+
+import com.google.gerrit.entities.Project;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+
+public class UpdateGitMetricsTaskTest {
+
+  private final String projectName = "testProject";
+  private final Project.NameKey projectNameKey = Project.nameKey(projectName);
+  private Repository testRepository;
+  private Project testProject;
+
+  @Before
+  public void setupRepository() throws Exception {
+    Path p = Files.createTempDirectory("git_repo_metrics_");
+    try {
+      testRepository = new FileRepository(p.toFile());
+      testRepository.create(true);
+    } catch (Exception e) {
+      delete(p);
+      throw e;
+    }
+    testProject = Project.builder(projectNameKey).build();
+  }
+
+  @Test
+  public void shouldUpdateMetrics() {
+    UpdateGitMetricsTask updateGitMetricsTask =
+        new UpdateGitMetricsTask(testRepository, testProject);
+    updateGitMetricsTask.run();
+    assertThat(metrics.keySet()).isNotEmpty();
+  }
+}