Merge changes I37ebd2bd,I952dd5aa

* changes:
  SubscriptionGraph: trim log statements
  SubscribeSection: compress multiple log lines together
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 3b0cd9a..f558d30 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -75,6 +75,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.TimeZone;
@@ -553,11 +554,15 @@
     try {
       logDebug("Executing updateRepo on %d ops", ops.size());
       RepoContextImpl ctx = new RepoContextImpl();
-      for (BatchUpdateOp op : ops.values()) {
+      for (Entry<Change.Id, BatchUpdateOp> op : ops.entries()) {
         try (TraceContext.TraceTimer ignored =
             TraceContext.newTimer(
-                op.getClass().getSimpleName() + "#updateRepo", Metadata.empty())) {
-          op.updateRepo(ctx);
+                op.getClass().getSimpleName() + "#updateRepo",
+                Metadata.builder()
+                    .projectName(project.get())
+                    .changeId(op.getKey().get())
+                    .build())) {
+          op.getValue().updateRepo(ctx);
         }
       }
 
@@ -672,7 +677,8 @@
       for (BatchUpdateOp op : e.getValue()) {
         try (TraceContext.TraceTimer ignored =
             TraceContext.newTimer(
-                op.getClass().getSimpleName() + "#updateChange", Metadata.empty())) {
+                op.getClass().getSimpleName() + "#updateChange",
+                Metadata.builder().projectName(project.get()).changeId(id.get()).build())) {
           dirty |= op.updateChange(ctx);
         }
       }
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 1591738..fcf1cf4 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -121,8 +121,6 @@
     "elements/gr-app-element_html.ts",
     "elements/settings/gr-settings-view/gr-settings-view_html.ts",
     "elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts",
-    "elements/shared/gr-account-entry/gr-account-entry_html.ts",
-    "elements/shared/gr-account-label/gr-account-label_html.ts",
     "elements/shared/gr-account-list/gr-account-list_html.ts",
     "elements/shared/gr-autocomplete/gr-autocomplete_html.ts",
     "elements/shared/gr-change-status/gr-change-status_html.ts",
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
index 944c7d5..f0a3ca1 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
@@ -36,6 +36,7 @@
 } from '../../shared/gr-autocomplete/gr-autocomplete';
 import {appContext} from '../../../services/app-context';
 import {IronInputElement} from '@polymer/iron-input';
