Merge "FileEdits: Simplify instance creation"
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index bc859f3..6272cda 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -318,7 +318,7 @@
                       && !r.getName().startsWith(RefNames.REFS_TAGS)
                       && !r.isSymbolic()
                       && !r.getName().equals(RefNames.REFS_CONFIG))
-          .collect(toCollection(() -> new ArrayList<>(allRefs.size())));
+          .collect(Collectors.toList());
     } catch (IOException e) {
       throw new PermissionBackendException(e);
     }
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index b27cb9b..7f434ca 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -533,6 +533,9 @@
             // Multiply the timeout by the number of projects we're actually attempting to
             // submit.
             .defaultTimeoutMultiplier(cs.projects().size())
+            // By default, we only retry lock failures. Here it's better to also retry unexpected
+            // runtime exceptions.
+            .retryOn(t -> t instanceof RuntimeException)
             .call();
         submissionExecutor.afterExecutions(orm);
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index 7f8add8..02916c7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -43,7 +43,6 @@
 import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.httpd.restapi.ParameterParser;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
-import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.validators.CommitValidationException;
@@ -713,7 +712,7 @@
 
   @Test
   @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
-  public void autoRetryWithTrace() throws Exception {
+  public void noAutoRetryIfExceptionCausesNormalRetrying() throws Exception {
     String changeId = createChange().getChangeId();
     approve(changeId);
 
@@ -723,49 +722,6 @@
       RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
       assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
       assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).startsWith("retry-on-failure-");
-      assertThat(traceSubmitRule.traceId).startsWith("retry-on-failure-");
-      assertThat(traceSubmitRule.isLoggingForced).isTrue();
-    }
-  }
-
-  @Test
-  @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
-  public void noAutoRetryIfExceptionCausesNormalRetrying() throws Exception {
-    String changeId = createChange().getChangeId();
-    approve(changeId);
-
-    TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
-    traceSubmitRule.failAlways = true;
-    try (Registration registration =
-        extensionRegistry
-            .newRegistration()
-            .add(traceSubmitRule)
-            .add(
-                new ExceptionHook() {
-                  @Override
-                  public boolean shouldRetry(String actionType, String actionName, Throwable t) {
-                    return true;
-                  }
-                })) {
-      RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
-      assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
-      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
-      assertThat(traceSubmitRule.traceId).isNull();
-      assertThat(traceSubmitRule.isLoggingForced).isFalse();
-    }
-  }
-
-  @Test
-  public void noAutoRetryWithTraceIfDisabled() throws Exception {
-    String changeId = createChange().getChangeId();
-    approve(changeId);
-
-    TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
-    traceSubmitRule.failOnce = true;
-    try (Registration registration = extensionRegistry.newRegistration().add(traceSubmitRule)) {
-      RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
-      assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
-      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
       assertThat(traceSubmitRule.traceId).isNull();
       assertThat(traceSubmitRule.isLoggingForced).isFalse();
     }
diff --git a/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java
index 933ffb4..b185558 100644
--- a/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.protobuf.Parser;
 import java.lang.reflect.Type;
 import java.sql.Timestamp;
 import org.junit.Test;
@@ -174,23 +173,6 @@
     assertThat(convertedChangeMessage).isEqualTo(changeMessage);
   }
 
-  @Test
-  public void protoCanBeParsedFromBytes() throws Exception {
-    Entities.ChangeMessage proto =
-        Entities.ChangeMessage.newBuilder()
-            .setKey(
-                Entities.ChangeMessage_Key.newBuilder()
-                    .setChangeId(Entities.Change_Id.newBuilder().setId(543))
-                    .setUuid("change-message-21"))
-            .build();
-    byte[] bytes = proto.toByteArray();
-
-    Parser<Entities.ChangeMessage> parser = changeMessageProtoConverter.getParser();
-    Entities.ChangeMessage parsedProto = parser.parseFrom(bytes);
-
-    assertThat(parsedProto).isEqualTo(proto);
-  }
-
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void fieldsExistAsExpected() {
diff --git a/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
index bca5eea..bf39ff8 100644
--- a/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.inject.TypeLiteral;
-import com.google.protobuf.Parser;
 import java.lang.reflect.Type;
 import java.sql.Timestamp;
 import java.util.Date;
@@ -165,29 +164,6 @@
     assertThat(patchSetApproval.postSubmit()).isEqualTo(false);
   }
 
