Merge "Add no permissions message to reply dialog labels"
diff --git a/Documentation/dev-note-db.txt b/Documentation/dev-note-db.txt
index ef35d7d..0db3785 100644
--- a/Documentation/dev-note-db.txt
+++ b/Documentation/dev-note-db.txt
@@ -45,8 +45,8 @@
 
 Account and group data is migrated to NoteDb automatically using the normal
 schema upgrade process during updates. The remainder of this section details the
-configuration options that control migration of the change data, which is an
-ongoing process.
+configuration options that control migration of the change data, which is mostly
+but not fully implemented.
 
 Change migration state is configured in `gerrit.config` with options like
 `noteDb.changes.*`. These options are undocumented outside of this file, and the
diff --git a/WORKSPACE b/WORKSPACE
index 1ffe850..891e407e 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -863,13 +863,13 @@
 maven_jar(
     name = "codemirror_minified",
     artifact = "org.webjars.npm:codemirror-minified:" + CM_VERSION,
-    sha1 = "3e8767c9293614968176fcf66cb873d6eb8b3051",
+    sha1 = "27d5d8902b0c08c049f429575ff4f931e29d1664",
 )
 
 maven_jar(
     name = "codemirror_original",
     artifact = "org.webjars.npm:codemirror:" + CM_VERSION,
-    sha1 = "879c49085a44f062554a4e4a9ac248b7083d37cf",
+    sha1 = "76088a0cdf869ae0935821ba6720b2e0ed3e9108",
 )
 
 maven_jar(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 78fd32f..8413b5a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -398,7 +398,7 @@
   }
 
   /** @return true if this user can remove a reviewer for a change. */
-  public boolean canRemoveReviewer() {
+  boolean canRemoveReviewer() {
     return canPerform(Permission.REMOVE_REVIEWER);
   }
 
diff --git a/lib/codemirror/cm.bzl b/lib/codemirror/cm.bzl
index 168ab33..fd56354 100644
--- a/lib/codemirror/cm.bzl
+++ b/lib/codemirror/cm.bzl
@@ -214,7 +214,7 @@
     "z80",
 ]
 
-CM_VERSION = "5.22.0"
+CM_VERSION = "5.24.2"
 
 TOP = "META-INF/resources/webjars/codemirror/%s" % CM_VERSION
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index c320fa1..05306ae 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -96,7 +96,7 @@
               data-action-key$="[[action.__key]]"
               data-action-type$="[[action.__type]]"
               data-label$="[[action.label]]"
-              disabled$="[[!action.enabled]]"
+              disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
               on-tap="_handleActionTap"></gr-button>
         </template>
       </section>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 8a4a5e9..2b0916d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -126,10 +126,17 @@
           ];
         },
       },
+      _hasKnownChainState: {
+        type: Boolean,
+        value: false,
+      },
       changeNum: String,
       changeStatus: String,
       commitNum: String,
-      hasParent: Boolean,
+      hasParent: {
+        type: Boolean,
+        observer: '_computeChainState',
+      },
       patchNum: String,
       commitMessage: {
         type: String,
@@ -489,6 +496,22 @@
       return key === '/' ? key : '/' + key;
     },
 
+    /**
+     * Returns true if hasParent is defined (can be either true or false).
+     * returns false otherwise.
+     * @return {boolean} hasParent
+     */
+    _computeChainState: function(hasParent) {
+      this._hasKnownChainState = true;
+    },
+
+    _calculateDisabled: function(action, hasKnownChainState) {
+      if (action.__key === 'rebase' && hasKnownChainState === false) {
+        return true;
+      }
+      return !action.enabled;
+    },
+
     _handleConfirmDialogCancel: function() {
       this._hideAllDialogs();
     },
@@ -503,19 +526,8 @@
     },
 
     _handleRebaseConfirm: function() {
-      var payload = {};
       var el = this.$.confirmRebase;
-      if (el.clearParent) {
-        // There is a subtle but important difference between setting the base
-        // to an empty string and omitting it entirely from the payload. An
-        // empty string implies that the parent should be cleared and the
-        // change should be rebased on top of the target branch. Leaving out
-        // the base implies that it should be rebased on top of its current
-        // parent.
-        payload.base = '';
-      } else if (el.base && el.base.length > 0) {
-        payload.base = el.base;
-      }
+      var payload = {base: el.base};
       this.$.overlay.close();
       el.hidden = true;
       this._fireAction('/rebase', this.revisionActions.rebase, true, payload);
@@ -617,47 +629,50 @@
     },
 
     _handleResponse: function(action, response) {
+      if (!response) { return; }
       return this.$.restAPI.getResponseObject(response).then(function(obj) {
-        switch (action.__key) {
-          case ChangeActions.REVERT:
-            this._setLabelValuesOnRevert(obj.change_id);
-            /* falls through */
-          case RevisionActions.CHERRYPICK:
-            page.show(this.changePath(obj._number));
-            break;
-          case ChangeActions.DELETE:
-          case RevisionActions.DELETE:
-            if (action.__type === ActionType.CHANGE) {
-              page.show('/');
-            } else {
-              page.show(this.changePath(this.changeNum));
-            }
-            break;
-          default:
-            this.dispatchEvent(new CustomEvent('reload-change',
-                {detail: {action: action.__key}, bubbles: false}));
-            break;
-        }
+          switch (action.__key) {
+            case ChangeActions.REVERT:
+              this._setLabelValuesOnRevert(obj.change_id);
+              /* falls through */
+            case RevisionActions.CHERRYPICK:
+              page.show(this.changePath(obj._number));
+              break;
+            case ChangeActions.DELETE:
+            case RevisionActions.DELETE:
+              if (action.__type === ActionType.CHANGE) {
+                page.show('/');
+              } else {
+                page.show(this.changePath(this.changeNum));
+              }
+              break;
+            default:
+              this.dispatchEvent(new CustomEvent('reload-change',
+                  {detail: {action: action.__key}, bubbles: false}));
+              break;
+          }
       }.bind(this));
     },
 
     _handleResponseError: function(response) {
-      if (response.ok) { return response; }
-
       return response.text().then(function(errText) {
-        alert('Could not perform action: ' + errText);
-        throw Error(errText);
-      });
+        this.fire('show-alert',
+            { message: 'Could not perform action: ' + errText });
+        if (errText.indexOf('Change is already up to date') !== 0) {
+          throw Error(errText);
+        }
+      }.bind(this));
     },
 
     _send: function(method, payload, actionEndpoint, revisionAction,
-        cleanupFn) {
+        cleanupFn, opt_errorFn) {
       var url = this.$.restAPI.getChangeActionURL(this.changeNum,
           revisionAction ? this.patchNum : null, actionEndpoint);
-      return this.$.restAPI.send(method, url, payload).then(function(response) {
-        cleanupFn.call(this);
-        return response;
-      }.bind(this)).then(this._handleResponseError.bind(this));
+      return this.$.restAPI.send(method, url, payload,
+          this._handleResponseError, this).then(function(response) {
+            cleanupFn.call(this);
+            return response;
+      }.bind(this));
     },
 
     _handleAbandonTap: function() {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 9d27ddc..c0293ad 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -252,6 +252,33 @@
       });
     });
 
