Add push cert status to change screen

Add a status icon next to the uploader with a check/warning/X
depending on the push cert status. Include messages in the title on
hover. If the uploader is the same as the change owner, place the
status icon next to the owner.

PushCertificateInfo and GpgKeyInfo are moved to gerrit-gwtui-common so
they can be referenced from ChangeInfo.

The new question mark icon is the help-browser icon from the
public-domain Tango collection:
http://tango.freedesktop.org/Tango_Icon_Library

Change-Id: Iec666c668342b00fd92609dd28feb082d5a560a8
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
index 0db7ea4..a5a02cd 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
@@ -104,4 +104,7 @@
 
   @Source("warning.png")
   public ImageResource warning();
+
+  @Source("question.png")
+  public ImageResource question();
 }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index ed3d4ec..dd88b04 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -311,6 +311,9 @@
     public final native boolean hasFetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
     public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
 
+    public final native boolean hasPushCertificate() /*-{ return this.hasOwnProperty('push_certificate'); }-*/;
+    public final native PushCertificateInfo pushCertificate() /*-{ return this.push_certificate; }-*/;
+
     public static void sortRevisionInfoByNumber(JsArray<RevisionInfo> list) {
       final int editParent = findEditParent(list);
       Collections.sort(Natives.asList(list), new Comparator<RevisionInfo>() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
similarity index 68%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
index d1bb426..f7477a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
@@ -12,17 +12,34 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.account;
+package com.google.gerrit.client.info;
 
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArrayString;
 
 public class GpgKeyInfo extends JavaScriptObject {
+  public enum Status {
+    BAD, OK, TRUSTED;
+  }
+
   public final native String id() /*-{ return this.id; }-*/;
   public final native String fingerprint() /*-{ return this.fingerprint; }-*/;
   public final native JsArrayString userIds() /*-{ return this.user_ids; }-*/;
   public final native String key() /*-{ return this.key; }-*/;
 
+  private final native String statusRaw() /*-{ return this.status; }-*/;
+  public final Status status() {
+    String s = statusRaw();
+    if (s == null) {
+      return null;
+    }
+    return Status.valueOf(s);
+  }
+
+  public final native boolean hasProblems()
+  /*-{ return this.hasOwnProperty('problems'); }-*/;
+  public final native JsArrayString problems() /*-{ return this.problems; }-*/;
+
   protected GpgKeyInfo() {
   }
 }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/PushCertificateInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/PushCertificateInfo.java
new file mode 100644
index 0000000..ebfec1a
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/PushCertificateInfo.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.google.gerrit.client.info;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class PushCertificateInfo extends JavaScriptObject {
+  public final native String certificate() /*-{ return this.certificate; }-*/;
+  public final native GpgKeyInfo key() /*-{ return this.key; }-*/;
+
+  protected PushCertificateInfo() {
+  }
+}
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/question.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/question.png
new file mode 100644
index 0000000..f25fc3f
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/question.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
index 0aa3a40..a1bcfe8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GpgKeyInfo;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.NativeString;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
index dc35b1e..99d791b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.info.GpgKeyInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index 829ee6f..eabf0e5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
@@ -38,6 +38,8 @@
 import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
 import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.info.FileInfo;
+import com.google.gerrit.client.info.GpgKeyInfo;
+import com.google.gerrit.client.info.PushCertificateInfo;
 import com.google.gerrit.client.projects.ConfigInfoCache;
 import com.google.gerrit.client.projects.ConfigInfoCache.Entry;
 import com.google.gerrit.client.rpc.CallbackGroup;
@@ -112,17 +114,18 @@
   private static final Binder uiBinder = GWT.create(Binder.class);
 
   interface Style extends CssResource {
-    String labelName();
     String avatar();
-    String label_user();
-    String label_ok();
-    String label_reject();
+    String hashtagName();
+    String highlight();
+    String labelName();
     String label_may();
     String label_need();
+    String label_ok();
+    String label_reject();
+    String label_user();
+    String pushCertStatus();
     String replyBox();
     String selected();
-    String highlight();
-    String hashtagName();
   }
 
   static ChangeScreen get(NativeEvent in) {
@@ -164,11 +167,14 @@
   @UiField Reviewers reviewers;
   @UiField Hashtags hashtags;
   @UiField Element hashtagTableRow;
+
   @UiField FlowPanel ownerPanel;
   @UiField InlineHyperlink ownerLink;
+
   @UiField Element uploaderRow;
   @UiField FlowPanel uploaderPanel;
   @UiField InlineLabel uploaderName;
+
   @UiField Element statusText;
   @UiField Image projectSettings;
   @UiField AnchorElement projectSettingsLink;
@@ -292,11 +298,19 @@
     p.add(extensionPanel);
   }
 
+  private boolean enableSignedPush() {
+    return Gerrit.info().receive().enableSignedPush();
+  }
+
   void loadChangeInfo(boolean fg, AsyncCallback<ChangeInfo> cb) {
     RestApi call = ChangeApi.detail(changeId.get());
-    ChangeList.addOptions(call, EnumSet.of(
-      ListChangesOption.CHANGE_ACTIONS,
-      ListChangesOption.ALL_REVISIONS));
+    EnumSet<ListChangesOption> opts = EnumSet.of(
+      ListChangesOption.ALL_REVISIONS,
+      ListChangesOption.CHANGE_ACTIONS);
+    if (enableSignedPush()) {
+      opts.add(ListChangesOption.PUSH_CERTIFICATES);
+    }
+    ChangeList.addOptions(call, opts);
     if (!fg) {
       call.background();
     }
@@ -1146,13 +1160,14 @@
   }
 
   private void renderChangeInfo(ChangeInfo info) {
+    RevisionInfo revisionInfo = info.revision(revision);
     changeInfo = info;
     lastDisplayedUpdate = info.updated();
 
     labels.set(info);
 
     renderOwner(info);
-    renderUploader(info, revision);
+    renderUploader(info, revisionInfo);
     renderActionTextDate(info);
     renderDiffBaseListBox(info);
     initReplyButton(info, revision);
@@ -1189,7 +1204,7 @@
     // render it faster.
     if (!info.status().isOpen()
         || !revision.equals(info.currentRevision())
-        || info.revision(revision).isEdit()) {
+        || revisionInfo.isEdit()) {
       setVisible(strategy, false);
     }
 
@@ -1200,7 +1215,6 @@
     quickApprove.setVisible(false);
     actions.reloadRevisionActions(emptyMap);
 
-    RevisionInfo revisionInfo = info.revision(revision);
     boolean current = revision.equals(info.currentRevision())
         && !revisionInfo.isEdit();
 
@@ -1252,10 +1266,12 @@
         : String.valueOf(info.owner()._accountId()), Change.Status.NEW));
   }
 
