Update approvals in web UI to adapt to rules.pl submit_rule

The UI now shows whatever the results of the submit_rule are, which
permits the submit_rule to make an ApprovalCategory optional, or to
make a new label required. Currently making a new label required is
also going to make the change unsubmittable, as we do not yet have
a way to store the new label.

Change-Id: I9c6600c181e9f22ff980539d535caa8458d9a654
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalDetail.java
index 9834693..318c60b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalDetail.java
@@ -21,9 +21,9 @@
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Comparator;
-import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
+import java.util.Set;
 
 public class ApprovalDetail {
   public static final Comparator<ApprovalDetail> SORT =
@@ -43,6 +43,8 @@
   protected List<PatchSetApproval> approvals;
   protected boolean canRemove;
 
+  private transient Set<String> approved;
+  private transient Set<String> rejected;
   private transient int hasNonZero;
   private transient Timestamp sortOrder = EG_D;
 
@@ -66,13 +68,17 @@
     canRemove = removeable;
   }
 
-  public Map<ApprovalCategory.Id, PatchSetApproval> getApprovalMap() {
-    final HashMap<ApprovalCategory.Id, PatchSetApproval> r;
-    r = new HashMap<ApprovalCategory.Id, PatchSetApproval>();
-    for (final PatchSetApproval ca : approvals) {
-      r.put(ca.getCategoryId(), ca);
+  public List<PatchSetApproval> getPatchSetApprovals() {
+    return approvals;
+  }
+
+  public PatchSetApproval getPatchSetApproval(ApprovalCategory.Id category) {
+    for (PatchSetApproval psa : approvals) {
+      if (psa.getCategoryId().equals(category)) {
+        return psa;
+      }
     }
-    return r;
+    return null;
   }
 
   public void sortFirst() {
@@ -91,4 +97,26 @@
       hasNonZero = 1;
     }
   }
+
+  public void approved(String label) {
+    if (approved == null) {
+      approved = new HashSet<String>();
+    }
+    approved.add(label);
+  }
+
+  public void rejected(String label) {
+    if (rejected == null) {
+      rejected = new HashSet<String>();
+    }
+    rejected.add(label);
+  }
+
+  public boolean isApproved(String label) {
+    return approved != null && approved.contains(label);
+  }
+
+  public boolean isRejected(String label) {
+    return rejected != null && rejected.contains(label);
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index f196c05..c4c56d8 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ChangeMessage;
 import com.google.gerrit.reviewdb.PatchSet;
@@ -23,7 +22,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
 /** Detail necessary to display a change. */
 public class ChangeDetail {
@@ -38,7 +36,7 @@
   protected List<ChangeInfo> neededBy;
   protected List<PatchSet> patchSets;
   protected List<ApprovalDetail> approvals;
-  protected Set<ApprovalCategory.Id> missingApprovals;
+  protected List<SubmitRecord> submitRecords;
   protected boolean canSubmit;
   protected List<ChangeMessage> messages;
   protected PatchSet.Id currentPatchSetId;
@@ -153,12 +151,12 @@
     Collections.sort(approvals, ApprovalDetail.SORT);
   }
 
-  public Set<ApprovalCategory.Id> getMissingApprovals() {
-    return missingApprovals;
+  public void setSubmitRecords(List<SubmitRecord> all) {
+    submitRecords = all;
   }
 
-  public void setMissingApprovals(Set<ApprovalCategory.Id> a) {
-    missingApprovals = a;
+  public List<SubmitRecord> getSubmitRecords() {
+    return submitRecords;
   }
 
   public boolean isCurrentPatchSet(final PatchSetDetail detail) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
new file mode 100644
index 0000000..953ae99
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2011 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.common.data;
+
+import com.google.gerrit.reviewdb.Account;
+
+import java.util.List;
+
+/**
+ * Describes the state required to submit a change.
+ */
+public class SubmitRecord {
+  public static enum Status {
+    /** The change is ready for submission. */
+    OK,
+
+    /** The change is missing a required label. */
+    NOT_READY,
+
+    /** The change has been closed. */
+    CLOSED,
+
+    /**
+     * An internal server error occurred preventing computation.
+     * <p>
+     * Additional detail may be available in {@link SubmitRecord#errorMessage}.
+     */
+    RULE_ERROR;
+  }
+
+  public Status status;
+  public List<Label> labels;
+  public String errorMessage;
+
+  public static class Label {
+    public static enum Status {
+      /**
+       * This label provides what is necessary for submission.
+       * <p>
+       * If provided, {@link Label#appliedBy} describes the user account
+       * that applied this label to the change.
+       */
+      OK,
+
+      /**
+       * This label prevents the change from being submitted.
+       * <p>
+       * If provided, {@link Label#appliedBy} describes the user account
+       * that applied this label to the change.
+       */
+      REJECT,
+
+      /**
+       * The label is required for submission, but has not been satisfied.
+       */
+      NEED,
+
+      /**
+       * The label is required for submission, but is impossible to complete.
+       * The likely cause is access has not been granted correctly by the
+       * project owner or site administrator.
+       */
+      IMPOSSIBLE;
+    }
+
+    public String label;
+    public Status status;
+    public Account.Id appliedBy;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index 4a5c1ce..9081e38 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -24,11 +24,11 @@
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.common.data.ApprovalDetail;
 import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ReviewerResult;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -46,13 +46,15 @@
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 /** Displays a table of {@link ApprovalDetail} objects for a change record. */
 public class ApprovalTable extends Composite {
-  private final List<ApprovalType> types;
+  private final ApprovalTypes types;
   private final Grid table;
   private final Widget missing;
   private final Panel addReviewer;
@@ -61,10 +63,9 @@
   private AccountInfoCache accountCache = AccountInfoCache.empty();
 
   public ApprovalTable() {
-    types = Gerrit.getConfig().getApprovalTypes().getApprovalTypes();
-    table = new Grid(1, 3 + types.size());
+    types = Gerrit.getConfig().getApprovalTypes();
+    table = new Grid(1, 3);
     table.addStyleName(Gerrit.RESOURCES.css().infoTable());
-    displayHeader();
 
     missing = new Widget() {
       {
@@ -95,7 +96,9 @@
     setStyleName(Gerrit.RESOURCES.css().approvalTable());
   }
 
-  private void displayHeader() {
+  private void displayHeader(List<String> labels) {
+    table.resizeColumns(2 + labels.size());
+
     final CellFormatter fmt = table.getCellFormatter();
     int col = 0;
 
@@ -107,16 +110,12 @@
     fmt.setStyleName(0, col, Gerrit.RESOURCES.css().header());
     col++;
 
-    for (final ApprovalType t : types) {
-      table.setText(0, col, t.getCategory().getName());
+    for (String name : labels) {
+      table.setText(0, col, name);
       fmt.setStyleName(0, col, Gerrit.RESOURCES.css().header());
       col++;
     }
-
-    table.clearCell(0, col);
-    fmt.setStyleName(0, col, Gerrit.RESOURCES.css().header());
-    fmt.addStyleName(0, col, Gerrit.RESOURCES.css().rightmost());
-    col++;
+    fmt.addStyleName(0, col - 1, Gerrit.RESOURCES.css().rightmost());
   }
 
   public void setAccountInfoCache(final AccountInfoCache aic) {
@@ -128,40 +127,115 @@
     return AccountDashboardLink.link(accountCache, id);
   }
 
-  public void display(final Change change, final Set<ApprovalCategory.Id> need,
-      final List<ApprovalDetail> rows) {
-    changeId = change.getId();
+  void display(ChangeDetail detail) {
+    List<String> columns = new ArrayList<String>();
+    List<ApprovalDetail> rows = detail.getApprovals();
 
-    if (rows.isEmpty()) {
-      table.setVisible(false);
-    } else {
-      table.resizeRows(1 + rows.size());
-      for (int i = 0; i < rows.size(); i++) {
-        displayRow(i + 1, rows.get(i), change);
-      }
-      table.setVisible(true);
-    }
+    changeId = detail.getChange().getId();
 
     final Element missingList = missing.getElement();
     while (DOM.getChildCount(missingList) > 0) {
       DOM.removeChild(missingList, DOM.getChild(missingList, 0));
     }
-
     missing.setVisible(false);
-    if (need != null) {
-      for (final ApprovalType at : types) {
-        if (need.contains(at.getCategory().getId())) {
-          final Element li = DOM.createElement("li");
-          li.setClassName(Gerrit.RESOURCES.css().missingApproval());
-          DOM.setInnerText(li, Util.M.needApproval(at.getCategory().getName(),
-              at.getMax().formatValue(), at.getMax().getName()));
-          DOM.appendChild(missingList, li);
-          missing.setVisible(true);
+
+    if (detail.getSubmitRecords() != null) {
+      HashSet<String> reportedMissing = new HashSet<String>();
+
+      HashMap<Account.Id, ApprovalDetail> byUser =
+          new HashMap<Account.Id, ApprovalDetail>();
+      for (ApprovalDetail ad : detail.getApprovals()) {
+        byUser.put(ad.getAccount(), ad);
+      }
+
+      for (SubmitRecord rec : detail.getSubmitRecords()) {
+        if (rec.labels == null) {
+          continue;
         }
+
+        for (SubmitRecord.Label lbl : rec.labels) {
+          if (!columns.contains(lbl.label)) {
+            columns.add(lbl.label);
+          }
+
+          switch (lbl.status) {
+            case OK: {
+              ApprovalDetail ad = byUser.get(lbl.appliedBy);
+              if (ad != null) {
+                ad.approved(lbl.label);
+              }
+              break;
+            }
+
+            case REJECT: {
+              ApprovalDetail ad = byUser.get(lbl.appliedBy);
+              if (ad != null) {
+                ad.rejected(lbl.label);
+              }
+              break;
+            }
+
+            case NEED:
+            case IMPOSSIBLE:
+              if (reportedMissing.add(lbl.label)) {
+                Element li = DOM.createElement("li");
+                li.setClassName(Gerrit.RESOURCES.css().missingApproval());
+                DOM.setInnerText(li, Util.M.needApproval(lbl.label));
+                DOM.appendChild(missingList, li);
+              }
+              break;
+          }
+        }
+      }
+      missing.setVisible(!reportedMissing.isEmpty());
+
+    } else {
+      for (ApprovalDetail ad : rows) {
+        for (PatchSetApproval psa : ad.getPatchSetApprovals()) {
+          ApprovalType legacyType = types.byId(psa.getCategoryId());
+          if (legacyType == null) {
+            continue;
+          }
+          String labelName = legacyType.getCategory().getLabelName();
+          if (psa.getValue() == legacyType.getMax().getValue()) {
+            ad.approved(labelName);
+          } else if (psa.getValue() == legacyType.getMin().getValue()) {
+            ad.rejected(labelName);
+          }
+          if (!columns.contains(labelName)) {
+            columns.add(labelName);
+          }
+        }
+        Collections.sort(columns, new Comparator<String>() {
+          @Override
+          public int compare(String o1, String o2) {
+            ApprovalType a = types.byLabel(o1);
+            ApprovalType b = types.byLabel(o2);
+            int cmp = 0;
+            if (a != null && b != null) {
+              cmp = a.getCategory().getPosition() - b.getCategory().getPosition();
+            }
+            if (cmp == 0) {
+              cmp = o1.compareTo(o2);
+            }
+            return cmp;
+          }
+        });
       }
     }
 
-    addReviewer.setVisible(Gerrit.isSignedIn() && change.getStatus().isOpen());
+    if (rows.isEmpty()) {
+      table.setVisible(false);
+    } else {
+      displayHeader(columns);
+      table.resizeRows(1 + rows.size());
+      for (int i = 0; i < rows.size(); i++) {
+        displayRow(i + 1, rows.get(i), detail.getChange(), columns);
+      }
+      table.setVisible(true);
+    }
+
+    addReviewer.setVisible(Gerrit.isSignedIn() && detail.getChange().getStatus().isOpen());
   }
 
   private void doAddReviewer() {
@@ -210,10 +284,11 @@
             final ChangeDetail r = result.getChange();
             if (r != null) {
               setAccountInfoCache(r.getAccounts());
-              display(r.getChange(), r.getMissingApprovals(), r.getApprovals());
+              display(r);
             }
           }
 
+
           @Override
           public void onFailure(final Throwable caught) {
             addMemberBox.setEnabled(true);
@@ -223,10 +298,8 @@
   }
 
   private void displayRow(final int row, final ApprovalDetail ad,
-      final Change change) {
+      final Change change, List<String> columns) {
     final CellFormatter fmt = table.getCellFormatter();
-    final Map<ApprovalCategory.Id, PatchSetApproval> am = ad.getApprovalMap();
-    final StringBuilder hint = new StringBuilder();
     int col = 0;
 
     table.setWidget(row, col++, link(ad.getAccount()));
@@ -250,31 +323,30 @@
     }
     fmt.setStyleName(row, col++, Gerrit.RESOURCES.css().removeReviewerCell());
 
-    for (final ApprovalType type : types) {
+    for (String labelName : columns) {
       fmt.setStyleName(row, col, Gerrit.RESOURCES.css().approvalscore());
 
-      final PatchSetApproval ca = am.get(type.getCategory().getId());
-      if (ca == null || ca.getValue() == 0) {
-        table.clearCell(row, col);
-        col++;
-        continue;
-      }
-
-      final ApprovalCategoryValue acv = type.getValue(ca);
-      if (acv != null) {
-        if (hint.length() > 0) {
-          hint.append("; ");
-        }
-        hint.append(acv.getName());
-      }
-
-      if (type.isMaxNegative(ca)) {
+      if (ad.isRejected(labelName)) {
         table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
 
-      } else if (type.isMaxPositive(ca)) {
+      } else if (ad.isApproved(labelName)) {
         table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
 
       } else {
+        ApprovalType legacyType = types.byLabel(labelName);
+        if (legacyType == null) {
+          table.clearCell(row, col);
+          col++;
+          continue;
+        }
+
+        PatchSetApproval ca = ad.getPatchSetApproval(legacyType.getCategory().getId());
+        if (ca == null || ca.getValue() == 0) {
+          table.clearCell(row, col);
+          col++;
+          continue;
+        }
+
         String vstr = String.valueOf(ca.getValue());
         if (ca.getValue() > 0) {
           vstr = "+" + vstr;
@@ -288,10 +360,7 @@
       col++;
     }
 
-    table.setText(row, col, hint.toString());
-    fmt.setStyleName(row, col, Gerrit.RESOURCES.css().rightmost());
-    fmt.addStyleName(row, col, Gerrit.RESOURCES.css().approvalhint());
-    col++;
+    fmt.addStyleName(row, col - 1, Gerrit.RESOURCES.css().rightmost());
   }
 
   private void doRemove(final ApprovalDetail ad, final PushButton remove) {
@@ -302,7 +371,7 @@
           public void onSuccess(ReviewerResult result) {
             if (result.getErrors().isEmpty()) {
               final ChangeDetail r = result.getChange();
-              display(r.getChange(), r.getMissingApprovals(), r.getApprovals());
+              display(r);
             } else {
               final ReviewerResult.Error resultError =
                   result.getErrors().get(0);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index cbf4a6b..53d1c18 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -43,7 +43,7 @@
   String copiedFrom(String sourcePath);
   String otherFrom(String sourcePath);
 
-  String needApproval(String categoryName, String value, String valueName);
+  String needApproval(String labelName);
   String publishComments(String changeId, int ps);
   String lineHeader(int line);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index 5982ad5..6f6e34e2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -24,7 +24,7 @@
 copiedFrom = copied from {0}
 otherFrom = from {0}
 
-needApproval = Need {0} {1} ({2})
+needApproval = Need {0}
 publishComments = Change {0} - Patch Set {1}: Publish Comments
 lineHeader = Line {0}:
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index edb25e4..6bd4ebf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
@@ -306,8 +306,7 @@
         .getCurrentPatchSetDetail().getInfo(), detail.getAccounts());
     dependsOn.display(detail.getDependsOn());
     neededBy.display(detail.getNeededBy());
-    approvals.display(detail.getChange(), detail.getMissingApprovals(), detail
-        .getApprovals());
+    approvals.display(detail);
 
     for (PatchSet pId : detail.getPatchSets()) {
       if (patchesList != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
index f7f8ae1..274f8f5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.PatchSetPublishDetail;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
@@ -221,18 +220,13 @@
 
   private void initApprovals(final PatchSetPublishDetail r, final Panel body) {
     ApprovalTypes types = Gerrit.getConfig().getApprovalTypes();
-
-    for (ApprovalType type : types.getApprovalTypes()) {
-      String permission = Permission.forLabel(type.getCategory().getLabelName());
-      PermissionRange range = r.getRange(permission);
-      if (range != null && !range.isEmpty()) {
-        initApprovalType(r, body, type, range);
-      }
-    }
-
     for (PermissionRange range : r.getLabels()) {
-      if (!range.isEmpty() && types.byLabel(range.getLabel()) == null) {
-        // TODO: this is a non-standard label. Offer it without the type.
+      ApprovalType type = types.byLabel(range.getLabel());
+      if (type != null) {
+        // Legacy type, use radio buttons.
+        initApprovalType(r, body, type, range);
+      } else {
+        // TODO Newer style label.
       }
     }
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index fc3fb8f..2705991 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
@@ -19,10 +19,10 @@
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ChangeInfo;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ChangeMessage;
 import com.google.gerrit.reviewdb.PatchSet;
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.CanSubmitResult;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.workflow.CategoryFunction;
@@ -99,7 +98,6 @@
     if (patch == null) {
       throw new NoSuchEntityException();
     }
-    final CanSubmitResult canSubmitResult = control.canSubmit(patch.getId());
 
     aic.want(change.getOwner());
 
@@ -109,12 +107,26 @@
 
     detail.setCanAbandon(change.getStatus().isOpen() && control.canAbandon());
     detail.setCanRestore(change.getStatus() == Change.Status.ABANDONED && control.canRestore());
-    detail.setCanSubmit(canSubmitResult == CanSubmitResult.OK);
     detail.setStarred(control.getCurrentUser().getStarredChanges().contains(
         changeId));
 
     detail.setCanRevert(change.getStatus() == Change.Status.MERGED && control.canAddPatchSet());
 
+    if (detail.getChange().getStatus().isOpen()) {
+      List<SubmitRecord> submitRecords = control.canSubmit(db, patch.getId());
+      for (SubmitRecord rec : submitRecords) {
+        if (rec.labels != null) {
+          for (SubmitRecord.Label lbl : rec.labels) {
+            aic.want(lbl.appliedBy);
+          }
+        }
+        if (rec.status == SubmitRecord.Status.OK && control.getRefControl().canSubmit()) {
+          detail.setCanSubmit(true);
+        }
+      }
+      detail.setSubmitRecords(submitRecords);
+    }
+
     loadPatchSets();
     loadMessages();
     if (change.currentPatchSetId() != null) {
@@ -145,16 +157,9 @@
       final FunctionState fs =
           functionState.create(detail.getChange(), psId, allApprovals);
 
-      final Set<ApprovalCategory.Id> missingApprovals =
-          new HashSet<ApprovalCategory.Id>();
-
       for (final ApprovalType at : approvalTypes.getApprovalTypes()) {
         CategoryFunction.forCategory(at.getCategory()).run(at, fs);
-        if (!fs.isValid(at)) {
-          missingApprovals.add(at.getCategory().getId());
-        }
       }
-      detail.setMissingApprovals(missingApprovals);
     }
 
     final boolean canRemoveReviewers = detail.getChange().getStatus().isOpen() //
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
index 58daa09..ded92e0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
-import com.google.gerrit.common.data.AccountInfoCache;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.PatchSetPublishDetail;
 import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchLineComment;
@@ -28,7 +30,6 @@
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.CanSubmitResult;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.client.OrmException;
@@ -37,7 +38,9 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 final class PatchSetPublishDetailFactory extends Handler<PatchSetPublishDetail> {
   interface Factory {
@@ -47,12 +50,12 @@
   private final PatchSetInfoFactory infoFactory;
   private final ReviewDb db;
   private final ChangeControl.Factory changeControlFactory;
+  private final ApprovalTypes approvalTypes;
   private final AccountInfoCacheFactory aic;
   private final IdentifiedUser user;
 
   private final PatchSet.Id patchSetId;
 
-  private AccountInfoCache accounts;
   private PatchSetInfo patchSetInfo;
   private Change change;
   private List<PatchLineComment> drafts;
@@ -62,10 +65,12 @@
       final ReviewDb db,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
       final ChangeControl.Factory changeControlFactory,
+      final ApprovalTypes approvalTypes,
       final IdentifiedUser user, @Assisted final PatchSet.Id patchSetId) {
     this.infoFactory = infoFactory;
     this.db = db;
     this.changeControlFactory = changeControlFactory;
+    this.approvalTypes = approvalTypes;
     this.aic = accountInfoCacheFactory.create();
     this.user = user;
 
@@ -81,32 +86,92 @@
     patchSetInfo = infoFactory.get(patchSetId);
     drafts = db.patchComments().draft(patchSetId, user.getAccountId()).toList();
 
+    aic.want(change.getOwner());
+
+    PatchSetPublishDetail detail = new PatchSetPublishDetail();
+    detail.setPatchSetInfo(patchSetInfo);
+    detail.setChange(change);
+    detail.setDrafts(drafts);
+
     List<PermissionRange> allowed = Collections.emptyList();
     List<PatchSetApproval> given = Collections.emptyList();
 
     if (change.getStatus().isOpen()
         && patchSetId.equals(change.currentPatchSetId())) {
-      allowed = new ArrayList<PermissionRange>(control.getLabelRanges());
-      Collections.sort(allowed);
+      // TODO Push this selection of labels down into the Prolog interpreter.
+      // Ideally we discover the labels the user can apply here based on doing
+      // a findall() over the space of labels they can apply combined against
+      // the submit rule, thereby skipping any mutually exclusive cases. However
+      // those are not common, so it might just be reasonable to take this
+      // simple approach.
+
+      Map<String, PermissionRange> rangeByName =
+          new HashMap<String, PermissionRange>();
+      for (PermissionRange r : control.getLabelRanges()) {
+        if (r.isLabel()) {
+          rangeByName.put(r.getLabel(), r);
+        }
+      }
+      allowed = new ArrayList<PermissionRange>();
 
       given = db.patchSetApprovals() //
           .byPatchSetUser(patchSetId, user.getAccountId()) //
           .toList();
+
+      boolean couldSubmit = false;
+      List<SubmitRecord> submitRecords = control.canSubmit(db, patchSetId);
+      for (SubmitRecord rec : submitRecords) {
+        if (rec.status == SubmitRecord.Status.OK) {
+          couldSubmit = true;
+        }
+
+        if (rec.labels != null) {
+          int ok = 0;
+
+          for (SubmitRecord.Label lbl : rec.labels) {
+            boolean canMakeOk = false;
+            PermissionRange range = rangeByName.get(lbl.label);
+            if (range != null) {
+              if (!allowed.contains(range)) {
+                allowed.add(range);
+              }
+
+              ApprovalType at = approvalTypes.byLabel(lbl.label);
+              if (at != null && at.getMax().getValue() == range.getMax()) {
+                canMakeOk = true;
+              } else if (at == null) {
+                canMakeOk = true;
+              }
+            }
+
+            switch (lbl.status) {
+              case OK:
+                ok++;
+                break;
+
+              case NEED:
+                if (canMakeOk) {
+                  ok++;
+                }
+                break;
+            }
+          }
+
+          if (rec.status == SubmitRecord.Status.NOT_READY
+              && ok == rec.labels.size()) {
+            couldSubmit = true;
+          }
+        }
+      }
+
+      if (couldSubmit && control.getRefControl().canSubmit()) {
+        detail.setCanSubmit(true);
+      }
     }
 
-    aic.want(change.getOwner());
-    accounts = aic.create();
-
-    PatchSetPublishDetail detail = new PatchSetPublishDetail();
-    detail.setAccounts(accounts);
-    detail.setPatchSetInfo(patchSetInfo);
-    detail.setChange(change);
-    detail.setDrafts(drafts);
     detail.setLabels(allowed);
     detail.setGiven(given);
-
-    final CanSubmitResult canSubmitResult = control.canSubmit(patchSetId);
-    detail.setCanSubmit(canSubmitResult == CanSubmitResult.OK);
+    detail.setAccounts(aic.create());
 
     return detail;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
index cbefae6..909e59a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.rpc.changedetail;
 
 import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.Change;
@@ -25,13 +26,14 @@
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeQueue;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.CanSubmitResult;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import java.util.List;
+
 class SubmitAction extends Handler<ChangeDetail> {
   interface Factory {
     SubmitAction create(PatchSet.Id patchSetId);
@@ -72,12 +74,51 @@
     final ChangeControl changeControl =
         changeControlFactory.validateFor(changeId);
 
-    CanSubmitResult err = changeControl.canSubmit(db, patchSetId);
-    if (err == CanSubmitResult.OK) {
-      ChangeUtil.submit(patchSetId, user, db, opFactory, merger);
-      return changeDetailFactory.create(changeId).call();
-    } else {
-      throw new IllegalStateException(err.getMessage());
+    List<SubmitRecord> result = changeControl.canSubmit(db, patchSetId);
+    if (result.isEmpty()) {
+      throw new IllegalStateException("Cannot submit");
+    }
+
+    switch (result.get(0).status) {
+      case OK:
+        ChangeUtil.submit(patchSetId, user, db, opFactory, merger);
+        return changeDetailFactory.create(changeId).call();
+
+      case NOT_READY: {
+        StringBuilder msg = new StringBuilder();
+        for (SubmitRecord.Label lbl : result.get(0).labels) {
+          switch (lbl.status) {
+            case OK:
+              break;
+
+            case REJECT:
+              throw new IllegalStateException("Blocked by " + lbl.label);
+
+            case NEED:
+              throw new IllegalStateException("Needs " + lbl.label);
+
+            case IMPOSSIBLE:
+              throw new IllegalStateException("Cannnot submit, check project access");
+
+            default:
+              throw new IllegalArgumentException("Unknown status " + lbl.status);
+          }
+        }
+        throw new IllegalStateException("Cannot submit");
+      }
+
+      case CLOSED:
+        throw new IllegalStateException("Change is closed");
+
+      case RULE_ERROR:
+        if (result.get(0).errorMessage != null) {
+          throw new IllegalStateException(result.get(0).errorMessage);
+        } else {
+          throw  new IllegalStateException("Internal rule error");
+        }
+
+      default:
+        throw new IllegalStateException("Uknown status " + result.get(0).status);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CanSubmitResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CanSubmitResult.java
deleted file mode 100644
index 0c14b80..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CanSubmitResult.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2010 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.server.project;
-
-/**
- * Result from {@code ChangeControl.canSubmit()}.
- *
- * @see ChangeControl#canSubmit(com.google.gerrit.reviewdb.PatchSet.Id,
- *      com.google.gerrit.reviewdb.ReviewDb,
- *      com.google.gerrit.common.data.ApprovalTypes,
- *      com.google.gerrit.server.workflow.FunctionState.Factory)
- */
-public class CanSubmitResult {
-  /** Magic constant meaning submitting is possible. */
-  public static final CanSubmitResult OK = new CanSubmitResult("OK");
-
-  private final String errorMessage;
-
-  CanSubmitResult(String error) {
-    this.errorMessage = error;
-  }
-
-  public String getMessage() {
-    return errorMessage;
-  }
-
-  @Override
-  public String toString() {
-    return "CanSubmitResult[" + getMessage() + "]";
-  }
-}
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 8180759..72db734 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
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.project;
 
 import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
@@ -39,6 +41,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 
@@ -224,36 +227,29 @@
     return false;
   }
 
-  /** @return {@link CanSubmitResult#OK}, or a result with an error message. */
-  public CanSubmitResult canSubmit(final PatchSet.Id patchSetId) {
+  public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet.Id patchSetId) {
     if (change.getStatus().isClosed()) {
-      return new CanSubmitResult("Change " + change.getId() + " is closed");
+      SubmitRecord rec = new SubmitRecord();
+      rec.status = SubmitRecord.Status.CLOSED;
+      return Collections.singletonList(rec);
     }
-    if (!patchSetId.equals(change.currentPatchSetId())) {
-      return new CanSubmitResult("Patch set " + patchSetId + " is not current");
-    }
-    if (!getRefControl().canSubmit()) {
-      return new CanSubmitResult("User does not have permission to submit");
-    }
-    if (!(getCurrentUser() instanceof IdentifiedUser)) {
-      return new CanSubmitResult("User is not signed-in");
-    }
-    return CanSubmitResult.OK;
-  }
 
-  /** @return {@link CanSubmitResult#OK}, or a result with an error message. */
-  public CanSubmitResult canSubmit(ReviewDb db, PatchSet.Id patchSetId) {
-    CanSubmitResult result = canSubmit(patchSetId);
-    if (result != CanSubmitResult.OK) {
-      return result;
+    if (!patchSetId.equals(change.currentPatchSetId())) {
+      SubmitRecord rec = new SubmitRecord();
+      rec.status = SubmitRecord.Status.RULE_ERROR;
+      rec.errorMessage = "Patch set " + patchSetId + " is not current";
+      return Collections.singletonList(rec);
     }
 
     PrologEnvironment env;
     try {
       env = getProjectControl().getProjectState().newPrologEnvironment();
     } catch (CompileException err) {
-      log.error("cannot consult rules.pl", err);
-      return new CanSubmitResult("Error reading submit rule");
+      log.error("Cannot consult rules.pl for " + getProject().getName(), err);
+      SubmitRecord rec = new SubmitRecord();
+      rec.status = SubmitRecord.Status.RULE_ERROR;
+      rec.errorMessage = "Error loading project rules, check server log";
+      return Collections.singletonList(rec);
     }
 
     env.set(StoredValues.REVIEW_DB, db);
@@ -265,8 +261,11 @@
       "gerrit", "locate_submit_rule",
       new VariableTerm());
     if (submitRule == null) {
-      log.error("Error in locate_submit_rule: no submit_rule found");
-      return new CanSubmitResult("Error in finding submit rule");
+      log.error("No user:submit_rule found for " + getProject().getName());
+      SubmitRecord rec = new SubmitRecord();
+      rec.status = SubmitRecord.Status.RULE_ERROR;
+      rec.errorMessage = "Error loading project rules, check server log";
+      return Collections.singletonList(rec);
     }
 
     List<Term> results = new ArrayList<Term>();
@@ -278,8 +277,19 @@
         results.add(template[1]);
       }
     } catch (PrologException err) {
-      log.error("PrologException calling " + submitRule, err);
-      return new CanSubmitResult("Error in submit rule");
+      log.error("Exception calling " + submitRule + " on change "
+          + change.getId() + " of " + getProject().getName(), err);
+      SubmitRecord rec = new SubmitRecord();
+      rec.status = SubmitRecord.Status.RULE_ERROR;
+      rec.errorMessage = "Error evaluating project rules, check server log";
+      return Collections.singletonList(rec);
+    } catch (RuntimeException err) {
+      log.error("Exception calling " + submitRule + " on change "
+          + change.getId() + " of " + getProject().getName(), err);
+      SubmitRecord rec = new SubmitRecord();
+      rec.status = SubmitRecord.Status.RULE_ERROR;
+      rec.errorMessage = "Error evaluating project rules, check server log";
+      return Collections.singletonList(rec);
     }
 
     if (results.isEmpty()) {
@@ -287,65 +297,133 @@
       // at least one result informing the caller of the labels that are
       // required for this change to be submittable. Each label will indicate
       // whether or not that is actually possible given the permissions.
-      log.error("Submit rule has no solution: " + submitRule);
-      return new CanSubmitResult("Error in submit rule (no solution possible)");
+      log.error("Submit rule " + submitRule + " for change " + change.getId()
+          + " of " + getProject().getName() + " has no solution.");
+      SubmitRecord rec = new SubmitRecord();
+      rec.status = SubmitRecord.Status.RULE_ERROR;
+      rec.errorMessage = "Project submit rule has no solution";
+      return Collections.singletonList(rec);
     }
 
-    // The last result produced will be an "ok(P)" format if submit is possible.
-    // This is always true because can_submit (called above) will cut away all
-    // choice points once a solution is found.
-    Term last = results.get(results.size() - 1);
-    if (last.isStructure() && 1 == last.arity() && "ok".equals(last.name())) {
-      // Term solution = last.arg(0);
-      return CanSubmitResult.OK;
-    }
+    // Convert the results from Prolog Cafe's format to Gerrit's common format.
+    // can_submit/1 terminates when an ok(P) record is found. Therefore walk
+    // the results backwards, using only that ok(P) record if it exists. This
+    // skips partial results that occur early in the output. Later after the loop
+    // the out collection is reversed to restore it to the original ordering.
+    //
+    List<SubmitRecord> out = new ArrayList<SubmitRecord>(results.size());
+    for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
+      Term submitRecord = results.get(resultIdx);
+      SubmitRecord rec = new SubmitRecord();
+      out.add(rec);
 
-    // For now only process the first result. Later we can examine all of the
-    // results and proposes different alternative paths to a submit solution.
-    Term first = results.get(0);
-    if (!first.isStructure() || 1 != first.arity() || !"not_ready".equals(first.name())) {
-      log.error("Unexpected result from can_submit: " + first);
-      return new CanSubmitResult("Error in submit rule");
-    }
-
-    Term submitRecord = first.arg(0);
-    if (!submitRecord.isStructure()) {
-      log.error("Invalid result from submit rule " + submitRule + ": " + submitRecord);
-      return new CanSubmitResult("Error in submit rule");
-    }
-
-    for (Term state : ((StructureTerm) submitRecord).args()) {
-      if (!state.isStructure() || 2 != state.arity() || !"label".equals(state.name())) {
-        log.error("Invalid result from submit rule " + submitRule + ": " + submitRecord);
-        return new CanSubmitResult("Invalid submit rule result");
+      if (!submitRecord.isStructure() || 1 != submitRecord.arity()) {
+        log.error("Submit rule " + submitRule + " for change " + change.getId()
+            + " of " + getProject().getName() + " output invalid result: "
+            + submitRecord);
+        SubmitRecord err = new SubmitRecord();
+        err.status = SubmitRecord.Status.RULE_ERROR;
+        rec.errorMessage = "Error evaluating project rules, check server log";
+        return Collections.singletonList(err);
       }
 
-      String label = state.arg(0).name();
-      Term status = state.arg(1);
+      if ("ok".equals(submitRecord.name())) {
+        rec.status = SubmitRecord.Status.OK;
 
-      if ("ok".equals(status.name())) {
-        continue;
-
-      } else if ("reject".equals(status.name())) {
-        return new CanSubmitResult("Submit blocked by " + label);
-
-      } else if ("need".equals(status.name())) {
-        if (status.isStructure() && status.arg(0).isInteger()) {
-          IntegerTerm val = (IntegerTerm) status.arg(0);
-          if (1 < val.intValue()) {
-            label += "+" + val.intValue();
-          }
-        }
-        return new CanSubmitResult("Requires " + label);
-
-      } else if ("impossble".equals(status.name())) {
-        return new CanSubmitResult("Requires " + label + " (check permissions)");
+      } else if ("not_ready".equals(submitRecord.name())) {
+        rec.status = SubmitRecord.Status.NOT_READY;
 
       } else {
-        return new CanSubmitResult("Invalid submit rule result");
+        log.error("Submit rule " + submitRule + " for change " + change.getId()
+            + " of " + getProject().getName() + " output invalid result: "
+            + submitRecord);
+        SubmitRecord err = new SubmitRecord();
+        err.status = SubmitRecord.Status.RULE_ERROR;
+        rec.errorMessage = "Error evaluating project rules, check server log";
+        return Collections.singletonList(err);
+      }
+
+      // Unpack the one argument. This should also be a structure with one
+      // argument per label that needs to be reported on to the caller.
+      //
+      submitRecord = submitRecord.arg(0);
+
+      if (!submitRecord.isStructure()) {
+        log.error("Submit rule " + submitRule + " for change " + change.getId()
+            + " of " + getProject().getName() + " output invalid result: "
+            + submitRecord);
+        SubmitRecord err = new SubmitRecord();
+        err.status = SubmitRecord.Status.RULE_ERROR;
+        rec.errorMessage = "Error evaluating project rules, check server log";
+        return Collections.singletonList(err);
+      }
+
+      rec.labels = new ArrayList<SubmitRecord.Label> (submitRecord.arity());
+
+      for (Term state : ((StructureTerm) submitRecord).args()) {
+        if (!state.isStructure() || 2 != state.arity() || !"label".equals(state.name())) {
+          log.error("Submit rule " + submitRule + " for change " + change.getId()
+              + " of " + getProject().getName() + " output invalid result: "
+              + submitRecord);
+          SubmitRecord err = new SubmitRecord();
+          err.status = SubmitRecord.Status.RULE_ERROR;
+          rec.errorMessage = "Error evaluating project rules, check server log";
+          return Collections.singletonList(err);
+        }
+
+        SubmitRecord.Label lbl = new SubmitRecord.Label();
+        rec.labels.add(lbl);
+
+        lbl.label = state.arg(0).name();
+        Term status = state.arg(1);
+
+        if ("ok".equals(status.name())) {
+          lbl.status = SubmitRecord.Label.Status.OK;
+          appliedBy(lbl, status);
+
+        } else if ("reject".equals(status.name())) {
+          lbl.status = SubmitRecord.Label.Status.REJECT;
+          appliedBy(lbl, status);
+
+        } else if ("need".equals(status.name())) {
+          lbl.status = SubmitRecord.Label.Status.NEED;
+
+        } else if ("impossible".equals(status.name())) {
+          lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
+
+        } else {
+          log.error("Submit rule " + submitRule + " for change " + change.getId()
+              + " of " + getProject().getName() + " output invalid result: "
+              + submitRecord);
+          SubmitRecord err = new SubmitRecord();
+          err.status = SubmitRecord.Status.RULE_ERROR;
+          rec.errorMessage = "Error evaluating project rules, check server log";
+          return Collections.singletonList(err);
+        }
+      }
+
+      if (rec.status == SubmitRecord.Status.OK) {
+        break;
       }
     }
+    Collections.reverse(out);
 
-    return CanSubmitResult.OK;
+    return out;
+  }
+
+  private void appliedBy(SubmitRecord.Label label, Term status) {
+    if (status.isStructure() && status.arity() == 1) {
+      Term who = status.arg(0);
+      if (isUser(who)) {
+        label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
+      }
+    }
+  }
+
+  private static boolean isUser(Term who) {
+    return who.isStructure()
+        && who.arity() == 1
+        && who.name().equals("user")
+        && who.arg(0).isInteger();
   }
 }
\ No newline at end of file
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 93b2ba5..6404898 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.Branch;
@@ -32,7 +33,6 @@
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.patch.PublishComments;
-import com.google.gerrit.server.project.CanSubmitResult;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -204,7 +204,7 @@
   }
 
   private void approveOne(final PatchSet.Id patchSetId) throws
-      NoSuchChangeException, UnloggedFailure, OrmException, EmailException {
+      NoSuchChangeException, OrmException, EmailException, Failure {
 
     final Change.Id changeId = patchSetId.getParentKey();
     ChangeControl changeControl = changeControlFactory.validateFor(changeId);
@@ -250,11 +250,61 @@
     }
 
     if (submitChange) {
-      CanSubmitResult result = changeControl.canSubmit(db, patchSetId);
-      if (result == CanSubmitResult.OK) {
-        toSubmit.add(patchSetId);
-      } else {
-        throw error(result.getMessage());
+      List<SubmitRecord> result = changeControl.canSubmit(db, patchSetId);
+      if (result.isEmpty()) {
+        throw new Failure(1, "ChangeControl.canSubmit returned empty list");
+      }
+      switch (result.get(0).status) {
+        case OK:
+          if (changeControl.getRefControl().canSubmit()) {
+            toSubmit.add(patchSetId);
+          } else {
+            throw error("change " + changeId + ": you do not have submit permission");
+          }
+          break;
+
+        case NOT_READY: {
+          StringBuilder msg = new StringBuilder();
+          for (SubmitRecord.Label lbl : result.get(0).labels) {
+            switch (lbl.status) {
+              case OK:
+                break;
+
+              case REJECT:
+                if (msg.length() > 0) msg.append("\n");
+                msg.append("change " + changeId + ": blocked by " + lbl.label);
+                break;
+
+              case NEED:
+                if (msg.length() > 0) msg.append("\n");
+                msg.append("change " + changeId + ": needs " + lbl.label);
+                break;
+
+              case IMPOSSIBLE:
+                if (msg.length() > 0) msg.append("\n");
+                msg.append("change " + changeId + ": needs " + lbl.label
+                    + " (check project access)");
+                break;
+
+              default:
+                throw new Failure(1, "Unsupported label status " + lbl.status);
+            }
+          }
+          throw error(msg.toString());
+        }
+
+        case CLOSED:
+          throw error("change " + changeId + " is closed");
+
+        case RULE_ERROR:
+          if (result.get(0).errorMessage != null) {
+            throw error("change " + changeId + ": " + result.get(0).errorMessage);
+          } else {
+            throw error("change " + changeId + ": internal rule error");
+          }
+
+        default:
+          throw new Failure(1, "Unsupported status " + result.get(0).status);
       }
     }
   }