Merge "Allow admin to create repository submit requirements"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index dc0f346..33a3b38 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1661,6 +1661,18 @@
 +
 By default `false`.
 
+[[change.allowMarkdownBase64ImagesInComments]]change.allowMarkdownBase64ImagesInComments::
++
+Allows Base64 encoded images, embedded via Markdown, to be rendered in Gerrit comments.
++
+This feature addresses the need for teams to share images directly within Gerrit comments. 
+Instead of relying on external image hosting, images are encoded into Base64 strings
+and included in the comment text using Markdown image syntax.
++
+When enabled, the Gerrit UI will detect and render these inline images.
++
+By default `false`.
+
 [[change.propagateSubmitRequirementErrors]]change.propagateSubmitRequirementErrors::
 +
 If set, requests that access the submit requirements of a change fail with an
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 73cd1f1..e066038 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -2051,6 +2051,8 @@
 link:config-gerrit.html#change.enableRobotComments[Are robot comments enabled?].
 |`conflicts_predicate_enabled`|not set if `false`|
 link:config-gerrit.html#change.conflictsPredicateEnabled[Are conflicts enabled?].
+|`allow_markdown_base64_images_in_comments`|not set if `false`|
+link:config-gerrit.html#change.allowMarkdownBase64ImagesInComments[Are markdown base64 images in comments allowed?].
 |=============================
 
 [[change-index-config-info]]
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
index a4f77a1..2d68974 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
@@ -471,7 +471,7 @@
 
   private ChangeInserter getChangeInserter(Change.Id changeId, String refName, ObjectId commitId) {
     ChangeInserter inserter = changeInserterFactory.create(changeId, commitId, refName);
-    inserter.setMessage(String.format("Uploaded patchset %d.", inserter.getPatchSetId().get()));
+    inserter.setMessage(String.format("Uploaded patch set %d.", inserter.getPatchSetId().get()));
     return inserter;
   }
 
@@ -667,7 +667,7 @@
       PatchSetInserter patchSetInserter =
           patchsetInserterFactory.create(changeNotes, patchsetId, newPatchsetCommit);
       patchSetInserter.setCheckAddPatchSetPermission(false);
-      patchSetInserter.setMessage(String.format("Uploaded patchset %d.", patchsetId.get()));
+      patchSetInserter.setMessage(String.format("Uploaded patch set %d.", patchsetId.get()));
       return patchSetInserter;
     }
 
diff --git a/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java b/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
index 80bf130..dd4549f 100644
--- a/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
@@ -23,4 +23,5 @@
   public String mergeabilityComputationBehavior;
   public Boolean enableRobotComments;
   public Boolean conflictsPredicateEnabled;
+  public Boolean allowMarkdownBase64ImagesInComments;
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 0b254ce..df94b54 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -228,6 +228,8 @@
     info.enableRobotComments = toBoolean(config.getBoolean("change", "enableRobotComments", false));
     info.conflictsPredicateEnabled =
         toBoolean(config.getBoolean("change", "conflictsPredicateEnabled", true));
+    info.allowMarkdownBase64ImagesInComments =
+        toBoolean(config.getBoolean("change", "allowMarkdownBase64ImagesInComments", false));
     return info;
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 634906e..c15b4c6 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -597,17 +597,56 @@
     ChangeApi change = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
     CherryPickInput in = new CherryPickInput();
     in.destination = "master";
-    in.message = "it generates a new patch set\n\nChange-Id: " + r.getChangeId();
-    ChangeInfo cherryInfo = change.revision(r.getCommit().name()).cherryPick(in).get();
+    ChangeInfo cherryInfo = change.current().cherryPick(in).get();
     assertThat(cherryInfo.messages).hasSize(2);
-    Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
+    Iterator<ChangeMessageInfo> cherryMessageIt = cherryInfo.messages.iterator();
     assertThat(cherryInfo.cherryPickOfChange).isEqualTo(change.get()._number);
 
     // Existing change was updated.
     assertThat(cherryInfo._number).isEqualTo(change.get()._number);
+    assertThat(cherryInfo.cherryPickOfChange).isEqualTo(change.get()._number);
     assertThat(cherryInfo.cherryPickOfPatchSet).isEqualTo(1);
