Append submitted info to the change REST endpoint.

Add a timestamp indicating when a change was submitted.  This info
only appears when a change has been submitted.

Feature: Issue 3579
Change-Id: I76a50214e367c2c01ef666e46e3ed6fa2d652a50
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 4ce11c4..9846329 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1084,6 +1084,7 @@
     "status": "MERGED",
     "created": "2013-02-01 09:59:32.126000000",
     "updated": "2013-02-21 11:16:36.775000000",
+    "submitted": "2013-02-21 11:16:36.615000000",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -1658,6 +1659,7 @@
     "status": "MERGED",
     "created": "2013-02-01 09:59:32.126000000",
     "updated": "2013-02-21 11:16:36.775000000",
+    "submitted": "2013-02-21 11:16:36.615000000",
     "mergeable": true,
     "insertions": 34,
     "deletions": 101,
@@ -3945,6 +3947,9 @@
 |`updated`            ||
 The link:rest-api.html#timestamp[timestamp] of when the change was last
 updated.
+|`submitted`          |only set for merged changes|
+The link:rest-api.html#timestamp[timestamp] of when the change was
+submitted.
 |`starred`            |not set if `false`|
 Whether the calling user has starred this change.
 |`reviewed`           |not set if `false`|
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 3c02969..664061e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -725,6 +725,25 @@
   }
 
   @Test
+  public void submitted() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .info().submitted).isNull();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .submit();
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .info().submitted).isNotNull();
+  }
+
+  @Test
   public void check() throws Exception {
     // TODO(dborowitz): Re-enable when ConsistencyChecker supports notedb.
     assume().that(notesMigration.enabled()).isFalse();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 5847831..1e0dc12 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -143,6 +143,7 @@
     assertThat(out.topic).isEqualTo(in.topic);
     assertThat(out.status).isEqualTo(in.status);
     assertThat(out.revisions).hasSize(1);
+    assertThat(out.submitted).isNull();
     Boolean draft = Iterables.getOnlyElement(out.revisions.values()).draft;
     assertThat(booleanToDraftStatus(draft)).isEqualTo(in.status);
     return out;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index cc7fb77..d697502 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -34,6 +34,7 @@
   public ChangeStatus status;
   public Timestamp created;
   public Timestamp updated;
+  public Timestamp submitted;
   public Boolean starred;
   public Boolean reviewed;
   public SubmitType submitType;
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 e87cadd..2c85556 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
@@ -78,6 +78,10 @@
     return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
   }
 
+  public final Timestamp submitted() {
+    return JavaSqlTimestamp_JsonSerializer.parseTimestamp(submittedRaw());
+  }
+
   public final String idAbbreviated() {
     return new Change.Key(changeId()).abbreviate();
   }
@@ -113,6 +117,7 @@
   public final native AccountInfo owner() /*-{ return this.owner; }-*/;
   private final native String createdRaw() /*-{ return this.created; }-*/;
   private final native String updatedRaw() /*-{ return this.updated; }-*/;
+  private final native String submittedRaw() /*-{ return this.submitted; }-*/;
   public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
   public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
   public final native NativeMap<LabelInfo> allLabels() /*-{ return this.labels; }-*/;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index c754a48..8f30394 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -433,6 +433,7 @@
     }
 
     out.labels = labelsFor(ctl, cd, has(LABELS), has(DETAILED_LABELS));
+    out.submitted = getSubmittedOn(cd);
 
     if (out.labels != null && has(DETAILED_LABELS)) {
       // If limited to specific patch sets but not the current patch set, don't
@@ -663,6 +664,12 @@
     }
   }
 
+  private Timestamp getSubmittedOn(ChangeData cd)
+      throws OrmException {
+    Optional<PatchSetApproval> s = cd.getSubmitApproval();
+    return s.isPresent() ? s.get().getGranted() : null;
+  }
+
   private Map<String, LabelWithStatus> labelsForClosedChange(ChangeData cd,
       LabelTypes labelTypes, boolean standard, boolean detailed)
       throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 65c7295..ae03248 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -18,6 +18,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
@@ -736,6 +737,20 @@
     return allApprovals;
   }
 
+  /**
+   * @return The submit ('SUBM') approval label
+   * @throws OrmException an error occurred reading the database.
+   */
+  public Optional<PatchSetApproval> getSubmitApproval()
+    throws OrmException {
+    for (PatchSetApproval psa : currentApprovals()) {
+      if (psa.isSubmit()) {
+        return Optional.fromNullable(psa);
+      }
+    }
+    return Optional.absent();
+  }
+
   public SetMultimap<ReviewerStateInternal, Account.Id> reviewers()
       throws OrmException {
     return approvalsUtil.getReviewers(notes(), approvals().values());