Merge "Fix copy behavior in comment textarea"
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
similarity index 62%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
rename to gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index fd2385b..521ccc4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
@@ -34,18 +35,25 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.edit.ChangeEditModifier;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.ReceiveCommitsAdvertiseRefsHook;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.Util;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testutil.DisabledReviewDb;
+import com.google.gerrit.testutil.TestChanges;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
@@ -53,11 +61,12 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
 @NoHttpd
-public class VisibleRefFilterIT extends AbstractDaemonTest {
+public class RefAdvertisementIT extends AbstractDaemonTest {
   @Inject
   private ChangeEditModifier editModifier;
 
@@ -74,12 +83,23 @@
   @Inject
   private Provider<CurrentUser> userProvider;
 
+  @Inject
+  private ChangeNoteUtil noteUtil;
+
+  @Inject
+  @AnonymousCowardName
+  private String anonymousCowardName;
+
   private AccountGroup.UUID admins;
 
-  private Change.Id c1;
-  private Change.Id c2;
+  private ChangeData c1;
+  private ChangeData c2;
+  private ChangeData c3;
+  private ChangeData c4;
   private String r1;
   private String r2;
+  private String r3;
+  private String r4;
 
   @Before
   public void setUp() throws Exception {
@@ -111,17 +131,31 @@
         .branch("branch")
         .create(new BranchInput());
 
+    // First 2 changes are merged, which means the tags pointing to them are
+    // visible.
     allow(Permission.SUBMIT, admins, "refs/for/refs/heads/*");
     PushOneCommit.Result mr = pushFactory.create(db, admin.getIdent(), testRepo)
         .to("refs/for/master%submit");
     mr.assertOkStatus();
-    c1 = mr.getChange().getId();
-    r1 = changeRefPrefix(c1);
+    c1 = mr.getChange();
+    r1 = changeRefPrefix(c1.getId());
     PushOneCommit.Result br = pushFactory.create(db, admin.getIdent(), testRepo)
         .to("refs/for/branch%submit");
     br.assertOkStatus();
-    c2 = br.getChange().getId();
-    r2 = changeRefPrefix(c2);
+    c2 = br.getChange();
+    r2 = changeRefPrefix(c2.getId());
+
+    // Second 2 changes are unmerged.
+    mr = pushFactory.create(db, admin.getIdent(), testRepo)
+        .to("refs/for/master");
+    mr.assertOkStatus();
+    c3 = mr.getChange();
+    r3 = changeRefPrefix(c3.getId());
+    br = pushFactory.create(db, admin.getIdent(), testRepo)
+        .to("refs/for/branch");
+    br.assertOkStatus();
+    c4 = br.getChange();
+    r4 = changeRefPrefix(c4.getId());
 
     try (Repository repo = repoManager.openRepository(project)) {
       // master-tag -> master
@@ -139,7 +173,7 @@
   }
 
   @Test
-  public void allRefsVisibleNoRefsMetaConfig() throws Exception {
+  public void uploadPackAllRefsVisibleNoRefsMetaConfig() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     Util.allow(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
     Util.allow(cfg, Permission.READ, admins, RefNames.REFS_CONFIG);
@@ -147,12 +181,16 @@
     saveProjectConfig(project, cfg);
 
     setApiUser(user);
-    assertRefs(
+    assertUploadPackRefs(
         "HEAD",
         r1 + "1",
         r1 + "meta",
         r2 + "1",
         r2 + "meta",
+        r3 + "1",
+        r3 + "meta",
+        r4 + "1",
+        r4 + "meta",
         "refs/heads/branch",
         "refs/heads/master",
         "refs/tags/branch-tag",
@@ -160,16 +198,20 @@
   }
 
   @Test
-  public void allRefsVisibleWithRefsMetaConfig() throws Exception {
+  public void uploadPackAllRefsVisibleWithRefsMetaConfig() throws Exception {
     allow(Permission.READ, REGISTERED_USERS, "refs/*");
     allow(Permission.READ, REGISTERED_USERS, RefNames.REFS_CONFIG);
 
-    assertRefs(
+    assertUploadPackRefs(
         "HEAD",
         r1 + "1",
         r1 + "meta",
         r2 + "1",
         r2 + "meta",
+        r3 + "1",
+        r3 + "meta",
+        r4 + "1",
+        r4 + "meta",
         "refs/heads/branch",
         "refs/heads/master",
         RefNames.REFS_CONFIG,
@@ -178,28 +220,32 @@
   }
 
   @Test
-  public void subsetOfBranchesVisibleIncludingHead() throws Exception {
+  public void uploadPackSubsetOfBranchesVisibleIncludingHead() throws Exception {
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
     deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
     setApiUser(user);
-    assertRefs(
+    assertUploadPackRefs(
         "HEAD",
         r1 + "1",
         r1 + "meta",
+        r3 + "1",
+        r3 + "meta",
         "refs/heads/master",
         "refs/tags/master-tag");
   }
 
   @Test
-  public void subsetOfBranchesVisibleNotIncludingHead() throws Exception {
+  public void uploadPackSubsetOfBranchesVisibleNotIncludingHead() throws Exception {
     deny(Permission.READ, REGISTERED_USERS, "refs/heads/master");
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
     setApiUser(user);
-    assertRefs(
+    assertUploadPackRefs(
         r2 + "1",
         r2 + "meta",
+        r4 + "1",
+        r4 + "meta",
         "refs/heads/branch",
         "refs/tags/branch-tag",
         // master branch is not visible but master-tag is reachable from branch
@@ -208,12 +254,12 @@
   }
 
   @Test
-  public void subsetOfBranchesVisibleWithEdit() throws Exception {
+  public void uploadPackSubsetOfBranchesVisibleWithEdit() throws Exception {
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
     deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
-    Change c = notesFactory.createChecked(db, project, c1).getChange();
-    PatchSet ps1 = getPatchSet(new PatchSet.Id(c1, 1));
+    Change c = notesFactory.createChecked(db, project, c1.getId()).getChange();
+    PatchSet ps1 = getPatchSet(new PatchSet.Id(c1.getId(), 1));
 
     // Admin's edit is not visible.
     setApiUser(admin);
@@ -223,59 +269,64 @@
     setApiUser(user);
     editModifier.createEdit(c, ps1);
 
-    assertRefs(
+    assertUploadPackRefs(
         "HEAD",
         r1 + "1",
         r1 + "meta",
+        r3 + "1",
+        r3 + "meta",
         "refs/heads/master",
         "refs/tags/master-tag",
-        "refs/users/01/1000001/edit-" + c1.get() + "/1");
+        "refs/users/01/1000001/edit-" + c1.getId() + "/1");
   }
 
   @Test
-  public void subsetOfRefsVisibleWithAccessDatabase() throws Exception {
+  public void uploadPackSubsetOfRefsVisibleWithAccessDatabase() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
     try {
       deny(Permission.READ, REGISTERED_USERS, "refs/heads/master");
       allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
-      Change c = notesFactory.createChecked(db, project, c1).getChange();
-      PatchSet ps1 = getPatchSet(new PatchSet.Id(c1, 1));
+      PatchSet ps1 = getPatchSet(new PatchSet.Id(c1.getId(), 1));
       setApiUser(admin);
-      editModifier.createEdit(c, ps1);
+      editModifier.createEdit(c1.change(), ps1);
       setApiUser(user);
 
-      assertRefs(
+      assertUploadPackRefs(
           // Change 1 is visible due to accessDatabase capability, even though
           // refs/heads/master is not.
           r1 + "1",
           r1 + "meta",
           r2 + "1",
           r2 + "meta",
+          r3 + "1",
+          r3 + "meta",
+          r4 + "1",
+          r4 + "meta",
           "refs/heads/branch",
           "refs/tags/branch-tag",
           // See comment in subsetOfBranchesVisibleNotIncludingHead.
           "refs/tags/master-tag",
           // All edits are visible due to accessDatabase capability.
-          "refs/users/00/1000000/edit-" + c1.get() + "/1");
+          "refs/users/00/1000000/edit-" + c1.getId() + "/1");
     } finally {
       removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
     }
   }
 
   @Test
-  public void draftRefs() throws Exception {
+  public void uploadPackDraftRefs() throws Exception {
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
 
     PushOneCommit.Result br = pushFactory.create(db, admin.getIdent(), testRepo)
         .to("refs/drafts/master");
     br.assertOkStatus();
-    Change.Id c3 = br.getChange().getId();
-    String r3 = changeRefPrefix(c3);
+    Change.Id c5 = br.getChange().getId();
+    String r5 = changeRefPrefix(c5);
 
-    // Only admin can see admin's draft change.
+    // Only admin can see admin's draft change (5).
     setApiUser(admin);
-    assertRefs(
+    assertUploadPackRefs(
         "HEAD",
         r1 + "1",
         r1 + "meta",
@@ -283,6 +334,10 @@
         r2 + "meta",
         r3 + "1",
         r3 + "meta",
+        r4 + "1",
+        r4 + "meta",
+        r5 + "1",
+        r5 + "meta",
         "refs/heads/branch",
         "refs/heads/master",
         RefNames.REFS_CONFIG,
@@ -291,12 +346,16 @@
 
     // user can't.
     setApiUser(user);
-    assertRefs(
+    assertUploadPackRefs(
         "HEAD",
         r1 + "1",
         r1 + "meta",
         r2 + "1",
         r2 + "meta",
+        r3 + "1",
+        r3 + "meta",
+        r4 + "1",
+        r4 + "meta",
         "refs/heads/branch",
         "refs/heads/master",
         "refs/tags/branch-tag",
@@ -304,7 +363,7 @@
   }
 
   @Test
-  public void noSearchingChangeCacheImpl() throws Exception {
+  public void uploadPackNoSearchingChangeCacheImpl() throws Exception {
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
 
     setApiUser(user);
@@ -320,6 +379,10 @@
           r1 + "meta",
           r2 + "1",
           r2 + "meta",
+          r3 + "1",
+          r3 + "meta",
+          r4 + "1",
+          r4 + "meta",
           "refs/heads/branch",
           "refs/heads/master",
           "refs/tags/branch-tag",
@@ -328,7 +391,7 @@
   }
 
   @Test
-  public void sequencesWithAccessDatabase() throws Exception {
+  public void uploadPackSequencesWithAccessDatabase() throws Exception {
     assume().that(notesMigration.readChangeSequence()).isTrue();
     try (Repository repo = repoManager.openRepository(allProjects)) {
       setApiUser(user);
@@ -348,6 +411,82 @@
     }
   }
 
+  @Test
+  public void receivePackListsOpenChangesAsAdditionalHaves() throws Exception {
+    ReceiveCommitsAdvertiseRefsHook.Result r = getReceivePackRefs();
+    assertThat(r.allRefs().keySet()).containsExactly(
+        // meta refs are excluded even when NoteDb is enabled.
+        "HEAD",
+        "refs/heads/branch",
+        "refs/heads/master",
+        "refs/meta/config",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag");
+    assertThat(r.additionalHaves()).containsExactly(obj(c3, 1), obj(c4, 1));
+  }
+
+  @Test
+  public void receivePackRespectsVisibilityOfOpenChanges() throws Exception {
+    allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
+    deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
+    setApiUser(user);
+
+    assertThat(getReceivePackRefs().additionalHaves())
+        .containsExactly(obj(c3, 1));
+  }
+
+  @Test
+  public void receivePackListsOnlyLatestPatchSet() throws Exception {
+    testRepo.reset(obj(c3, 1));
+    PushOneCommit.Result r = amendChange(c3.change().getKey().get());
+    r.assertOkStatus();
+    c3 = r.getChange();
+    assertThat(getReceivePackRefs().additionalHaves())
+        .containsExactly(obj(c3, 2), obj(c4, 1));
+  }
+
+  @Test
+  public void receivePackOmitsMissingObject() throws Exception {
+    // Use the tactic from ConsistencyCheckerIT to insert a new patch set with a
+    // missing object.
+    String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    try (Repository repo = repoManager.openRepository(project)) {
+      TestRepository<?> tr = new TestRepository<>(repo);
+      String subject = "Subject for missing commit";
+      Change c = new Change(c3.change());
+      PatchSet.Id psId = new PatchSet.Id(c3.getId(), 2);
+      c.setCurrentPatchSet(psId, subject, c.getOriginalSubject());
+
+      PatchSet ps = TestChanges.newPatchSet(psId, rev, admin.getId());
+      db.patchSets().insert(Collections.singleton(ps));
+      db.changes().update(Collections.singleton(c));
+
+      if (notesMigration.commitChangeWrites()) {
+        PersonIdent committer = serverIdent.get();
+        PersonIdent author = noteUtil.newIdent(
+            accountCache.get(admin.getId()).getAccount(),
+            committer.getWhen(),
+            committer,
+            anonymousCowardName);
+        tr.branch(RefNames.changeMetaRef(c3.getId()))
+            .commit()
+            .author(author)
+            .committer(committer)
+            .message(
+                "Update patch set " + psId.get() + "\n"
+                    + "\n"
+                    + "Patch-set: " + psId.get() + "\n"
+                    + "Commit: " + rev + "\n"
+                    + "Subject: " + subject + "\n")
+            .create();
+      }
+      indexer.index(db, c.getProject(), c.getId());
+    }
+
+    assertThat(getReceivePackRefs().additionalHaves())
+        .containsExactly(obj(c4, 1));
+  }
+
   /**
    * Assert that refs seen by a non-admin user match expected.
    *
@@ -356,7 +495,8 @@
    *     from the expected list before comparing to the actual results.
    * @throws Exception
    */
-  private void assertRefs(String... expectedWithMeta) throws Exception {
+  private void assertUploadPackRefs(String... expectedWithMeta)
+      throws Exception {
     try (Repository repo = repoManager.openRepository(project)) {
       assertRefs(
           repo,
@@ -391,6 +531,15 @@
     }
   }
 
+  private ReceiveCommitsAdvertiseRefsHook.Result getReceivePackRefs()
+      throws Exception {
+    ReceiveCommitsAdvertiseRefsHook hook =
+        new ReceiveCommitsAdvertiseRefsHook(queryProvider, project);
+    try (Repository repo = repoManager.openRepository(project)) {
+      return hook.advertiseRefs(repo.getAllRefs());
+    }
+  }
+
   private ProjectControl projectControl() throws Exception {
     return projectControlFactory.controlFor(project, userProvider.get());
   }
@@ -402,4 +551,12 @@
         projectControlFactory.controlFor(project, userProvider.get()),
         db, true);
   }
+
+  private static ObjectId obj(ChangeData cd, int psNum) throws Exception {
+    PatchSet.Id psId = new PatchSet.Id(cd.getId(), psNum);
+    PatchSet ps = cd.patchSet(psId);
+    assertWithMessage("%s not found in %s", psId, cd.patchSets()).that(ps)
+        .isNotNull();
+    return ObjectId.fromString(ps.getRevision().get());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
index 51c2a80..5df6caf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -16,11 +16,15 @@
 
 import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.util.MagicBranch;
@@ -46,6 +50,13 @@
   private static final Logger log = LoggerFactory
       .getLogger(ReceiveCommitsAdvertiseRefsHook.class);
 
+  @VisibleForTesting
+  @AutoValue
+  public abstract static class Result {
+    public abstract Map<String, Ref> allRefs();
+    public abstract Set<ObjectId> additionalHaves();
+  }
+
   private final Provider<InternalChangeQuery> queryProvider;
   private final Project.NameKey projectName;
 
@@ -77,28 +88,46 @@
         throw ex;
       }
     }
+    Result r = advertiseRefs(oldRefs);
+    rp.setAdvertisedRefs(r.allRefs(), r.additionalHaves());
+  }
+
+  @VisibleForTesting
+  public Result advertiseRefs(Map<String, Ref> oldRefs) {
     Map<String, Ref> r = Maps.newHashMapWithExpectedSize(oldRefs.size());
+    Set<ObjectId> allPatchSets = Sets.newHashSetWithExpectedSize(oldRefs.size());
     for (Map.Entry<String, Ref> e : oldRefs.entrySet()) {
       String name = e.getKey();
       if (!skip(name)) {
         r.put(name, e.getValue());
       }
+      if (name.startsWith(RefNames.REFS_CHANGES)) {
+        allPatchSets.add(e.getValue().getObjectId());
+      }
     }
-    rp.setAdvertisedRefs(r, advertiseOpenChanges());
+    return new AutoValue_ReceiveCommitsAdvertiseRefsHook_Result(
+        r, advertiseOpenChanges(allPatchSets));
   }
 
-  private Set<ObjectId> advertiseOpenChanges() {
+  private Set<ObjectId> advertiseOpenChanges(Set<ObjectId> allPatchSets) {
     // Advertise some recent open changes, in case a commit is based on one.
     int limit = 32;
     try {
       Set<ObjectId> r = Sets.newHashSetWithExpectedSize(limit);
       for (ChangeData cd : queryProvider.get()
+          .setRequestedFields(ImmutableSet.of(ChangeField.PATCH_SET.getName()))
           .enforceVisibility(true)
           .setLimit(limit)
           .byProjectOpen(projectName)) {
         PatchSet ps = cd.currentPatchSet();
         if (ps != null) {
-          r.add(ObjectId.fromString(ps.getRevision().get()));
+          ObjectId id = ObjectId.fromString(ps.getRevision().get());
+          // Ensure we actually observed a patch set ref pointing to this
+          // object, in case the database is out of sync with the repo and the
+          // object doesn't actually exist.
+          if (allPatchSets.contains(id)) {
+            r.add(id);
+          }
         }
       }
       return r;
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 108ac92..3ad2d257 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -59,6 +59,9 @@
       .notApproved {
         background-color: #ffd4d4;
       }
+      .labelStatus {
+        max-width: 9em;
+      }
       @media screen and (max-width: 50em), screen and (min-width: 75em) {
         :host {
           display: table;
@@ -174,6 +177,14 @@
         </span>
       </section>
     </template>
+    <template is="dom-if" if="[[_showLabelStatus]]">
+      <section>
+        <span class="title">Label Status</span>
+        <span class="value labelStatus">
+          [[_computeSubmitStatus(change.labels)]]
+        </span>
+      </section>
+    </template>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-change-metadata.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 661a296..184215e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -37,6 +37,10 @@
         type: Boolean,
         computed: '_computeShowReviewersByState(serverConfig)',
       },
+      _showLabelStatus: {
+        type: Boolean,
+        computed: '_computeShowLabelStatus(change)',
+      },
     },
 
     behaviors: [
@@ -143,5 +147,30 @@
         }
       }.bind(this));
     },
+
+    _computeShowLabelStatus: function(change) {
+      var isNewChange = change.status === this.ChangeStatus.NEW;
+      var hasLabels = Object.keys(change.labels).length > 0;
+      return isNewChange && hasLabels;
+    },
+
+    _computeSubmitStatus: function(labels) {
+      var missingLabels = [];
+      var output = '';
+      for (var label in labels) {
+        var obj = labels[label];
+        if (!obj.optional && !obj.approved) {
+          missingLabels.push(label);
+        }
+      }
+      if (missingLabels.length) {
+        output += 'Needs ';
+        output += missingLabels.join(' and ');
+        output += missingLabels.length > 1 ? ' labels' : ' label';
+      } else {
+        output = 'Ready to ubmit';
+      }
+      return output;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 22080e8..281636c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -78,6 +78,22 @@
       assert.isTrue(hasCc());
     });
 
+    test('computes submit status', function() {
+      var labels = {};
+      assert.equal(element._computeSubmitStatus(labels), 'Ready to Submit');
+      labels = { test: {} };
+      assert.equal(element._computeSubmitStatus(labels), 'Needs test Label');
+      labels.test.approved = true;
+      assert.equal(element._computeSubmitStatus(labels), 'Ready to Submit');
+      labels.test.approved = false;
+      labels.test.optional = true;
+      assert.equal(element._computeSubmitStatus(labels), 'Ready to Submit');
+      labels.test.optional = false;
+      labels.test2 = {};
+      assert.equal(element._computeSubmitStatus(labels),
+          'Needs test and test2 Labels');
+    });
+
     suite('remove reviewer votes', function() {
       var sandbox;
       setup(function() {