Add ssh command "test-submit-rule"

The command creates a fresh prolog environment and loads a prolog script
either from stdin or rules.pl in the project refs/meta/config branch
depending on the options used.
can_submit/1 is then queried and the results are returned in human
readable text or json.

Change-Id: I246a89f9f35718320b69ec43ddf11fbe002ed566
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 3895b90..ccd9ffc 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -147,6 +147,9 @@
 link:cmd-plugin-remove.html[gerrit plugin rm]::
     Alias for 'gerrit plugin remove'.
 
+link:cmd-test-submit-rule.html[gerrit test-submit-rule]::
+	Test prolog submit rules.
+
 link:cmd-kill.html[kill]::
 	Kills a scheduled or running task.
 
diff --git a/Documentation/cmd-test-submit-rule.txt b/Documentation/cmd-test-submit-rule.txt
new file mode 100644
index 0000000..5b70bd1
--- /dev/null
+++ b/Documentation/cmd-test-submit-rule.txt
@@ -0,0 +1,93 @@
+gerrit test-submit-rule
+=======================
+
+NAME
+----
+gerrit test-submit-rule - Test prolog submit rules with a chosen changeset.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit test-submit-rule'
+  [-s]
+  [--no-filters]
+  [--format {TEXT | JSON}]
+  CHANGE
+
+DESCRIPTION
+-----------
+Provides a way to test prolog link:prolog-cookbook.html[submit rules].
+
+OPTIONS
+-------
+-s::
+	Reads a rules.pl file from stdin instead of rules.pl in refs/meta/config.
+
+--no-filters::
+	Don't run the submit_filter/2 from the parent projects of the specified change.
+
+--format::
+  What output format to display the results in.
++
+--
+`text`:: Simple text based format.
+`json`:: A JSON object described in link:json.html#submitRecord[submit record].
+`json_compact`:: Minimized JSON output.
+--
+
+ACCESS
+------
+Can be used by anyone that has permission to read the specified changeset.
+
+EXAMPLES
+--------
+
+
+Test submit_rule from stdin.
+====
+ $ cat non-author-codereview.pl | ssh -p 29418 review.example.com gerrit test-submit-rule -s I78f2c6673db24e4e92ed32f604c960dc952437d9
+ Non-Author-Code-Review: NOT_READY
+ Verified: NOT_READY
+ Code-Review: NOT_READY by Anonymous Coward <test@email.com>
+
+ NOT_READY
+====
+
+Test submit_rule from stdin and return the results as JSON.
+====
+ cat non-author-codereview.pl | ssh -p 29418 review.example.com gerrit test-submit-rule --format=JSON -s I78f2c6673db24e4e92ed32f604c960dc952437d9
+ {
+  "approvals": [
+    {
+      "type": "Verified",
+      "value": "NEED"
+    },
+    {
+      "type": "Code-Review",
+      "value": "OK",
+      "by": {
+        "email": "test@email.com",
+        "username": "test"
+      }
+    }
+  ],
+  "value": "NOT_READY"
+ }
+====
+
+Test the active submit_rule from the refs/meta/config branch, ignoring filters in the project parents.
+====
+ $ ssh -p 29418 review.example.com gerrit test-submit-rule I78f2c6673db24e4e92ed32f604c960dc952437d9 --no-filters
+ Verified: NOT_READY
+ Code-Review: NOT_READY by Anonymous Coward <test@email.com>
+
+ NOT_READY
+====
+
+SCRIPTING
+---------
+Can be used either interactively for testing new prolog submit rules, or from a script to check the submit status of a change.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 0863623..db4b021 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -570,7 +570,7 @@
         && who.arg(0).isInteger();
   }
 