+import {fireAlert} from '../../../utils/event-util';
 
 export interface GrEditControls {
   $: {
@@ -209,6 +210,11 @@
   }
 
   _handleOpenConfirm(e: Event) {
+    if (!this.change || !this._path) {
+      fireAlert(this, 'You must enter a path.');
+      this._closeDialog(this.$.openDialog);
+      return;
+    }
     const url = GerritNav.getEditUrlForDiff(
       this.change,
       this._path,
@@ -220,6 +226,7 @@
 
   _handleUploadConfirm(path: string, fileData: string) {
     if (!this.change || !path || !fileData) {
+      fireAlert(this, 'You must enter a path and data.');
       this._closeDialog(this.$.openDialog);
       return Promise.resolve();
     }
@@ -238,6 +245,11 @@
     // Get the dialog before the api call as the event will change during bubbling
     // which will make Polymer.dom(e).path an empty array in polymer 2
     const dialog = this._getDialogFromEvent(e);
+    if (!this.change || !this._path) {
+      fireAlert(this, 'You must enter a path.');
+      this._closeDialog(dialog);
+      return;
+    }
     this.restApiService
       .deleteFileInChangeEdit(this.change._number, this._path)
       .then(res => {
@@ -251,6 +263,11 @@
 
   _handleRestoreConfirm(e: Event) {
     const dialog = this._getDialogFromEvent(e);
+    if (!this.change || !this._path) {
+      fireAlert(this, 'You must enter a path.');
+      this._closeDialog(dialog);
+      return;
+    }
     this.restApiService
       .restoreFileInChangeEdit(this.change._number, this._path)
       .then(res => {
@@ -264,6 +281,11 @@
 
   _handleRenameConfirm(e: Event) {
     const dialog = this._getDialogFromEvent(e);
+    if (!this.change || !this._path || !this._newPath) {
+      fireAlert(this, 'You must enter a old path and a new path.');
+      this._closeDialog(dialog);
+      return;
+    }
     return this.restApiService
       .renameFileInChangeEdit(this.change._number, this._path, this._newPath)
       .then(res => {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
index 5436128..f703037 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
@@ -173,9 +173,9 @@
         <gr-account-link
           .account="${this.account}"
           .change="${this.change}"
-          ?force-attention=${this.forceAttention}
-          ?highlight-attention=${this.highlightAttention}
-          .voteable-text=${this.voteableText}
+          ?forceAttention=${this.forceAttention}
+          ?highlightAttention=${this.highlightAttention}
+          .voteableText=${this.voteableText}
         >
         </gr-account-link>
         <gr-button
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
index 944054e..c250428 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
@@ -54,13 +54,13 @@
    */
 
   @property({type: Boolean})
-  allowAnyInput?: boolean;
+  allowAnyInput = false;
 
   @property({type: Boolean})
-  borderless?: boolean;
+  borderless = false;
 
   @property({type: String})
-  placeholder?: string;
+  placeholder = '';
 
   @property({type: Number})
   suggestFrom = 0;
@@ -69,7 +69,7 @@
   querySuggestions: AutocompleteQuery = () => Promise.resolve([]);
 
   @property({type: String, observer: '_inputTextChanged'})
-  _inputText?: string;
+  _inputText = '';
 
   get focusStart() {
     return this.$.input.focusStart;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index d7078ed..cc66734 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -190,11 +190,7 @@
       : '';
   }
 
-  _computeName(
-    account?: AccountInfo,
-    config?: ServerInfo,
-    firstName?: boolean
-  ) {
+  _computeName(account: AccountInfo, firstName: boolean, config?: ServerInfo) {
     return getDisplayName(config, account, firstName);
   }
 
@@ -264,12 +260,12 @@
     highlight: boolean,
     account: AccountInfo,
     change: ChangeInfo,
-    selfAccount: AccountInfo,
-    selected: boolean
+    selected: boolean,
+    selfAccount?: AccountInfo
   ) {
     if (selected) return true;
     return (
-      this._hasUnforcedAttention(highlight, account, change) &&
+      !!this._hasUnforcedAttention(highlight, account, change) &&
       (isInvolved(change, selfAccount) || isSelf(account, selfAccount))
     );
   }
@@ -278,16 +274,16 @@
     highlight: boolean,
     account: AccountInfo,
     change: ChangeInfo,
-    selfAccount: AccountInfo,
     force: boolean,
-    selected: boolean
+    selected: boolean,
+    selfAccount?: AccountInfo
   ) {
     const enabled = this._computeAttentionButtonEnabled(
       highlight,
       account,
       change,
-      selfAccount,
-      selected
+      selected,
+      selfAccount
     );
     return enabled
       ? 'Click to remove the user from the attention set'
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
index 03178c7..352763b 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
@@ -111,9 +111,9 @@
         link=""
         aria-label="Remove user from attention set"
         on-click="_handleRemoveAttentionClick"
-        disabled="[[!_computeAttentionButtonEnabled(highlightAttention, account, change, _selfAccount, selected)]]"
-        has-tooltip="[[_computeAttentionButtonEnabled(highlightAttention, account, change, _selfAccount, false)]]"
-        title="[[_computeAttentionIconTitle(highlightAttention, account, change, _selfAccount, forceAttention, selected)]]"
+        disabled="[[!_computeAttentionButtonEnabled(highlightAttention, account, change, selected, _selfAccount)]]"
+        has-tooltip="[[_computeAttentionButtonEnabled(highlightAttention, account, change, false, _selfAccount)]]"
+        title="[[_computeAttentionIconTitle(highlightAttention, account, change, forceAttention, selected, _selfAccount)]]"
         ><iron-icon class="attention" icon="gr-icons:attention"></iron-icon>
       </gr-button>
     </template>
@@ -126,7 +126,7 @@
       <gr-avatar account="[[account]]" imageSize="32"></gr-avatar>
     </template>
     <span class="text" part="gr-account-label-text">
-      <span class="name">[[_computeName(account, _config, firstName)]]</span>
+      <span class="name">[[_computeName(account, firstName, _config)]]</span>
       <template is="dom-if" if="[[!hideStatus]]">
         <template is="dom-if" if="[[account.status]]">
           <iron-icon class="status" icon="gr-icons:calendar"></iron-icon>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.ts
index efaa9f7..574e450 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.ts
@@ -55,12 +55,12 @@
   suite('_computeName', () => {
     test('not showing anonymous', () => {
       const account = {name: 'Wyatt'};
-      assert.deepEqual(element._computeName(account), 'Wyatt');
+      assert.deepEqual(element._computeName(account, false), 'Wyatt');
     });
 
     test('showing anonymous but no config', () => {
       const account = {};
-      assert.deepEqual(element._computeName(account), 'Anonymous');
+      assert.deepEqual(element._computeName(account, false), 'Anonymous');
     });
 
     test('test for Anonymous Coward user and replace with Anonymous', () => {
@@ -71,7 +71,10 @@
         },
       };
       const account = {};
-      assert.deepEqual(element._computeName(account, config), 'Anonymous');
+      assert.deepEqual(
+        element._computeName(account, false, config),
+        'Anonymous'
+      );
     });
 
     test('test for anonymous_coward_name', () => {
@@ -82,7 +85,10 @@
         },
       };
       const account = {};
-      assert.deepEqual(element._computeName(account, config), 'TestAnon');
+      assert.deepEqual(
+        element._computeName(account, false, config),
+        'TestAnon'
+      );
     });
   });
 
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 6e37697..5145527 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -26,6 +26,7 @@
   CHANGE = 'change',
   CHANGED = 'changed',
   CHANGE_MESSAGE_DELETED = 'change-message-deleted',
+  COMMIT = 'commit',
   DIALOG_CHANGE = 'dialog-change',
   DROP = 'drop',
   EDITABLE_CONTENT_SAVE = 'editable-content-save',
@@ -58,6 +59,8 @@
     /* prettier-ignore */
     'changed': ChangedEvent;
     'change-message-deleted': ChangeMessageDeletedEvent;
+    /* prettier-ignore */
+    'commit': CommitEvent;
     'dialog-change': DialogChangeEvent;
     /* prettier-ignore */
     'drop': DropEvent;
@@ -109,6 +112,8 @@
 }
 export type ChangeMessageDeletedEvent = CustomEvent<ChangeMessageDeletedEventDetail>;
 
+export type CommitEvent = CustomEvent;
+
 // TODO(milutin) - remove once new gr-dialog will do it out of the box
 // This informs gr-app-element to remove footer, header from a11y tree
 export interface DialogChangeEventDetail {
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index e030878..ddfaeb4 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -202,6 +202,10 @@
 		moduleImportRegexp = regexp.MustCompile("(?m)^((import|export).*'/node_modules/)lit-(element|html).js';$")
 		data = moduleImportRegexp.ReplaceAll(data, []byte("${1}lit-${3}/lit-${3}.js';"))
 
+		// 'immer' imports and exports have to be resolved to 'immer/dist/immer.esm.js'.
+		moduleImportRegexp = regexp.MustCompile("(?m)^((import|export).*'/node_modules/)immer.js';$")
+		data = moduleImportRegexp.ReplaceAll(data, []byte("${1}/immer/dist/immer.esm.js';"))
+
 		if strings.HasSuffix(normalizedContentPath, "/node_modules/page/page.js") {
 			// Can't import page.js directly, because this is undefined.
 			// Replace it with window