-    assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
-    assertThat(cherryIt.next().message).isEqualTo("Patch Set 2: Cherry Picked from branch master.");
+    assertThat(cherryMessageIt.next().message).isEqualTo("Uploaded patch set 1.");
+    assertThat(cherryMessageIt.next().message)
+        .isEqualTo("Patch Set 2: Cherry Picked from branch master.");
+  }
+
+  @Test
+  public void restoreOldPatchSetByCherryPickToSameBranch() throws Exception {
+    Change.Id changeId =
+        changeOperations.newChange().project(project).file("a.txt").content("aContent").create();
+    ChangeApi change = gApi.changes().id(project.get(), changeId.get());
+
+    // Amend the change, deleting file a.txt and adding file b.txt.
+    changeOperations
+        .change(changeId)
+        .newPatchset()
+        .file("a.txt")
+        .delete()
+        .file("b.txt")
+        .content("bContent")
+        .create();
+
+    // Restore patch set 1 by cherry-picking it.
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "master";
+    ChangeInfo cherryInfo = change.revision(1).cherryPick(in).get();
+    assertThat(cherryInfo.messages).hasSize(3);
+    Iterator<ChangeMessageInfo> cherryMessageIt = cherryInfo.messages.iterator();
+    assertThat(cherryInfo.cherryPickOfChange).isEqualTo(change.get()._number);
+
+    // Existing change was updated.
+    assertThat(cherryInfo._number).isEqualTo(change.get()._number);
+    assertThat(cherryInfo.cherryPickOfChange).isEqualTo(change.get()._number);
+    assertThat(cherryInfo.cherryPickOfPatchSet).isEqualTo(1);
+    assertThat(cherryMessageIt.next().message).isEqualTo("Uploaded patch set 1.");
+    assertThat(cherryMessageIt.next().message).isEqualTo("Uploaded patch set 2.");
+    assertThat(cherryMessageIt.next().message)
+        .isEqualTo("Patch Set 3: Cherry Picked from branch master.");
+
+    // File a.txt has been restored and b.txt has been removed.
+    Map<String, FileInfo> files = change.current().files();
+    assertThat(files.keySet()).containsExactly("a.txt", COMMIT_MSG);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 479baf3..8fd40c3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -73,7 +73,7 @@
   @GerritConfig(name = "change.updateDelay", value = "50s")
   @GerritConfig(name = "change.disablePrivateChanges", value = "true")
   @GerritConfig(name = "change.enableRobotComments", value = "false")
-
+  @GerritConfig(name = "change.allowMarkdownBase64ImagesInComments", value = "false")
   // download
   @GerritConfig(
       name = "download.archive",
@@ -114,6 +114,7 @@
     assertThat(i.change.updateDelay).isEqualTo(50);
     assertThat(i.change.disablePrivateChanges).isTrue();
     assertThat(i.change.enableRobotComments).isNull();
+    assertThat(i.change.allowMarkdownBase64ImagesInComments).isNull();
 
     // download
     assertThat(i.download.archives).containsExactly("tar", "tbz2", "tgz", "txz");
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
index 17e24b5..66a461e 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
@@ -157,6 +157,7 @@
         .weblinks.show,
         .referenceContainer {
           display: block;
+          align-content: center;
         }
         .rightsText {
           margin-right: var(--spacing-s);
@@ -165,11 +166,15 @@
         .editing gr-button,
         .admin #editBtn {
           display: inline-block;
-          margin: var(--spacing-l) 0;
+          margin: var(--spacing-l);
         }
         .editing #editInheritFromInput {
           display: inline-block;
         }
+
+        .topLevelButtons {
+          display: flex;
+        }
       `,
     ];
   }
@@ -210,11 +215,40 @@
               }}
             ></gr-autocomplete>
           </h3>
-          <div class="weblinks ${this.weblinks?.length ? 'show' : ''}">
-            History:
-            ${this.weblinks?.map(
-              info => html`<gr-weblink .info=${info}></gr-weblink>`
-            )}
+          <div class="topLevelButtons">
+            <div class="weblinks ${this.weblinks?.length ? 'show' : ''}">
+              History:
+              ${this.weblinks?.map(
+                info => html`<gr-weblink .info=${info}></gr-weblink>`
+              )}
+            </div>
+            <div>
+              <gr-button
+                id="editBtn"
+                @click=${() => {
+                  this.handleEdit();
+                }}
+                >${this.editing ? 'Cancel' : 'Edit'}</gr-button
+              >
+              <gr-button
+                id="saveBtn"
+                class=${this.ownerOf && this.ownerOf.length === 0
+                  ? 'invisible'
+                  : ''}
+                primary
+                ?disabled=${!this.modified || this.disableSaveWithoutReview}
+                @click=${this.handleSave}
+                >Save</gr-button
+              >
+              <gr-button
+                id="saveReviewBtn"
+                class=${!this.canUpload ? 'invisible' : ''}
+                primary
+                ?disabled=${!this.modified}
+                @click=${this.handleSaveForReview}
+                >Save For Review</gr-button
+              >
+            </div>
           </div>
           ${this.sections?.map((section, index) =>
             this.renderPermissionSections(section, index)
@@ -226,33 +260,6 @@
               >Add Reference</gr-button
             >
           </div>
-          <div>
-            <gr-button
-              id="editBtn"
-              @click=${() => {
-                this.handleEdit();
-              }}
-              >${this.editing ? 'Cancel' : 'Edit'}</gr-button
-            >
-            <gr-button
-              id="saveBtn"
-              class=${this.ownerOf && this.ownerOf.length === 0
-                ? 'invisible'
-                : ''}
-              primary
-              ?disabled=${!this.modified || this.disableSaveWithoutReview}
-              @click=${this.handleSave}
-              >Save</gr-button
-            >
-            <gr-button
-              id="saveReviewBtn"
-              class=${!this.canUpload ? 'invisible' : ''}
-              primary
-              ?disabled=${!this.modified}
-              @click=${this.handleSaveForReview}
-              >Save For Review</gr-button
-            >
-          </div>
         </div>
       </div>
     `;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
index d5eb17a..6437e9f 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
@@ -137,7 +137,40 @@
               <a href="" id="inheritFromName" rel="noopener"> </a>
               <gr-autocomplete id="editInheritFromInput"> </gr-autocomplete>
             </h3>
-            <div class="weblinks">History:</div>
+            <div class="topLevelButtons">
+              <div class="weblinks">History:</div>
+              <div>
+                <gr-button
+                  aria-disabled="false"
+                  id="editBtn"
+                  role="button"
+                  tabindex="0"
+                >
+                  Edit
+                </gr-button>
+                <gr-button
+                  aria-disabled="true"
+                  disabled=""
+                  class="invisible"
+                  id="saveBtn"
+                  primary=""
+                  role="button"
+                  tabindex="-1"
+                >
+                  Save
+                </gr-button>
+                <gr-button
+                  aria-disabled="false"
+                  class="invisible"
+                  id="saveReviewBtn"
+                  primary=""
+                  role="button"
+                  tabindex="0"
+                >
+                  Save For Review
+                </gr-button>
+              </div>
+            </div>
             <div class="referenceContainer">
               <gr-button
                 aria-disabled="false"
@@ -148,37 +181,6 @@
                 Add Reference
               </gr-button>
             </div>
-            <div>
-              <gr-button
-                aria-disabled="false"
-                id="editBtn"
-                role="button"
-                tabindex="0"
-              >
-                Edit
-              </gr-button>
-              <gr-button
-                aria-disabled="true"
-                disabled=""
-                class="invisible"
-                id="saveBtn"
-                primary=""
-                role="button"
-                tabindex="-1"
-              >
-                Save
-              </gr-button>
-              <gr-button
-                aria-disabled="false"
-                class="invisible"
-                id="saveReviewBtn"
-                primary=""
-                role="button"
-                tabindex="0"
-              >
-                Save For Review
-              </gr-button>
-            </div>
           </div>
         </div>
       `
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 13c5d36..12e9a43 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -1780,7 +1780,7 @@
       '/edit:publish',
       assertUIActionInfo(this.actions.publishEdit),
       false,
-      {notify: NotifyType.NONE}
+      {notify: NotifyType.OWNER_REVIEWERS}
     );
     this.sendPublishEditEvent();
   }
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index d68cdbf..662c560 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -212,6 +212,9 @@
   @state()
   shouldRender = false;
 
+  @state()
+  runs: CheckRun[] = [];
+
   private readonly reporting = getAppContext().reportingService;
 
   private getChecksModel = resolve(this, checksModelToken);
@@ -223,6 +226,13 @@
       () => this.getChecksModel().checksSelectedAttemptNumber$,
       x => (this.selectedAttempt = x)
     );
+    subscribe(
+      this,
+      () => this.getChecksModel().allRunsSelectedPatchset$,
+      x => {
+        this.runs = x;
+      }
+    );
   }
 
   override firstUpdated() {
@@ -296,6 +306,18 @@
       attempt !== ALL_ATTEMPTS;
     const selected = this.selectedAttempt === attempt;
     return html`<div class="attemptDetail">
+      ${when(
+        typeof attempt === 'number',
+        () => html` <gr-hovercard-run
+          .run=${this.runs.find(
+            r =>
+              r.attempt === attempt &&
+              r.checkName === this.run?.checkName &&
+              r.pluginName === this.run?.pluginName
+          )}
+          .attempt=${attempt}
+        ></gr-hovercard-run>`
+      )}
       <input
         type="radio"
         id=${id}
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
index cc3ce77..b38a30e 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
@@ -20,6 +20,7 @@
 import {HovercardMixin} from '../../mixins/hovercard-mixin/hovercard-mixin';
 import {css, html, LitElement} from 'lit';
 import {checksStyles} from './gr-checks-styles';
+import {when} from 'lit/directives/when.js';
 
 // This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
 const base = HovercardMixin(LitElement);
@@ -29,6 +30,9 @@
   @property({type: Object})
   run?: RunResult | CheckRun;
 
+  @property({type: Number})
+  attempt?: number;
+
   static override get styles() {
     return [
       fontStyles,
@@ -146,6 +150,7 @@
           <div class="sectionContent">
             <h3 class="name heading-3">
               <span>${this.run.checkName}</span>
+              ${when(this.attempt, () => html`(Attempt ${this.attempt})`)}
             </h3>
           </div>
         </div>
@@ -196,6 +201,8 @@
   }
 
   private renderAttemptSection() {
+    // If an attempt is specified, we don't need to render the attempt section.
+    if (this.attempt) return;
     if (this.hideAttempts()) return;
     const attempts = this.computeAttempts();
     return html`
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 7f35f04..5731522 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -510,7 +510,7 @@
           HttpMethod.POST,
           '/edit:publish',
           undefined,
-          {notify: NotifyType.NONE},
+          {notify: NotifyType.OWNER_REVIEWERS},
           handleError
         )
         .then(res => {
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index c445817..fa2a4ff 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -131,31 +131,6 @@
           link: '$1',
           enabled: true,
         };
-
-        // Linkify schemeless URLs with proper domain structures.
-        this.repoCommentLinks['ALWAYS_LINK_SCHEMELESS'] = {
-          // (?<=\s|^|[('":[])   // Ensure the match is preceded by whitespace,
-          //                     // start of line, or one of ( ' " : [
-          // (                   // Start capture group 1
-          //   (?:               // Start non-capturing domain group
-          //     [\w-]+\.        //   Sequence of words/hyphens with dot, e.g. "a-b."
-          //   )+                // End domain group. Require at least one match
-          //   [\w-]+            // End with words/hyphens for TLD e.g. "com"
-          //   (?:               // Start optional non-capturing path/query/fragment group
-          //     [/?#]           //   Start with one of / ? #
-          //     [^\s'"]*        //   Followed by some chars that are not whitespace,
-          //                     //   ' or " (to not grab trailing quotes)
-          //   )?                // End optional path/query/fragment group
-          // )                   // End capture group 1
-          // (?=\s|$|[)'"!?.,])  // Ensure the match is followed by whitespace,
-          //                     // end of line, or one of ) ' " ! ? . ,
-          match:
-            '(?<=\\s|^|[(\'":[])((?:[\\w-]+\\.)+[\\w-]+(?:[/?#][^\\s\'"]*)?)(?=\\s|$|[)\'"!?.,])',
-          // Prepend http:// for the link href otherwise it will be treated as
-          // a relative URL.
-          link: 'http://$1',
-          enabled: true,
-        };
       }
     );
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index 38606c7..723267e 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -240,18 +240,7 @@
         element.content = url;
         await element.updateComplete;
         const a = queryAndAssert<HTMLElement>(element, 'a');
-
-        // URLs without scheme are upgraded to https:// by the
-        // ALWAYS_LINK_SCHEMELESS rule. URLs with http:// or https://
-        // are preserved by the ALWAYS_LINK_HTTP rule.
-        const isSchemeless =
-          !url.startsWith('http://') &&
-          !url.startsWith('https://') &&
-          !url.startsWith('mailto:') &&
-          !url.startsWith('/');
-        const expectedHref = isSchemeless ? `http://${url}` : url;
-
-        assert.equal(a.getAttribute('href'), expectedHref);
+        assert.equal(a.getAttribute('href'), url);
         assert.equal(a.innerText, url);
       };
 
@@ -265,14 +254,6 @@
       await checkLinking(
         'https://google.com/traces/list?project=gerrit&tid=123'
       );
-
-      await checkLinking('www.google.com');
-      await checkLinking('www.google.com/path');
-      await checkLinking('google.com');
-      await checkLinking('sub.google.co.uk');
-      await checkLinking('google-foo.com');
-      await checkLinking('google.io');
-      await checkLinking('google.com?q=1#frag');
     });
   });
 