-  private static Term toListTerm(List<Term> terms) {
+  public static Term toListTerm(List<Term> terms) {
     Term list = Prolog.Nil;
     for (int i = terms.size() - 1; i >= 0; i--) {
       list = new ListTerm(terms.get(i), list);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
index db4e985..90bc07e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
@@ -31,6 +31,7 @@
     command(gerrit, "rename-group").to(RenameGroupCommand.class);
     command(gerrit, "create-project").to(CreateProjectCommand.class);
     command(gerrit, "gsql").to(AdminQueryShell.class);
+    command(gerrit, "test-submit-rule").to(TestSubmitRule.class);
     command(gerrit, "set-reviewers").to(SetReviewersCommand.class);
     command(gerrit, "receive-pack").to(Receive.class);
     command(gerrit, "set-project-parent").to(AdminSetParent.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java
new file mode 100644
index 0000000..9a81140
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java
@@ -0,0 +1,240 @@
+// Copyright (C) 2012 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.google.gerrit.sshd.commands;
+
+import com.google.gerrit.common.data.AccountInfo;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.events.AccountAttribute;
+import com.google.gerrit.server.events.SubmitLabelAttribute;
+import com.google.gerrit.server.events.SubmitRecordAttribute;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
+import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
+import com.googlecode.prolog_cafe.lang.ListTerm;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.PrologClassLoader;
+import com.googlecode.prolog_cafe.lang.PrologException;
+import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+import com.googlecode.prolog_cafe.lang.VariableTerm;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/** Command that allows testing of prolog submit-rules in a live instance. */
+final class TestSubmitRule extends SshCommand {
+  @Inject
+  private ReviewDb db;
+
+  @Inject
+  private PrologEnvironment.Factory envFactory;
+
+  @Inject
+  private ChangeControl.Factory ccFactory;
+
+  @Inject
+  private AccountCache accountCache;
+
+  final @AnonymousCowardName String anonymousCowardName;
+
+  @Argument(index = 0, required = true, usage = "ChangeId to load in prolog environment")
+  private String changeId;
+
+  @Option(name = "-s",
+      usage = "Read prolog script from stdin instead of reading rules.pl from the refs/meta/config branch")
+  private boolean useStdin;
+
+  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+  private OutputFormat format = OutputFormat.TEXT;
+
+  @Option(name = "--no-filters", aliases = {"-n"},
+      usage = "Don't run the submit_filter/2 from the parent projects")
+  private boolean skipSubmitFilters;
+
+  private static final String[] PACKAGE_LIST = {Prolog.BUILTIN, "gerrit"};
+
+  @Inject
+  public TestSubmitRule(@AnonymousCowardName String anonymous) {
+    anonymousCowardName = anonymous;
+  }
+  private PrologMachineCopy newMachine() {
+    BufferingPrologControl ctl = new BufferingPrologControl();
+    ctl.setMaxDatabaseSize(16 * 1024);
+    ctl.setPrologClassLoader(new PrologClassLoader(getClass().getClassLoader()));
+    return PrologMachineCopy.save(ctl);
+  }
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    InputStreamReader inReader = new InputStreamReader(in);
+
+    try {
+      PrologEnvironment pcl;
+
+      List<Change> changeList =
+          db.changes().byKey(new Change.Key(changeId)).toList();
+      if (changeList.size() != 1)
+        throw new UnloggedFailure(1, "Invalid ChangeId");
+
+      Change c = changeList.get(0);
+      PatchSet ps = db.patchSets().get(c.currentPatchSetId());
+      // Will throw exception if current user can not access this change, and
+      // thus will leak information that a change-id is valid even though the
+      // user are not allowed to see the change.
+      // See http://code.google.com/p/gerrit/issues/detail?id=1586
+      ChangeControl cc = ccFactory.controlFor(c);
+      ProjectState projectState = cc.getProjectControl().getProjectState();
+
+      if (useStdin) {
+        pcl = envFactory.create(newMachine());
+      } else {
+        pcl = projectState.newPrologEnvironment();
+      }
+
+      pcl.set(StoredValues.REVIEW_DB, db);
+      pcl.set(StoredValues.CHANGE, c);
+      pcl.set(StoredValues.PATCH_SET, ps);
+      pcl.set(StoredValues.CHANGE_CONTROL, cc);
+      if (useStdin) {
+        pcl.initialize(PACKAGE_LIST);
+        pcl.execute(Prolog.BUILTIN, "consult_stream",
+            SymbolTerm.intern("stdin"), new JavaObjectTerm(inReader));
+      }
+
+      List<Term> results = new ArrayList<Term>();
+      Term submitRule =
+          pcl.once("gerrit", "locate_submit_rule", new VariableTerm());
+
+      for (Term[] template : pcl.all("gerrit", "can_submit", submitRule,
+          new VariableTerm())) {
+        results.add(template[1]);
+      }
+
+      if (!skipSubmitFilters) {
+        runSubmitFilters(projectState, results, pcl);
+      }
+
+      List<SubmitRecord> res = cc.resultsToSubmitRecord(submitRule, results);
+      for (SubmitRecord r : res) {
+        if (format.isJson()) {
+          SubmitRecordAttribute submitRecord = new SubmitRecordAttribute();
+          submitRecord.status = r.status.name();
+
+          List<SubmitLabelAttribute> submitLabels = new LinkedList<SubmitLabelAttribute>();
+          for(SubmitRecord.Label l : r.labels) {
+            SubmitLabelAttribute label = new SubmitLabelAttribute();
+            label.label = l.label;
+            label.status= l.status.name();
+            if(l.appliedBy != null) {
+              Account a = accountCache.get(l.appliedBy).getAccount();
+              label.by = new AccountAttribute();
+              label.by.email = a.getPreferredEmail();
+              label.by.name = a.getFullName();
+              label.by.username = a.getUserName();
+            }
+            submitLabels.add(label);
+          }
+          submitRecord.labels = submitLabels;
+          format.newGson().toJson(submitRecord, new TypeToken<SubmitRecordAttribute>() {}.getType(), stdout);
+          stdout.print('\n');
+        } else {
+          for(SubmitRecord.Label l : r.labels) {
+            stdout.print(l.label + ": " + l.status);
+            if(l.appliedBy != null) {
+              AccountInfo a = new AccountInfo(accountCache.get(l.appliedBy).getAccount());
+              stdout.print(" by " + a.getNameEmail(anonymousCowardName));
+            }
+            stdout.print('\n');
+          }
+          stdout.print("\n" + r.status.name() + "\n");
+        }
+      }
+    } catch (Exception e) {
+      throw new UnloggedFailure("Processing of prolog script failed: " + e);
+    }
+  }
+
+  private void runSubmitFilters(ProjectState projectState, List<Term> results,
+      PrologEnvironment pcl) throws UnloggedFailure {
+    ProjectState parentState = projectState.getParentState();
+    PrologEnvironment childEnv = pcl;
+    Set<Project.NameKey> projectsSeen = new HashSet<Project.NameKey>();
+    projectsSeen.add(projectState.getProject().getNameKey());
+
+    while (parentState != null) {
+      if (!projectsSeen.add(parentState.getProject().getNameKey())) {
+        // parent has been seen before, stop walk up inheritance tree
+        break;
+      }
+      PrologEnvironment parentEnv;
+      try {
+        parentEnv = parentState.newPrologEnvironment();
+      } catch (CompileException err) {
+        throw new UnloggedFailure("Cannot consult rules.pl for "
+            + parentState.getProject().getName() + err);
+      }
+
+      parentEnv.copyStoredValues(childEnv);
+      Term filterRule =
+          parentEnv.once("gerrit", "locate_submit_filter", new VariableTerm());
+      if (filterRule != null) {
+        try {
+          Term resultsTerm = ChangeControl.toListTerm(results);
+          results.clear();
+          Term[] template =
+              parentEnv.once("gerrit", "filter_submit_results", filterRule,
+                  resultsTerm, new VariableTerm());
+          @SuppressWarnings("unchecked")
+          final List<? extends Term> termList =
+              ((ListTerm) template[2]).toJava();
+          results.addAll(termList);
+        } catch (PrologException err) {
+          throw new UnloggedFailure("Exception calling " + filterRule + " of "
+              + parentState.getProject().getName() + err);
+        } catch (RuntimeException err) {
+          throw new UnloggedFailure("Exception calling " + filterRule + " of "
+              + parentState.getProject().getName() + err);
+        }
+      }
+
+      parentState = parentState.getParentState();
+      childEnv = parentEnv;
+    }
+  }
+}