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();
+ }
+}