@@ -767,18 +748,7 @@
         await element.updateComplete;
         const a = queryAndAssert<HTMLElement>(element, 'a');
         const p = queryAndAssert<HTMLElement>(element, 'p');
-
-        // URLs without scheme are upgraded to https:// by the
-        // ALWAYS_LINK_SCHEMELESS rule. URLs with http:// or https://
-        // are preserved by the ALWAYS_LINK_HTTP rule.
-        const isSchemeless =
-          !url.startsWith('http://') &&
-          !url.startsWith('https://') &&
-          !url.startsWith('mailto:') &&
-          !url.startsWith('/');
-        const expectedHref = isSchemeless ? `http://${url}` : url;
-
-        assert.equal(a.getAttribute('href'), expectedHref);
+        assert.equal(a.getAttribute('href'), url);
         assert.equal(p.innerText, url);
       };
 
@@ -789,14 +759,6 @@
       await checkLinking(
         'https://google.com/traces/list?project=gerrit&tid=123'
       );
-
-      await checkLinking('www.google.com');
-      await checkLinking('www.google.com/path');
-      await checkLinking('google.com');
-      await checkLinking('sub.google.co.uk');
-      await checkLinking('google-foo.com');
-      await checkLinking('google.io');
-      await checkLinking('google.com?q=1#frag');
     });
 
     suite('user suggest fix', () => {
diff --git a/polygerrit-ui/app/utils/label-util_test.ts b/polygerrit-ui/app/utils/label-util_test.ts
index a0b6c50..4e29079 100644
--- a/polygerrit-ui/app/utils/label-util_test.ts
+++ b/polygerrit-ui/app/utils/label-util_test.ts
@@ -27,6 +27,8 @@
   hasVotes,
   hasVoted,
   extractLabelsWithCountFrom,
+  orderSubmitRequirements,
+  StandardLabels,
 } from './label-util';
 import {
   AccountId,
@@ -901,4 +903,57 @@
       assert.isTrue(hasVoted(quickLabelInfo, account));
     });
   });