-  @Test
-  public void protoCanBeParsedFromBytes() throws Exception {
-    Entities.PatchSetApproval proto =
-        Entities.PatchSetApproval.newBuilder()
-            .setKey(
-                Entities.PatchSetApproval_Key.newBuilder()
-                    .setPatchSetId(
-                        Entities.PatchSet_Id.newBuilder()
-                            .setChangeId(Entities.Change_Id.newBuilder().setId(42))
-                            .setId(14))
-                    .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
-                    .setLabelId(Entities.LabelId.newBuilder().setId("label-8")))
-            .setValue(456)
-            .setGranted(987654L)
-            .build();
-    byte[] bytes = proto.toByteArray();
-
-    Parser<Entities.PatchSetApproval> parser = protoConverter.getParser();
-    Entities.PatchSetApproval parsedProto = parser.parseFrom(bytes);
-
-    assertThat(parsedProto).isEqualTo(proto);
-  }
-
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void fieldsExistAsExpected() {
diff --git a/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
index 2519e75..efeb24f 100644
--- a/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.inject.TypeLiteral;
-import com.google.protobuf.Parser;
 import java.lang.reflect.Type;
 import java.sql.Timestamp;
 import java.util.Optional;
@@ -148,23 +147,6 @@
                 .build());
   }
 