+    test('chain state', function() {
+      assert.equal(element._hasKnownChainState, false);
+      element.hasParent = true;
+      assert.equal(element._hasKnownChainState, true);
+      element.hasParent = false;
+    });
+
+    test('_calculateDisabled', function() {
+      var hasKnownChainState = false;
+      var action = {__key: 'rebase', enabled: true};
+      assert.equal(
+          element._calculateDisabled(action, hasKnownChainState), true);
+
+      action.__key = 'delete';
+      assert.equal(
+          element._calculateDisabled(action, hasKnownChainState), false);
+
+      action.__key = 'rebase';
+      hasKnownChainState = true;
+      assert.equal(
+          element._calculateDisabled(action, hasKnownChainState), false);
+
+      action.enabled = false;
+      assert.equal(
+          element._calculateDisabled(action, hasKnownChainState), true);
+    });
+
     test('rebase change', function(done) {
       var fireActionStub = sinon.stub(element, '_fireAction');
       flush(function() {
@@ -266,18 +293,20 @@
           method: 'POST',
           title: 'Rebase onto tip of branch or parent change',
         };
+        // rebase on other
         element.$.confirmRebase.base = '1234';
         element._handleRebaseConfirm();
         assert.deepEqual(fireActionStub.lastCall.args,
           ['/rebase', rebaseAction, true, {base: '1234'}]);
 
-        element.$.confirmRebase.base = '';
+        // rebase on parent
+        element.$.confirmRebase.base = null;
         element._handleRebaseConfirm();
         assert.deepEqual(fireActionStub.lastCall.args,
-          ['/rebase', rebaseAction, true, {}]);
+          ['/rebase', rebaseAction, true, {base: null}]);
 
-        element.$.confirmRebase.base = 'does not matter';
-        element.$.confirmRebase.clearParent = true;
+        // rebase on tip
+        element.$.confirmRebase.base = '';
         element._handleRebaseConfirm();
         assert.deepEqual(fireActionStub.lastCall.args,
           ['/rebase', rebaseAction, true, {base: ''}]);
@@ -288,6 +317,7 @@
     });
 
     test('two dialogs are not shown at the same time', function(done) {
+      element._hasKnownChainState = true;
       flush(function() {
         var rebaseButton = element.$$('gr-button[data-action-key="rebase"]');
         assert.ok(rebaseButton);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
index 129e325..c53a741 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -31,7 +31,7 @@
       label {
         cursor: pointer;
       }
-      .info {
+      .message {
         font-style: italic;
       }
       .parentRevisionContainer label,
@@ -43,45 +43,66 @@
       .parentRevisionContainer label {
         margin-bottom: .2em;
       }
-      .clearParentContainer {
+      .rebaseOption {
         margin: .5em 0;
       }
     </style>
     <gr-confirm-dialog
         confirm-label="Rebase"
         on-confirm="_handleConfirmTap"
-        on-cancel="_handleCancelTap"
-        disabled="[[!valueSelected]]">
+        on-cancel="_handleCancelTap">
       <div class="header">Confirm rebase</div>
       <div class="main">
-        <div class="parentRevisionContainer">
-          <label for="parentInput">
-            Parent revision
-            <span id="optionalText" hidden$="[[!rebaseOnCurrent]]"> (optional)
-            </span>
-            <span hidden$="[[rebaseOnCurrent]]"> (not current branch)</span>
+        <div id="rebaseOnParentContainer" class="rebaseOption"
+            hidden$="[[!_displayParentOption(rebaseOnCurrent, hasParent)]]">
+          <input id="rebaseOnParent"
+              name="rebaseOptions"
+              type="radio"
+              on-tap="_handleRebaseOnParent">
+          <label id="rebaseOnParentLabel" for="rebaseOnParent">
+            Rebase on parent change
           </label>
+        </div>
+        <div id="parentUpToDateMsg" class="message"
+            hidden$="[[!_displayParentUpToDateMsg(rebaseOnCurrent, hasParent)]]">
+          This change is up to date with its parent.
+        </div>
+        <div id="rebaseOnTip" class="rebaseOption"
+            hidden$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]">
+          <input id="rebaseOnTip"
+              name="rebaseOptions"
+              type="radio"
+              disabled$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]"
+              on-tap="_handleRebaseOnTip">
+          <label id="rebaseOnTipLabel" for="rebaseOnTip">
+            Rebase on top of the [[branch]] branch<span hidden="[[!hasParent]]">
+              (breaks relation chain)
+            </span>
+          </label>
+        </div>
+        <div id="tipUpToDateMsg" class="message"
+            hidden$="[[_displayTipOption(rebaseOnCurrent, hasParent)]]">
+          Change is up to date with the target branch already ([[branch]])
+        </div>
+        <div id="rebaseOnOther" class="rebaseOption">
+          <input id="rebaseOnOtherInput"
+              name="rebaseOptions"
+              type="radio"
+              on-tap="_handleRebaseOnOther">
+          <label id="rebaseOnOtherLabel" for="rebaseOnOtherInput">
+            Rebase on a specific change or ref <span hidden="[[!hasParent]]">
+              (breaks relation chain)
+            </span>
+          </label>
+        </div>
+        <div class="parentRevisionContainer">
           <input is="iron-input"
               type="text"
               id="parentInput"
               bind-value="{{base}}"
+              on-tap="_handleEnterChangeNumberTap"
               placeholder="Change number">
         </div>
-        <div class="clearParentContainer">
-          <input id="clearParent"
-              hidden$="[[!rebaseOnCurrent]]"
-              type="checkbox"
-              on-tap="_handleClearParentTap"
-              disabled="[[!rebaseOnCurrent]]">
-          <label id="clearParentLabel" for="clearParent"
-              hidden$="[[!rebaseOnCurrent]]">
-            Rebase on top of current branch (clear parent revision).
-          </label>
-          <p id="rebaseUpToDateInfo" class="info" for="clearParent"
-              hidden$="[[rebaseOnCurrent]]">
-            Already up to date with current branch.
-          </p>
-        </div>
       </div>
     </gr-confirm-dialog>
   </template>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
index 47fbee0..e1fbc09 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -33,15 +33,23 @@
       base: String,
       branch: String,
       hasParent: Boolean,
-      clearParent: {
-        type: Boolean,
-        value: false,
-      },
       rebaseOnCurrent: Boolean,
-      valueSelected: {
-        type: Boolean,
-        computed: '_updateValueSelected(base, clearParent)',
-      },
+    },
+
+    observers: [
+      '_updateSelectedOption(rebaseOnCurrent, hasParent)',
+    ],
+
+    _displayParentOption: function(rebaseOnCurrent, hasParent) {
+      return hasParent && rebaseOnCurrent;
+    },
+
+    _displayParentUpToDateMsg: function(rebaseOnCurrent, hasParent) {
+      return hasParent && !rebaseOnCurrent;
+    },
+
+    _displayTipOption: function(rebaseOnCurrent, hasParent) {
+      return !(!rebaseOnCurrent && !hasParent);
     },
 
     _handleConfirmTap: function(e) {
@@ -54,17 +62,44 @@
       this.fire('cancel', null, {bubbles: false});
     },
 
-    _handleClearParentTap: function(e) {
-      var clear = Polymer.dom(e).rootTarget.checked;
-      if (clear) {
-        this.base = '';
-      }
-      this.$.parentInput.disabled = clear;
-      this.clearParent = clear;
+    _handleRebaseOnOther: function(e) {
+      this.$.parentInput.focus();
     },
 
-    _updateValueSelected: function(base, clearParent) {
-      return base.length || clearParent;
+    /**
+     * There is a subtle but important difference between setting the base to an
+     * empty string and omitting it entirely from the payload. An empty string
+     * implies that the parent should be cleared and the change should be
+     * rebased on top of the target branch. Leaving out the base implies that it
+     * should be rebased on top of its current parent.
+     */
+    _handleRebaseOnTip: function(e) {
+      this.base = '';
+    },
+
+    _handleRebaseOnParent: function(e) {
+      this.base = null;
+    },
+
+    _handleEnterChangeNumberTap: function(e) {
+      this.$.rebaseOnOtherInput.checked = true;
+    },
+
+    /**
+     * Sets the default radio button based on the state of the app and
+     * the corresponding value to be submitted.
+     */
+    _updateSelectedOption: function(rebaseOnCurrent, hasParent) {
+      if (this._displayParentOption(rebaseOnCurrent, hasParent)) {
+        this.$.rebaseOnParent.checked = true;
+        this._handleRebaseOnParent();
+      } else if (this._displayTipOption(rebaseOnCurrent, hasParent)) {
+        this.$.rebaseOnTip.checked = true;
+        this._handleRebaseOnTip();
+      } else {
+        this.$.rebaseOnOtherInput.checked = true;
+        this._handleRebaseOnOther();
+      }
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
index d6e63d3..4f9bc96 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -38,47 +38,48 @@
       element = fixture('basic');
     });
 
-    test('controls with rebase on current available', function() {
+    test('controls with parent and rebase on current available', function() {
       element.rebaseOnCurrent = true;
+      element.hasParent = true;
       flushAsynchronousOperations();
-      // The correct content is hidden/displayed regarding the ability to rebase
-      // on top of the current branch.
-      assert.isFalse(element.$.clearParentLabel.hasAttribute('hidden'));
-      assert.isTrue(element.$.rebaseUpToDateInfo.hasAttribute('hidden'));
-      assert.isFalse(element.$.optionalText.hasAttribute('hidden'));
-
-      assert.isFalse(!!element.valueSelected);
-      assert.isFalse(element.$.parentInput.hasAttribute('disabled'));
-      assert.isFalse(element.$.clearParent.hasAttribute('disabled'));
-      assert.isFalse(element.$.clearParent.checked);
-      element.base = 'something great';
-      assert.isTrue(!!element.valueSelected);
-      MockInteractions.tap(element.$.clearParent);
-      assert.isTrue(!!element.valueSelected);
-      assert.isTrue(element.$.parentInput.hasAttribute('disabled'));
-      assert.isTrue(element.$.clearParent.checked);
-      assert.equal(element.base, '');
-      MockInteractions.tap(element.$.clearParent);
-      assert.isFalse(!!element.valueSelected);
+      assert.isTrue(element.$.rebaseOnParent.checked);
+      assert.isFalse(element.$.rebaseOnParentContainer.hasAttribute('hidden'));
+      assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden'));
+      assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden'));
+      assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden'));
     });
 
-    test('controls without rebase on current available', function() {
+    test('controls with parent rebase on current not available', function() {
       element.rebaseOnCurrent = false;
+      element.hasParent = true;
       flushAsynchronousOperations();
-      // The correct content is hidden/displayed regarding the ability to rebase
-      // on top of the current branch.
-      assert.isTrue(element.$.clearParentLabel.hasAttribute('hidden'));
-      assert.isFalse(element.$.rebaseUpToDateInfo.hasAttribute('hidden'));
-      assert.isTrue(element.$.optionalText.hasAttribute('hidden'));
+      assert.isTrue(element.$.rebaseOnTip.checked);
+      assert.isTrue(element.$.rebaseOnParentContainer.hasAttribute('hidden'));
+      assert.isFalse(element.$.parentUpToDateMsg.hasAttribute('hidden'));
+      assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden'));
+      assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden'));
+    });
 
-      assert.isFalse(!!element.valueSelected);
-      assert.isFalse(element.$.parentInput.hasAttribute('disabled'));
-      assert.isTrue(element.$.clearParent.hasAttribute('disabled'));
-      assert.isTrue(element.$.clearParentLabel.hasAttribute('hidden'));
-      assert.isFalse(element.$.rebaseUpToDateInfo.hasAttribute('hidden'));
+    test('controls without parent and rebase on current available', function() {
+      element.rebaseOnCurrent = true;
+      element.hasParent = false;
+      flushAsynchronousOperations();
+      assert.isTrue(element.$.rebaseOnTip.checked);
+      assert.isTrue(element.$.rebaseOnParentContainer.hasAttribute('hidden'));
+      assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden'));
+      assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden'));
+      assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden'));
+    });
 
-      element.base = 'something great';
-      assert.isTrue(!!element.valueSelected);
+    test('controls without parent rebase on current not available', function() {
+      element.rebaseOnCurrent = false;
+      element.hasParent = false;
+      flushAsynchronousOperations();
+      assert.isTrue(element.$.rebaseOnOtherInput.checked);
+      assert.isTrue(element.$.rebaseOnParentContainer.hasAttribute('hidden'));
+      assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden'));
+      assert.isTrue(element.$.rebaseOnTip.hasAttribute('hidden'));
+      assert.isFalse(element.$.tipUpToDateMsg.hasAttribute('hidden'));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
index 7c888da..bcd9053 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -49,8 +49,6 @@
       input {
         font-family: var(--monospace-font-family);
         font-size: inherit;
-        margin-bottom: .5em;
-        width: 60em;
       }
       li[selected] gr-button {
         color: #000;
@@ -71,6 +69,19 @@
         justify-content: space-between;
         padding-top: .75em;
       }
+      .command {
+        display: flex;
+        flex-wrap: wrap;
+        margin-bottom: .5em;
+        width: 60em;
+      }
+      .command label {
+        flex: 0 0 100%;
+      }
+      .copyCommand {
+        flex-grow: 1;
+        margin-right: .3em;
+      }
       .closeButtonContainer {
         display: flex;
         flex: 1;
@@ -112,10 +123,14 @@
         <div class="command">
           <label>[[command.title]]</label>
           <input is="iron-input"
+              class="copyCommand"
               type="text"
               bind-value="[[command.command]]"
               on-tap="_handleInputTap"
               readonly>
+          <gr-button class="copyToClipboard" on-tap="_copyToClipboard">
+            copy
+          </gr-button>
         </div>
       </template>
     </main>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
index 72425a4..41f6792 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -51,7 +51,11 @@
     ],
 
     focus: function() {
-      this.$.download.focus();
+      if (this._schemes.length) {
+        this.$$('.copyToClipboard').focus();
+      } else {
+        this.$.download.focus();
+      }
     },
 
     getFocusStops: function() {
@@ -162,5 +166,13 @@
         this._selectedScheme = schemes.sort()[0];
       }
     },
+
+    _copyToClipboard: function(e) {
+      e.target.parentElement.querySelector('.copyCommand').select();
+      document.execCommand('copy');
+      getSelection().removeAllRanges();
+      e.target.textContent = 'done';
+      setTimeout(function() { e.target.textContent = 'copy'; }, 1000);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index 7d80c09..ce28d4e 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -97,6 +97,45 @@
     };
   }
 
+  function getChangeObjectNoFetch() {
+    return {
+      current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72',
+      revisions: {
+        '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
+          _number: 1,
+          fetch: {},
+        }
+      }
+    };
+  }
+
+  suite('gr-download-dialog tests with no fetch options', function() {
+    var element;
+
+    setup(function() {
+      element = fixture('basic');
+      element.change = getChangeObjectNoFetch();
+      element.patchNum = 1;
+      element.config = {
+        schemes: {
+          'anonymous http': {},
+          http: {},
+          repo: {},
+          ssh: {},
+        },
+        archives: ['tgz', 'tar', 'tbz2', 'txz'],
+      };
+    });
+
+    test('focuses on first download link if no copy links', function() {
+      flushAsynchronousOperations();
+      var focusStub = sinon.stub(element.$.download, 'focus');
+      element.focus();
+      assert.isTrue(focusStub.called);
+      focusStub.restore();
+    });
+  });
+
   suite('gr-download-dialog tests', function() {
     var element;
 
@@ -115,14 +154,23 @@
       };
     });
 
-    test('focuses on first download link', function() {
-      var focusStub = sinon.stub(element.$.download, 'focus');
+    test('focuses on first copy link', function() {
+      flushAsynchronousOperations();
+      var focusStub = sinon.stub(element.$$('.copyToClipboard'), 'focus');
       element.focus();
       flushAsynchronousOperations();
       assert.isTrue(focusStub.called);
       focusStub.restore();
     });
 
+    test('copy to clipboard', function() {
+      flushAsynchronousOperations();
+      var clipboardSpy = sinon.spy(element, '_copyToClipboard');
+      var copyBtn = element.$$('.copyToClipboard');
+      MockInteractions.tap(copyBtn);
+      assert.isTrue(clipboardSpy.called);
+    });
+
     test('element visibility', function() {
       assert.isFalse(element.$$('ul').hasAttribute('hidden'));
       assert.isFalse(element.$$('main').hasAttribute('hidden'));
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
index 7c1759d..05dfbcf 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -21,7 +21,8 @@
 <dom-module id="gr-messages-list">
   <template>
     <style>
-      :host {
+      :host,
+      .messageListControls {
         display: block;
       }
       .header {
@@ -30,6 +31,7 @@
         margin-bottom: .35em;
       }
       .header,
+      #messageControlsContainer,
       gr-message {
         padding: 0 var(--default-horizontal-margin);
       }
@@ -40,10 +42,19 @@
         0% { background-color: #fff9c4; }
         100% { background-color: #fff; }
       }
+      #messageControlsContainer {
+        align-items: center;
+        background-color: #fef;
+        display: flex;
+        justify-content: center;
+      }
+      #messageControlsContainer gr-button {
+        padding: 0.4em;
+      }
     </style>
     <div class="header">
       <h3>Messages</h3>
-      <div>
+      <div class="messageListControls">
         <gr-button id="collapse-messages" link
             on-tap="_handleExpandCollapseTap">
           [[_computeExpandCollapseMessage(_expanded)]]
@@ -59,9 +70,21 @@
         </span>
       </div>
     </div>
+    <span
+        id="messageControlsContainer"
+        hidden$="[[_computeShowHideTextHidden(_visibleMessages.length, _processedMessages, _hideAutomated)]]">
+      <gr-button id="oldMessagesBtn" link on-tap="_handleShowAllTap">
+          [[_computeNumMessagesText(_visibleMessages.length, _processedMessages, _hideAutomated)]]
+      </gr-button>
+      /
+      <gr-button id="incrementMessagesBtn" link
+          on-tap="_handleIncrementShownMessages">
+        [[_computeIncrementText(_visibleMessages.length, _processedMessages, _hideAutomated)]]
+      </gr-button>
+    </span>
     <template
         is="dom-repeat"
-        items="[[_computeItems(messages, reviewerUpdates)]]"
+        items="[[_visibleMessages]]"
         as="message">
       <gr-message
           change-num="[[changeNum]]"
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index fbbaadb..33dec56 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -14,6 +14,9 @@
 (function() {
   'use strict';
 
+  var MAX_INITIAL_SHOWN_MESSAGES = 5;
+  var MESSAGES_INCREMENT = 5;
+
   Polymer({
     is: 'gr-messages-list',
 
@@ -42,6 +45,21 @@
         type: Boolean,
         value: false,
       },
+      /**
+       * The messages after processing and including merged reviewer updates.
+       */
+      _processedMessages: {
+        type: Array,
+        computed: '_computeItems(messages, reviewerUpdates)',
+        observer: '_processedMessagesChanged',
+      },
+      /**
+       * The subset of _processedMessages that is visible to the user.
+       */
+      _visibleMessages: {
+        type: Array,
+        value: function() { return []; },
+      },
     },
 
     scrollToMessage: function(messageID) {
@@ -59,6 +77,11 @@
       this._highlightEl(el);
     },
 
+    _isAutomated: function(message) {
+      return !!(message.reviewer ||
+          (message.tag && message.tag.indexOf('autogenerated') === 0));
+    },
+
     _computeItems: function(messages, reviewerUpdates) {
       messages = messages || [];
       reviewerUpdates = reviewerUpdates || [];
@@ -70,6 +93,7 @@
       for (var i = 0; i < messages.length; i++) {
         messages[i]._index = i;
       }
+
       while (mi < messages.length || ri < reviewerUpdates.length) {
         if (mi >= messages.length) {
           result = result.concat(reviewerUpdates.slice(ri));
@@ -124,6 +148,7 @@
 
     _handleAutomatedMessageToggleTap: function(e) {
       e.preventDefault();
+
       this._hideAutomated = !this._hideAutomated;
     },
 
@@ -133,8 +158,7 @@
 
     _hasAutomatedMessages: function(messages) {
       for (var i = 0; messages && i < messages.length; i++) {
-        if (messages[i].reviewer || (messages[i].tag &&
-            messages[i].tag.indexOf('autogenerated') === 0)) {
+        if (this._isAutomated(messages[i])) {
           return true;
         }
       }
@@ -196,5 +220,76 @@
       }
       return msgComments;
     },
+
+    /**
+     * Returns the number of messages to splice to the beginning of
+     * _visibleMessages. This is the minimum of the total number of messages
+     * remaining in the list and the number of messages needed to display five
+     * more visible messages in the list.
+     */
+    _getDelta: function(numVisible, messages, hideAutomated) {
+      var delta = MESSAGES_INCREMENT;
+      var msgsRemaining = messages.length - numVisible;
+      if (hideAutomated) {
+        var counter = 0;
+        var i;
+        for (i = msgsRemaining; i > 0 && counter < MESSAGES_INCREMENT; i--) {
+          if (!this._isAutomated(messages[i - 1])) { counter++; }
+        }
+        delta = msgsRemaining - i;
+      }
+      return Math.min(msgsRemaining, delta);
+    },
+
+    /**
+     * Gets the number of messages that would be visible, but do not currently
+     * exist in _visibleMessages.
+     */
+    _numRemaining: function(numVisible, messages, hideAutomated) {
+      var total = hideAutomated ?
+          messages.filter(function(msg) {
+            return !this._isAutomated(msg);
+          }.bind(this)).length :
+          messages.length;
+      return total - numVisible;
+    },
+
+    _computeIncrementText: function(numVisible, messages, hideAutomated) {
+      var delta = this._getDelta(numVisible, messages, hideAutomated);
+      delta = Math.min(
+          this._numRemaining(numVisible, messages, hideAutomated), delta);
+      return 'Show ' + Math.min(MESSAGES_INCREMENT, delta) + ' more';
+    },
+
+    _computeShowHideTextHidden: function(numVisible, messages, hideAutomated) {
+      if (numVisible >= messages.length) { return true; }
+      if (!hideAutomated) {
+        return numVisible >= messages.length;
+      }
+      var hiddenMessages = messages.slice(0, messages.length - numVisible);
+      return this._hasAutomatedMessages(hiddenMessages);
+    },
+
+    _handleShowAllTap: function() {
+      this._visibleMessages = this._processedMessages;
+    },
+
+    _handleIncrementShownMessages: function() {
+      var len = this._visibleMessages.length;
+      var delta = this._getDelta(len, this._processedMessages,
+          this._hideAutomated);
+      var newMessages = this._processedMessages.slice(-(len + delta), -len);
+      // Add newMessages to the beginning of _visibleMessages
+      this.splice.apply(this, ['_visibleMessages', 0, 0].concat(newMessages));
+    },
+
+    _processedMessagesChanged: function(messages) {
+      this._visibleMessages = messages.slice(-MAX_INITIAL_SHOWN_MESSAGES);
+    },
+
+    _computeNumMessagesText: function(numVisible, messages, hideAutomated) {
+      var total = this._numRemaining(numVisible, messages, hideAutomated);
+      return total === 1 ? 'Show 1 message' : 'Show all ' + total + ' messages';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index d22cfd5..079e27c 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -31,24 +31,34 @@
 </test-fixture>
 
 <script>
+
+  var randomMessage = function(opt_params) {
+    var params = opt_params || {};
+    var author1 = {
+      _account_id: 1115495,
+      name: 'Andrew Bonventre',
+      email: 'andybons@chromium.org',
+    };
+    return {
+      id: params.id || Math.random().toString(),
+      date: params.date || '2016-01-12 20:28:33.038000',
+      message: params.message || Math.random().toString(),
+      _revision_number: params._revision_number || 1,
+      author: params.author || author1,
+    };
+  };
+
+  var randomAutomated = function(opt_params) {
+    return Object.assign({tag: 'autogenerated:gerrit:replace'},
+        randomMessage(opt_params));
+  };
+
   suite('gr-messages-list tests', function() {
     var element;
     var messages;
 
-    var randomMessage = function(opt_params) {
-      var params = opt_params || {};
-      var author1 = {
-        _account_id: 1115495,
-        name: 'Andrew Bonventre',
-        email: 'andybons@chromium.org',
-      };
-      return {
-        id: params.id || Math.random().toString(),
-        date: params.date || '2016-01-12 20:28:33.038000',
-        message: params.message || Math.random().toString(),
-        _revision_number: params._revision_number || 1,
-        author: params.author || author1,
-      };
+    var getMessages = function() {
+      return Polymer.dom(element.root).querySelectorAll('gr-message');
     };
 
     setup(function() {
@@ -62,25 +72,72 @@
       flushAsynchronousOperations();
     });
 
+    test('show some old messages', function() {
+      assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+      element.messages = _.times(11, randomMessage);
+      flushAsynchronousOperations();
+
+      assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+      assert.equal(getMessages().length, 5);
+      assert.equal(element.$.incrementMessagesBtn.innerText,
+          'Show 5 more');
+      MockInteractions.tap(element.$.incrementMessagesBtn);
+      flushAsynchronousOperations();
+
+      assert.equal(getMessages().length, 10);
+      assert.equal(element.$.incrementMessagesBtn.innerText,
+          'Show 1 more');
+      MockInteractions.tap(element.$.incrementMessagesBtn);
+      flushAsynchronousOperations();
+
+      assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+      assert.equal(getMessages().length, 11);
+    });
+
+    test('show all old messages', function() {
+      assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+      element.messages = _.times(11, randomMessage);
+      flushAsynchronousOperations();
+
+      assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+      assert.equal(getMessages().length, 5);
+      assert.equal(element.$.oldMessagesBtn.innerText, 'Show all 6 messages');
+      MockInteractions.tap(element.$.oldMessagesBtn);
+      flushAsynchronousOperations();
+
+      assert.equal(getMessages().length, 11);
+      assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+    });
+
+    test('message count respects automated', function() {
+      element.messages = _.times(3, randomAutomated)
+          .concat(_.times(3, randomMessage));
+      flushAsynchronousOperations();
+
+      assert.equal(element.$.oldMessagesBtn.innerText, 'Show 1 message');
+      assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+      MockInteractions.tap(element.$.automatedMessageToggle);
+      flushAsynchronousOperations();
+
+      assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+    });
+
     test('expand/collapse all', function() {
-      var allMessageEls =
-          Polymer.dom(element.root).querySelectorAll('gr-message');
+      var allMessageEls = getMessages();
       for (var i = 0; i < allMessageEls.length; i++) {
         allMessageEls[i].expanded = false;
       }
       MockInteractions.tap(allMessageEls[1]);
       assert.isTrue(allMessageEls[1].expanded);
 
-      MockInteractions.tap(element.$$('.header gr-button'));
-      allMessageEls =
-          Polymer.dom(element.root).querySelectorAll('gr-message');
+      MockInteractions.tap(element.$$('#collapse-messages'));
+      allMessageEls = getMessages();
       for (var i = 0; i < allMessageEls.length; i++) {
         assert.isTrue(allMessageEls[i].expanded);
       }
 
       MockInteractions.tap(element.$$('#collapse-messages'));
-      allMessageEls =
-          Polymer.dom(element.root).querySelectorAll('gr-message');
+      allMessageEls = getMessages();
       for (var i = 0; i < allMessageEls.length; i++) {
         assert.isFalse(allMessageEls[i].expanded);
       }
@@ -88,25 +145,23 @@
 
     test('expand/collapse from external keypress', function() {
       element.handleExpandCollapse(true);
-      var allMessageEls =
-          Polymer.dom(element.root).querySelectorAll('gr-message');
+      var allMessageEls = getMessages();
       for (var i = 0; i < allMessageEls.length; i++) {
         assert.isTrue(allMessageEls[i].expanded);
       }
 
       // Expand/collapse all text also changes.
       assert.equal(element.$$('#collapse-messages').textContent.trim(),
-        'Collapse all');
+          'Collapse all');
 
       element.handleExpandCollapse(false);
-      var allMessageEls =
-          Polymer.dom(element.root).querySelectorAll('gr-message');
+      allMessageEls = getMessages();
       for (var i = 0; i < allMessageEls.length; i++) {
         assert.isFalse(allMessageEls[i].expanded);
       }
       // Expand/collapse all text also changes.
       assert.equal(element.$$('#collapse-messages').textContent.trim(),
-        'Expand all');
+          'Expand all');
     });
 
     test('hide messages does not appear when no automated messages',
@@ -115,8 +170,7 @@
     });
 
     test('scroll to message', function() {
-      var allMessageEls =
-          Polymer.dom(element.root).querySelectorAll('gr-message');
+      var allMessageEls = getMessages();
       for (var i = 0; i < allMessageEls.length; i++) {
         allMessageEls[i].expanded = false;
       }
@@ -192,7 +246,7 @@
             patch_set: 2,
             author: author,
           },
-        ]
+        ],
       };
       var messages = [].concat(
           randomMessage(),
@@ -220,8 +274,7 @@
       };
       var isMarvin = isAuthor.bind(null, author);
       flushAsynchronousOperations();
-      var messageElements =
-          Polymer.dom(element.root).querySelectorAll('gr-message');
+      var messageElements = getMessages();
       assert.equal(messageElements.length, messages.length);
       assert.deepEqual(messageElements[1].message, messages[1]);
       assert.deepEqual(messageElements[2].message, messages[2]);
@@ -258,7 +311,7 @@
       element.messages = messages;
       element.comments = comments;
       flushAsynchronousOperations();
-      var messageEls = Polymer.dom(element.root).querySelectorAll('gr-message');
+      var messageEls = getMessages();
       assert.equal(messageEls.length, 1);
       assert.equal(messageEls[0].message.message, messages[0].message);
     });
@@ -268,21 +321,11 @@
     var element;
     var messages;
 
-    var randomMessage = function(opt_params) {
-      var params = opt_params || {};
-      var author1 = {
-        _account_id: 1115495,
-        name: 'Andrew Bonventre',
-        email: 'andybons@chromium.org',
-      };
-      return {
-        id: params.id || Math.random().toString(),
-        date: params.date || '2016-01-12 20:28:33.038000',
-        message: params.message || Math.random().toString(),
-        _revision_number: params._revision_number || 1,
-        author: params.author || author1,
-        tag: 'autogenerated:gerrit:replace',
-      };
+    var getMessages = function() {
+      return Polymer.dom(element.root).querySelectorAll('gr-message');
+    };
+    var getHiddenMessages = function() {
+      return Polymer.dom(element.root).querySelectorAll('gr-message[hidden]');
     };
 
     var randomMessageReviewer = {
@@ -295,7 +338,7 @@
         getLoggedIn: function() { return Promise.resolve(false); },
       });
       element = fixture('basic');
-      messages = _.times(2, randomMessage);
+      messages = _.times(2, randomAutomated);
       messages.push(randomMessageReviewer);
       element.messages = messages;
       flushAsynchronousOperations();
@@ -306,42 +349,51 @@
     });
 
     test('autogenerated messages are not hidden initially', function() {
-      var allHiddenMessageEls =
-          Polymer.dom(element.root).querySelectorAll('gr-message[hidden]');
+      var allHiddenMessageEls = getHiddenMessages();
 
       //There are no hidden messages.
       assert.isFalse(!!allHiddenMessageEls.length);
     });
 
-    test('autogenerated messages are hidden after clicking hide button',
-        function() {
-      var allHiddenMessageEls =
-          Polymer.dom(element.root).querySelectorAll('gr-message[hidden]');
+    test('autogenerated messages hidden after hide button tap', function() {
+      var allHiddenMessageEls = getHiddenMessages();
 
       element._hideAutomated = false;
-      MockInteractions.tap(element.$$('#automatedMessageToggle'));
-      allMessageEls =
-          Polymer.dom(element.root).querySelectorAll('gr-message');
-      allHiddenMessageEls =
-          Polymer.dom(element.root).querySelectorAll('gr-message[hidden]');
+      MockInteractions.tap(element.$.automatedMessageToggle);
+      flushAsynchronousOperations();
+      allMessageEls = getMessages();
+      allHiddenMessageEls = getHiddenMessages();
 
       // Autogenerated messages are now hidden.
       assert.equal(allHiddenMessageEls.length, allMessageEls.length);
     });
 
-    test('autogenerated messages are not hidden after clicking show button',
-        function() {
-      var allHiddenMessageEls =
-          Polymer.dom(element.root).querySelectorAll('gr-message[hidden]');
+    test('autogenerated messages not hidden after show button tap', function() {
+      var allHiddenMessageEls = getHiddenMessages();
 
       element._hideAutomated = true;
-      MockInteractions.tap(element.$$('#automatedMessageToggle'));
-      allHiddenMessageEls =
-          Polymer.dom(element.root).querySelectorAll('gr-message[hidden]');
+      MockInteractions.tap(element.$.automatedMessageToggle);
+      allHiddenMessageEls = getHiddenMessages();
 
       //Autogenerated messages are now hidden.
       assert.isFalse(!!allHiddenMessageEls.length);
     });
-});
 
+    test('_getDelta', function() {
+      var messages = [randomMessage()];
+      assert.equal(element._getDelta(0, messages, false), 1);
+      assert.equal(element._getDelta(0, messages, true), 1);
+
+
+      messages = _.times(7, randomMessage);
+      assert.equal(element._getDelta(0, messages, false), 5);
+      assert.equal(element._getDelta(0, messages, true), 5);
+
+      messages = _.times(4, randomMessage)
+          .concat(_.times(2, randomAutomated))
+          .concat(_.times(3, randomMessage));
+      assert.equal(element._getDelta(2, messages, false), 5);
+      assert.equal(element._getDelta(2, messages, true), 7);
+    });
+  });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index d62dbf8..54a72f6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -480,20 +480,38 @@
     return index + 1;
   };
 
+  GrDiffBuilder.prototype._advancePastTagClose = function(html, index) {
+    while (index < html.length &&
+           html.charCodeAt(index) !== GrDiffBuilder.GREATER_THAN_CODE) {
+      index++;
+    }
+    return index + 1;
+  };
+
   GrDiffBuilder.prototype._addNewlines = function(text, html) {
     var htmlIndex = 0;
     var indices = [];
     var numChars = 0;
+    var prevHtmlIndex = 0;
     for (var i = 0; i < text.length; i++) {
       if (numChars > 0 && numChars % this._prefs.line_length === 0) {
         indices.push(htmlIndex);
       }
       htmlIndex = this._advanceChar(html, htmlIndex);
       if (text[i] === '\t') {
+        // Advance past tab closing tag.
+        htmlIndex = this._advancePastTagClose(html, htmlIndex);
+        // ~~ is a faster Math.floor
+        if (~~(numChars / this._prefs.line_length) !==
+            ~~((numChars + this._prefs.tab_size) / this._prefs.line_length)) {
+          // Tab crosses line limit - push it to the next line.
+          indices.push(prevHtmlIndex);
+        }
         numChars += this._prefs.tab_size;
       } else {
         numChars++;
       }
+      prevHtmlIndex = htmlIndex;
     }
     var result = html;
     // Since the result string is being altered in place, start from the end
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index e5fe1d7..88ce477 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -97,7 +97,7 @@
       assert.equal(buttons[2].textContent, '+10↓');
     });
 
-    test('newlines', function() {
+    test('newlines 1', function() {
       var text = 'abcdef';
       assert.equal(builder._addNewlines(text, text), text);
       text = 'a'.repeat(20);
@@ -105,8 +105,10 @@
           'a'.repeat(10) +
           GrDiffBuilder.LINE_FEED_HTML +
           'a'.repeat(10));
+    });
 
-      text = '<span class="thumbsup">👍</span>';
+    test('newlines 2', function() {
+      var text = '<span class="thumbsup">👍</span>';
       var html = '&lt;span class=&quot;thumbsup&quot;&gt;👍&lt;&#x2F;span&gt;';
       assert.equal(builder._addNewlines(text, html),
           '&lt;span clas' +
@@ -116,10 +118,13 @@
           'p&quot;&gt;👍&lt;&#x2F;spa' +
           GrDiffBuilder.LINE_FEED_HTML +
           'n&gt;');
+    });
 
-      text = '01234\t56789';
-      assert.equal(builder._addNewlines(text, text),
-          '01234\t5' +
+    test('newlines 3', function() {
+      var text = '01234\t56789';
+      var html = '01234<span>\t</span>56789';
+      assert.equal(builder._addNewlines(text, html),
+          '01234<span>\t</span>5' +
           GrDiffBuilder.LINE_FEED_HTML +
           '6789');
     });
@@ -154,6 +159,17 @@
       });
     });
 
+    test('_createTextEl linewrap with tabs', function() {
+      var text = _.times(7, _.constant('\t')).join('') + '!';
+      var line = {text: text, highlights: []};
+      var el = builder._createTextEl(line);
+      var tabEl = el.querySelector('.contentText > .br');
+      assert.isOk(tabEl);
+      assert.equal(
+          el.querySelector('.contentText .tab:nth-child(2)').nextSibling,
+          tabEl);
+    });
+
     test('text length with tabs and unicode', function() {
       assert.equal(builder._textLength('12345', 4), 5);
       assert.equal(builder._textLength('\t\t12', 4), 10);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index 4ca7f28..cb9a7b8 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -50,6 +50,7 @@
     setup(function() {
       element = fixture('basic');
       element.change = {};
+      element._hasKnownChainState = false;
       var plugin;
       Gerrit.install(function(p) { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
@@ -110,7 +111,7 @@
         var button = element.$$('[data-action-key="' + key + '"]');
         assert.isOk(button);
         assert.equal(button.getAttribute('data-label'), 'Bork!');
-        assert.isFalse(button.disabled);
+        assert.isNotOk(button.disabled);
         changeActions.setLabel(key, 'Yo');
         changeActions.setEnabled(key, false);
         flush(function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index b9b1f53..b54840e 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -693,7 +693,7 @@
       return fetch(url, options).then(function(response) {
         if (!response.ok) {
           if (opt_errFn) {
-            opt_errFn.call(null, response);
+            opt_errFn.call(opt_ctx || null, response);
             return undefined;
           }
           this.fire('server-error', {response: response});