+
+  suite('orderSubmitRequirements', () => {
+    test('orders priority requirements first', () => {
+      const codeReview = {
+        ...createSubmitRequirementResultInfo(),
+        name: StandardLabels.CODE_REVIEW,
+      };
+      const codeOwners = {
+        ...createSubmitRequirementResultInfo(),
+        name: StandardLabels.CODE_OWNERS,
+      };
+      const presubmitVerified = {
+        ...createSubmitRequirementResultInfo(),
+        name: StandardLabels.PRESUBMIT_VERIFIED,
+      };
+      const customLabel = createSubmitRequirementResultInfo('Custom-Label');
+
+      const requirements = [
+        customLabel,
+        codeReview,
+        presubmitVerified,
+        codeOwners,
+      ];
+      const ordered = orderSubmitRequirements(requirements);
+
+      assert.deepEqual(ordered, [
+        codeReview,
+        codeOwners,
+        presubmitVerified,
+        customLabel,
+      ]);
+    });
+
+    test('preserves order of non-priority requirements', () => {
+      const customLabel1 = {
+        ...createSubmitRequirementResultInfo(),
+        name: 'Custom-Label-1',
+      };
+      const customLabel2 = {
+        ...createSubmitRequirementResultInfo(),
+        name: 'Custom-Label-2',
+      };
+      const customLabel3 = {
+        ...createSubmitRequirementResultInfo(),
+        name: 'Custom-Label-3',
+      };
+
+      const requirements = [customLabel2, customLabel1, customLabel3];
+      const ordered = orderSubmitRequirements(requirements);
+
+      assert.deepEqual(ordered, requirements);
+    });
+  });
 });