-  @Test
-  public void protoCanBeParsedFromBytes() throws Exception {
-    Entities.PatchSet proto =
-        Entities.PatchSet.newBuilder()
-            .setId(
-                Entities.PatchSet_Id.newBuilder()
-                    .setChangeId(Entities.Change_Id.newBuilder().setId(103))
-                    .setId(73))
-            .build();
-    byte[] bytes = proto.toByteArray();
-
-    Parser<Entities.PatchSet> parser = patchSetProtoConverter.getParser();
-    Entities.PatchSet parsedProto = parser.parseFrom(bytes);
-
-    assertThat(parsedProto).isEqualTo(proto);
-  }
-
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void fieldsExistAsExpected() {
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index a0c53c6..3548ec8 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit a0c53c6c5ad1ba8f8967ed6d2bcb18995f734cad
+Subproject commit 3548ec83c0c271a8768a6b03b0c28711521ed6cf
diff --git a/polygerrit-ui/app/api/core.ts b/polygerrit-ui/app/api/core.ts
index 91c2b7b..5820139 100644
--- a/polygerrit-ui/app/api/core.ts
+++ b/polygerrit-ui/app/api/core.ts
@@ -32,7 +32,7 @@
  * however a range with end_line set to 5 and end_character equal to 0 will not
  * include any characters on line 5.
  */
-export interface CommentRange {
+export declare interface CommentRange {
   /** The start line number of the range. (1-based) */
   start_line: number;
 
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 496653f..b5624c6 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -219,7 +219,7 @@
   end_line: number;
 }
 
-export interface CoverageRange {
+export declare interface CoverageRange {
   type: CoverageType;
   side: Side;
   code_range: LineRange;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 59ce443..e361f9f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -164,6 +164,7 @@
 import {takeUntil} from 'rxjs/operators';
 import {aPluginHasRegistered} from '../../../services/checks/checks-model';
 import {Subject} from 'rxjs';
+import {GrRelatedChangesListExperimental} from '../gr-related-changes-list-experimental/gr-related-changes-list-experimental';
 
 const CHANGE_ID_ERROR = {
   MISMATCH: 'mismatch',
@@ -2296,6 +2297,9 @@
       this._editingCommitMessage = false;
       const relatedChangesLoaded = coreDataPromise.then(() => {
         this.getRelatedChangesList()?.reload();
+        if (this._isNewChangeSummaryUiEnabled) {
+          this.getRelatedChangesListExperimental()?.reload();
+        }
       });
       allDataPromises.push(relatedChangesLoaded);
     }
@@ -2819,6 +2823,12 @@
       '#relatedChanges'
     );
   }
+
+  getRelatedChangesListExperimental() {
+    return this.shadowRoot!.querySelector<GrRelatedChangesListExperimental>(
+      '#relatedChangesExperimental'
+    );
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index 06a1939..ed33916 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -594,11 +594,14 @@
                 </gr-endpoint-param>
               </gr-endpoint-decorator>
             </div>
-            <template is="dom-if" if="[[_isNewChangeSummaryUiEnabled]]">
-              <gr-related-changes-list-experimental></gr-related-changes-list-experimental>
-            </template>
-            <template is="dom-if" if="[[!_isNewChangeSummaryUiEnabled]]">
-              <div class="relatedChanges">
+            <div class="relatedChanges">
+              <template is="dom-if" if="[[_isNewChangeSummaryUiEnabled]]">
+                <gr-related-changes-list-experimental
+                  change="[[_change]]"
+                  id="relatedChangesExperimental"
+                ></gr-related-changes-list-experimental>
+              </template>
+              <template is="dom-if" if="[[!_isNewChangeSummaryUiEnabled]]">
                 <gr-related-changes-list
                   id="relatedChanges"
                   class$="[[_computeRelatedChangesClass(_relatedChangesCollapsed)]]"
@@ -620,8 +623,8 @@
                     [[_computeCollapseText(_relatedChangesCollapsed)]]
                   </gr-button>
                 </div>
-              </div>
-            </template>
+              </template>
+            </div>
           </div>
         </div>
       </div>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
index ab7566f..a66bb04 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
@@ -16,22 +16,217 @@
  */
 import {html} from 'lit-html';
 import {GrLitElement} from '../../lit/gr-lit-element';
-import {customElement} from 'lit-element';
+import {customElement, property, css} from 'lit-element';
 import {sharedStyles} from '../../../styles/shared-styles';
+import {
+  SubmittedTogetherInfo,
+  ChangeInfo,
+  RelatedChangeAndCommitInfo,
+} from '../../../types/common';
+import {appContext} from '../../../services/app-context';
+import {ParsedChangeInfo} from '../../../types/types';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {pluralize} from '../../../utils/string-util';
+import {ChangeStatus} from '../../../constants/constants';
 
+function isChangeInfo(
+  x: ChangeInfo | RelatedChangeAndCommitInfo | ParsedChangeInfo
+): x is ChangeInfo | ParsedChangeInfo {
+  return (x as ChangeInfo)._number !== undefined;
+}
 @customElement('gr-related-changes-list-experimental')
 export class GrRelatedChangesListExperimental extends GrLitElement {
+  @property()
+  change?: ParsedChangeInfo;
+
+  @property()
+  _submittedTogether?: SubmittedTogetherInfo = {
+    changes: [],
+    non_visible_changes: 0,
+  };
+
+  private readonly restApiService = appContext.restApiService;
+
   static get styles() {
-    return [sharedStyles];
+    return [
+      sharedStyles,
+      css`
+        .title {
+          font-weight: var(--font-weight-bold);
+          color: var(--deemphasized-text-color);
+          padding-left: var(--metadata-horizontal-padding);
+        }
+        h4,
+        section gr-related-change {
+          display: flex;
+        }
+        h4:before,
+        section gr-related-change:before {
+          content: ' ';
+          flex-shrink: 0;
+          width: 1.2em;
+        }
+        .note {
+          color: var(--error-text-color);
+        }
+      `,
+    ];
   }
 
   render() {
-    return html``;
+    const submittedTogetherChanges = this._submittedTogether?.changes ?? [];
+    const countNonVisibleChanges =
+      this._submittedTogether?.non_visible_changes ?? 0;
+    return html` <section
+      id="submittedTogether"
+      ?hidden=${!submittedTogetherChanges?.length &&
+      !this._submittedTogether?.non_visible_changes}
+    >
+      <h4 class="title">Submitted together</h4>
+      ${submittedTogetherChanges.map(
+        relatedChange =>
+          html`<gr-related-change
+            .currentChange="${this._changesEqual(relatedChange, this.change)}"
+            .change="${relatedChange}"
+          ></gr-related-change>`
+      )}
+      <div class="note" ?hidden=${!countNonVisibleChanges}>
+        (+ ${pluralize(countNonVisibleChanges, 'non-visible change')})
+      </div>
+    </section>`;
+  }
+
+  reload() {
+    if (!this.change) return Promise.reject(new Error('change missing'));
+    return this.restApiService
+      .getChangesSubmittedTogether(this.change._number)
+      .then(response => {
+        this._submittedTogether = response;
+      });
+  }
+
+  /**
+   * Do the given objects describe the same change? Compares the changes by
+   * their numbers.
+   */
+  _changesEqual(
+    a?: ChangeInfo | RelatedChangeAndCommitInfo,
+    b?: ChangeInfo | ParsedChangeInfo | RelatedChangeAndCommitInfo
+  ) {
+    const aNum = this._getChangeNumber(a);
+    const bNum = this._getChangeNumber(b);
+    return aNum === bNum;
+  }
+
+  /**
+   * Get the change number from either a ChangeInfo (such as those included in
+   * SubmittedTogetherInfo responses) or get the change number from a
+   * RelatedChangeAndCommitInfo (such as those included in a
+   * RelatedChangesInfo response).
+   */
+  _getChangeNumber(
+    change?: ChangeInfo | ParsedChangeInfo | RelatedChangeAndCommitInfo
+  ) {
+    // Default to 0 if change property is not defined.
+    if (!change) return 0;
+
+    if (isChangeInfo(change)) {
+      return change._number;
+    }
+    return change._change_number;
+  }
+}
+
+@customElement('gr-related-change')
+export class GrRelatedChange extends GrLitElement {
+  @property()
+  change?: ChangeInfo;
+
+  @property()
+  currentChange = false;
+
+  static get styles() {
+    return [
+      sharedStyles,
+      css`
+        a {
+          display: block;
+        }
+        .changeContainer,
+        a {
+          max-width: 100%;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+        .changeContainer {
+          display: flex;
+        }
+        .strikethrough {
+          color: var(--deemphasized-text-color);
+          text-decoration: line-through;
+        }
+        .submittableCheck {
+          padding-left: var(--spacing-s);
+          color: var(--positive-green-text-color);
+          display: none;
+        }
+        .submittableCheck.submittable {
+          display: inline;
+        }
+        .arrowToCurrentChange {
+          position: absolute;
+        }
+      `,
+    ];
+  }
+
+  render() {
+    const change = this.change;
+    if (!change) throw new Error('Missing change');
+    const linkClass = this._computeLinkClass(change);
+    return html`<span
+        role="img"
+        class="arrowToCurrentChange"
+        aria-label="Arrow marking current change"
+        ?hidden=${!this.currentChange}
+        >➔</span
+      >
+      <div class="changeContainer">
+        <a
+          href="${GerritNav.getUrlForChangeById(
+            change._number,
+            change.project
+          )}"
+          class="${linkClass}"
+          >${change.project}: ${change.branch}: ${change.subject}</a
+        >
+        <span
+          tabindex="-1"
+          title="Submittable"
+          class="submittableCheck ${linkClass}"
+          role="img"
+          aria-label="Submittable"
+          >✓</span
+        >
+      </div> `;
+  }
+
+  _computeLinkClass(change: ChangeInfo) {
+    const statuses = [];
+    if (change.status === ChangeStatus.ABANDONED) {
+      statuses.push('strikethrough');
+    }
+    if (change.submittable) {
+      statuses.push('submittable');
+    }
+    return statuses.join(' ');
   }
 }
 
 declare global {
   interface HTMLElementTagNameMap {
     'gr-related-changes-list-experimental': GrRelatedChangesListExperimental;
+    'gr-related-change': GrRelatedChange;
   }
 }