Merge "Add metric for sum of SHA1s"
diff --git a/multi-primary/localrefdb.groovy b/multi-primary/localrefdb.groovy
index b5d1632..fb73f89 100644
--- a/multi-primary/localrefdb.groovy
+++ b/multi-primary/localrefdb.groovy
@@ -29,15 +29,19 @@
 import java.util.concurrent.*
 import com.google.common.flogger.*
 import java.util.regex.Pattern
+import java.security.MessageDigest
 
 abstract class BaseSshCommand extends SshCommand {
-  void println(String msg) {
-    stdout.println msg
+  void println(String msg, boolean verbose = false) {
+    if (verbose) {
+      stdout.println msg
+    }
     stdout.flush()
   }
 
-  void error(String msg) { println("[ERROR] $msg") }
-  void warning(String msg) { println("[WARNING] $msg") }
+  void error(String msg) { println("[ERROR] $msg", true) }
+
+  void warning(String msg) { println("[WARNING] $msg", true) }
 }
 
 @Singleton
@@ -49,32 +53,51 @@
 
   CallbackMetric1<String, Integer> numRefsMetric
   final Map<String, Integer> projectsAndNumRefs = new ConcurrentHashMap()
+  CallbackMetric1<String, Integer> sha1AllRefsMetric
+  final Map<String, Integer> projectsAndSha1AllRefs = new ConcurrentHashMap()
 
-  void start() {
-    numRefsMetric =
-        metrics.newCallbackMetric(
-            "localrefdb/num_refs_per_project",
-            Integer.class,
-            new Description("Number of local refs").setGauge(),
-            Field.ofString("repository_name", { it.projectName } as BiConsumer)
-                .description("The name of the repository.")
-                .build())
-
+  void setupTrigger(CallbackMetric1<String, Integer> refsMetric, Map<String, Integer> projecsAndRefs) {
     metrics.newTrigger(
-        numRefsMetric, { ->
-      if (projectsAndNumRefs.isEmpty()) {
-        numRefsMetric.forceCreate("")
+        refsMetric, { ->
+      if (projecsAndRefs.isEmpty()) {
+        refsMetric.forceCreate("")
       } else {
-        projectsAndNumRefs.each { e ->
-          numRefsMetric.set(e.key, e.value)
+        projecsAndRefs.each { e ->
+          refsMetric.set(e.key, e.value)
         }
-        numRefsMetric.prune()
+        refsMetric.prune()
       }
     })
   }
 
+  CallbackMetric1<String, Integer> createCallbackMetric(String name, String description) {
+    metrics.newCallbackMetric(
+        name,
+        Integer.class,
+        new Description(description).setGauge(),
+        Field.ofString("repository_name", { it.projectName } as BiConsumer)
+            .description(description)
+            .build())
+  }
+
+  void start() {
+    numRefsMetric = createCallbackMetric("localrefdb/num_refs_per_project", "Number of local refs")
+
+    setupTrigger(numRefsMetric, projectsAndNumRefs)
+
+
+    sha1AllRefsMetric = createCallbackMetric("localrefdb/sha1_all_refs_per_project", "A SHA1 computed from combining all SHA1s of the repository.")
+
+    setupTrigger(sha1AllRefsMetric, projectsAndSha1AllRefs)
+  }
+
+  def listRefs(Repository repo) {
+    repo.refDatabase.refs.findAll { ref -> !(ref.name.startsWith("refs/users/.*")) && !ref.symbolic }
+  }
+
   void stop() {
     numRefsMetric.remove()
+    sha1AllRefsMetric.remove()
   }
 }
 
@@ -85,6 +108,9 @@
   @Argument(index = 0, usage = "Project name", metaVar = "PROJECTS", required = true)
   String[] projects
 
+  @Option(name = "--verbose", usage = "Display verbose logging")
+  boolean verbose = false
+
   @Inject
   GitRepositoryManager repoMgr
 
@@ -98,11 +124,10 @@
     projects.each { project ->
       try {
         def projectName = Project.nameKey(project)
-
         repoMgr.openRepository(projectName).with { repo ->
-          println "Counting refs of project $project ..."
-          def filteredRefs = repo.refDatabase.refs.findAll { ref -> !(ref.name.startsWith("refs/users/.*")) && !ref.symbolic }
-          println "Result: $project has ${filteredRefs.size()} refs"
+          println("Counting refs of project $project ...", verbose)
+          def filteredRefs = refdbMetrics.listRefs(repo)
+          println("Result: $project has ${filteredRefs.size()} refs", true)
           refdbMetrics.projectsAndNumRefs.put(sanitizeProjectName.sanitize(project), filteredRefs.size())
         }
       } catch (RepositoryNotFoundException e) {
@@ -112,6 +137,60 @@
   }
 }
 
+@CommandMetaData(name = "sha1-all-refs", description = "Combine all the local refs, excluding user edits, and publish the value as 'sha1_all_refs_per_project' metric")
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+class Sha1AllRefs extends BaseSshCommand {
+
+  @Argument(index = 0, usage = "Project names", metaVar = "PROJECT", required = true)
+  String[] projects
+
+  @Option(name = "--verbose", usage = "Display verbose output")
+  boolean verbose = false
+
+  @Inject
+  GitRepositoryManager repoMgr
+
+  @Inject
+  RefDbMetrics refdbMetrics
+
+  SanitizeProjectName sanitizeProjectName = new SanitizeProjectName()
+
+  public void run() {
+    refdbMetrics.projectsAndSha1AllRefs.clear()
+    projects.each { project ->
+      try {
+        def projectName = Project.nameKey(project)
+
+        repoMgr.openRepository(projectName).with { repo ->
+          def startTime = System.currentTimeMillis()
+          println("Adding refs of project $project ...", verbose)
+          def filteredRefs = refdbMetrics.listRefs(repo)
+          println("Result: $project has ${filteredRefs.size()} refs", verbose)
+          def md = MessageDigest.getInstance("SHA-1")
+          def sortingStartTime = System.currentTimeMillis()
+          def sortedFilteredRefs = filteredRefs.sort { it.name }
+          println("Sorting refs took ${System.currentTimeMillis() - sortingStartTime} millis", verbose)
+          sortedFilteredRefs.each { ref -> md.update(ref.getObjectId().toString().getBytes("UTF-8")) }
+          def sha1SumBytes = md.digest()
+          println("MD Digest of sum of all SHA1 for project $project is: ${sha1SumBytes.encodeBase64().toString()}", true)
+          def sha1Sum = truncateHashToInt(sha1SumBytes)
+          println("Truncated Int representation of sum of all SHA1 for project $project is: $sha1Sum", verbose)
+          println("Whole operation too ${System.currentTimeMillis() - startTime} millis", verbose)
+          refdbMetrics.projectsAndSha1AllRefs.put(sanitizeProjectName.sanitize(project), sha1Sum)
+        }
+      } catch (RepositoryNotFoundException e) {
+        error "Project $project not found"
+      }
+    }
+  }
+
+  // Source http://www.java2s.com/example/java-utility-method/sha1/sha1hashint-string-text-d6c0e.html
+  static int truncateHashToInt(byte[] bytes) {
+    int offset = bytes[bytes.length - 1] & 0x0f;
+    return (bytes[offset] & (0x7f << 24)) | (bytes[offset + 1] & (0xff << 16)) | (bytes[offset + 2] & (0xff << 8)) | (bytes[offset + 3] & 0xff);
+  }
+}
+
 class MetricsModule extends LifecycleModule {
   protected void configure() {
     listener().to(RefDbMetrics)
@@ -121,6 +200,7 @@
 class LocalRefDbCommandModule extends PluginCommandModule {
   protected void configureCommands() {
     command(CountRefs)
+    command(Sha1AllRefs)
   }
 }