-  private void renderUploader(ChangeInfo info, String revision) {
-    AccountInfo uploader = info.revision(revision).uploader();
-    if (uploader == null
-        || uploader._accountId() == info.owner()._accountId()) {
+  private void renderUploader(ChangeInfo changeInfo, RevisionInfo revInfo) {
+    AccountInfo uploader = revInfo.uploader();
+    boolean isOwner = uploader == null
+        || uploader._accountId() == changeInfo.owner()._accountId();
+    renderPushCertificate(revInfo, isOwner ? ownerPanel : uploaderPanel);
+    if (isOwner) {
       uploaderRow.getStyle().setDisplay(Display.NONE);
       return;
     }
@@ -1269,6 +1285,37 @@
     uploaderName.setTitle(email(uploader, name));
   }
 
+  private void renderPushCertificate(RevisionInfo revInfo, FlowPanel panel) {
+    if (!enableSignedPush()) {
+      return;
+    }
+    Image status = new Image();
+    panel.add(status);
+    status.setStyleName(style.pushCertStatus());
+    if (!revInfo.hasPushCertificate()
+        || revInfo.pushCertificate().key() == null) {
+      status.setResource(Gerrit.RESOURCES.question());
+      status.setTitle(Util.C.pushCertMissing());
+      return;
+    }
+    PushCertificateInfo certInfo = revInfo.pushCertificate();
+    GpgKeyInfo.Status s = certInfo.key().status();
+    switch (s) {
+      case BAD:
+        status.setResource(Gerrit.RESOURCES.redNot());
+        status.setTitle(problems(Util.C.pushCertBad(), certInfo));
+        break;
+      case OK:
+        status.setResource(Gerrit.RESOURCES.warning());
+        status.setTitle(problems(Util.C.pushCertOk(), certInfo));
+        break;
+      case TRUSTED:
+        status.setResource(Gerrit.RESOURCES.greenCheck());
+        status.setTitle(Util.C.pushCertTrusted());
+        break;
+    }
+  }
+
   private static String name(AccountInfo info) {
     return info.name() != null
         ? info.name()
@@ -1279,6 +1326,21 @@
     return info.email() != null ? info.email() : name;
   }
 
+  private static String problems(String msg, PushCertificateInfo info) {
+    if (info.key() == null
+        || !info.key().hasProblems()
+        || info.key().problems().length() == 0) {
+      return msg;
+    }
+
+    StringBuilder sb = new StringBuilder();
+    sb.append(msg).append(':');
+    for (String problem : Natives.asList(info.key().problems())) {
+      sb.append('\n').append(problem);
+    }
+    return sb.toString();
+  }
+
   private void renderSubmitType(String action) {
     try {
       SubmitType type = SubmitType.valueOf(action);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
index e40b431..c643072 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
@@ -334,6 +334,10 @@
     .changeExtension {
       padding-top: 5px;
     }
+
+    .pushCertStatus {
+      padding-left: 5px;
+    }
   </ui:style>
 
   <g:HTMLPanel styleName='{style.cs2}'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index ca4c633..1fb997f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -202,4 +202,9 @@
   String diffAllUnified();
 
   String votable();
+
+  String pushCertMissing();
+  String pushCertBad();
+  String pushCertOk();
+  String pushCertTrusted();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index e348161..a5fa7b4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -184,3 +184,8 @@
 diffAllUnified = All Unified
 
 votable = Votable:
+
+pushCertMissing = This patch set was created without a push certificate
+pushCertBad = Push certificate is invalid
+pushCertOk = Push certificate is valid, but key is not trusted
+pushCertTrusted = Push certificate is valid and key is trusted