Add SSH command for scanning projects

Add a SSH command to scan projects and individual branches on demand.

Change-Id: Id2b6b34942c12098b44cb56f70f5e1255c7b46ea
diff --git a/BUCK b/BUCK
index d894755..b521297 100644
--- a/BUCK
+++ b/BUCK
@@ -23,6 +23,7 @@
   manifest_entries = [
     'Gerrit-PluginName: repository-usage',
     'Gerrit-Module: com.googlesource.gerrit.plugins.repositoryuse.Module',
+    'Gerrit-SshModule: com.googlesource.gerrit.plugins.repositoryuse.SshModule',
   ],
   deps = DEPS,
   provided_deps = PROVIDED_DEPS,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java
index 1a97a63..1ca0697 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java
@@ -19,10 +19,13 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
+import com.google.inject.Scopes;
 import com.google.inject.Singleton;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
 import com.google.inject.internal.UniqueAnnotations;
 
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
 public class Module extends AbstractModule {
   @Override
   protected void configure() {
@@ -34,6 +37,12 @@
     install(new FactoryModuleBuilder()
         .implement(RefUpdateHandler.class, RefUpdateHandlerImpl.class)
         .build(RefUpdateHandlerFactory.class));
+    install(
+        new FactoryModuleBuilder().implement(ScanTask.class, ScanTaskImpl.class)
+            .build(ScanTaskFactory.class));
+    bind(ScanningQueue.class).in(Scopes.SINGLETON);
+    bind(LifecycleListener.class).annotatedWith(UniqueAnnotations.create())
+        .to(ScanningQueue.class);
     bind(LifecycleListener.class).annotatedWith(UniqueAnnotations.create())
         .to(SQLDriver.class);
   }
@@ -43,4 +52,10 @@
   SQLDriver provideSqlDriver() {
     return new SQLDriver();
   }
+
+  @Provides
+  @ScanningPool
+  ScheduledThreadPoolExecutor provideScanningPool(ScanningQueue queue) {
+    return queue.getPool();
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanCommand.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanCommand.java
new file mode 100644
index 0000000..c4f337c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanCommand.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2015 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.repositoryuse;
+
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.extensions.annotations.CapabilityScope;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+@RequiresCapability(value = "administrateServer", scope = CapabilityScope.CORE)
+@CommandMetaData(name = "scan", description = "Scan specific projects or branches")
+final class ScanCommand extends SshCommand {
+  @Option(name = "--all", usage = "push all known projects")
+  private boolean all;
+
+  @Option(name = "--branch", metaVar = "BRANCH", usage = "branches to scan")
+  private String[] branches;
+
+  @Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project name pattern")
+  private List<String> projects = new ArrayList<>(2);
+
+  private final ScanTaskFactory scanTaskFactory;
+  private final ScheduledThreadPoolExecutor pool;
+  private final ProjectCache projectCache;
+
+
+  @Inject
+  public ScanCommand(ScanTaskFactory scanTaskFactory,
+      @ScanningPool ScheduledThreadPoolExecutor pool,
+      ProjectCache projectCache) {
+    this.scanTaskFactory = scanTaskFactory;
+    this.pool = pool;
+    this.projectCache = projectCache;
+  }
+
+  @Override
+  protected void run() throws UnloggedFailure, Failure, Exception {
+    if (all && projects.size() > 0) {
+      throw new UnloggedFailure(1, "error: cannot combine --all and PROJECT");
+    }
+
+    if (all) {
+      for (NameKey project : projectCache.all()) {
+        projects.add(project.get());
+      }
+    }
+
+    for (String project : projects) {
+      if (branches == null || branches.length == 0) {
+        pool.execute(scanTaskFactory.create(project));
+      } else {
+        for (String branch : branches) {
+          pool.execute(scanTaskFactory.create(project, branch));
+        }
+      }
+    }
+  }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanTask.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanTask.java
new file mode 100644
index 0000000..52695eb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanTask.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2015 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.repositoryuse;
+
+public interface ScanTask extends Runnable {
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanTaskFactory.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanTaskFactory.java
new file mode 100644
index 0000000..5731a7d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanTaskFactory.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2015 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.repositoryuse;
+
+import com.google.inject.assistedinject.Assisted;
+
+public interface ScanTaskFactory {
+  public ScanTask create(String project);
+
+  public ScanTask create(@Assisted("project") String project,
+      @Assisted("branch") String branch);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanTaskImpl.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanTaskImpl.java
new file mode 100644
index 0000000..8f7d251
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanTaskImpl.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2015 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.repositoryuse;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class ScanTaskImpl implements ScanTask {
+  private static final Logger log =
+      LoggerFactory.getLogger(RefUpdateHandlerImpl.class);
+
+  private String project;
+  private String branch;
+  private RefUpdateHandlerFactory refUpdateHandlerFactory;
+  private GitRepositoryManager repoManager;
+
+  @AssistedInject
+  public ScanTaskImpl(@Assisted String project,
+      RefUpdateHandlerFactory refUpdateHandlerFactory,
+      GitRepositoryManager repoManager) {
+    init(project, null, refUpdateHandlerFactory, repoManager);
+  }
+
+  @AssistedInject
+  public ScanTaskImpl(@Assisted("project") String project,
+      @Assisted("branch") String branch,
+      RefUpdateHandlerFactory refUpdateHandlerFactory,
+      GitRepositoryManager repoManager) {
+    init(project, branch, refUpdateHandlerFactory, repoManager);
+  }
+
+  private void init(String project, String branch,
+      RefUpdateHandlerFactory refUpdateHandlerFactory,
+      GitRepositoryManager repoManager) {
+    this.project = project;
+    this.branch = branch;
+    this.refUpdateHandlerFactory = refUpdateHandlerFactory;
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public String toString() {
+    if (branch != null) {
+      return String.format("(repository-usage) scan %s branch %s", project,
+          branch);
+    }
+    return String.format("(repository-usage) scan %s", project);
+  }
+
+  @Override
+  public void run() {
+    Map<String, org.eclipse.jgit.lib.Ref> branches = null;
+    Project.NameKey nameKey = new Project.NameKey(project);
+    try {
+      try (Repository repo = repoManager.openRepository(nameKey)) {
+        branches = repo.getRefDatabase().getRefs(Constants.R_HEADS);
+      }
+    } catch (IOException e) {
+      log.error(e.getMessage(), e);
+    }
+    if (branch == null) {
+      for (String currentBranch : branches.keySet()) {
+        // Create with a "new" base commit to rescan entire branch
+        RefUpdate rescan = new RefUpdate(project,
+            branches.get(currentBranch).getName(), ObjectId.zeroId().getName(),
+            branches.get(currentBranch).getObjectId().name());
+        refUpdateHandlerFactory.create(rescan).run();
+      }
+    } else {
+      if (branch.startsWith(Constants.R_HEADS)) {
+        branch = branch.substring(Constants.R_HEADS.length());
+      }
+
+      if (branches.containsKey(branch)) {
+        RefUpdate rescan = new RefUpdate(project,
+            branches.get(branch).getName(), ObjectId.zeroId().getName(),
+            branches.get(branch).getObjectId().name());
+        refUpdateHandlerFactory.create(rescan).run();
+      } else {
+        log.warn(String.format("Branch %s does not exist; skipping", branch));
+      }
+    }
+  }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanningPool.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanningPool.java
new file mode 100644
index 0000000..69221cc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanningPool.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2015 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.repositoryuse;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface ScanningPool {
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanningQueue.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanningQueue.java
new file mode 100644
index 0000000..8cbc3bd
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ScanningQueue.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2015 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.repositoryuse;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+public class ScanningQueue implements LifecycleListener {
+  private final WorkQueue queue;
+  private WorkQueue.Executor threadPool;
+
+  @Inject
+  public ScanningQueue(WorkQueue queue) {
+    this.queue = queue;
+  }
+
+  @Override
+  public void start() {
+    threadPool = queue.createQueue(1, "(Repository-Usage)");
+  }
+
+  @Override
+  public void stop() {
+    if (threadPool != null) {
+      threadPool.unregisterWorkQueue();
+      threadPool = null;
+    }
+  }
+
+  public ScheduledThreadPoolExecutor getPool() {
+    return threadPool;
+  }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/SshModule.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/SshModule.java
new file mode 100644
index 0000000..fb57e95
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/SshModule.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2015 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.repositoryuse;
+
+import com.google.gerrit.sshd.PluginCommandModule;
+
+public class SshModule extends PluginCommandModule {
+
+  @Override
+  protected void configureCommands() {
+    command(ScanCommand.class);
+  }
+
+}