Merge "Add download attribute to diff view download link"
diff --git a/Documentation/pgm-LocalUsernamesToLowerCase.txt b/Documentation/pgm-LocalUsernamesToLowerCase.txt
index e0fe1b3..03aaabf 100644
--- a/Documentation/pgm-LocalUsernamesToLowerCase.txt
+++ b/Documentation/pgm-LocalUsernamesToLowerCase.txt
@@ -7,7 +7,7 @@
 == SYNOPSIS
 [verse]
 --
-_java_ -jar gerrit.war _LocalUsernamesToLowerCase
+_java_ -jar gerrit.war _LocalUsernamesToLowerCase_
   -d <SITE_PATH>
 --
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index 2068540..444f64f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -199,7 +199,7 @@
         T def, A options, String fmt, Object... args) {
       final String prompt = String.format(fmt, args);
       for (; ; ) {
-        String r = console.readLine("%-30s [%s/?]: ", prompt, def.toString());
+        String r = console.readLine("%-30s [%s/?]: ", prompt, def.toString().toLowerCase());
         if (r == null) {
           throw abort();
         }
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 83c4d80..09e5d34 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -468,10 +468,6 @@
         return;
       }
 
-      // If the patch changed, and was not set to undefined/undefined, we need
-      // not reload all resources -- only the commit info and the file list.
-      // If the patch range was set to undefined/undefined, the user is looking
-      // to refresh the whole view.
       const patchChanged = this._patchRange &&
           (value.patchNum !== undefined && value.basePatchNum !== undefined) &&
           (this._patchRange.patchNum !== value.patchNum ||
@@ -481,24 +477,28 @@
         this._initialLoadComplete = false;
       }
 
-      const patchNum = value.patchNum ||
-          this.computeLatestPatchNum(this._allPatchSets);
-
-      const basePatchNum = value.basePatchNum || 'PARENT';
-
-      this._patchRange = {patchNum, basePatchNum};
+      const patchRange = {
+        patchNum: value.patchNum,
+        basePatchNum: value.basePatchNum || 'PARENT',
+      };
+      this.$.fileList.collapseAllDiffs();
 
       if (this._initialLoadComplete && patchChanged) {
+        if (patchRange.patchNum == null) {
+          patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
+        }
+        this._patchRange = patchRange;
         this._reloadPatchNumDependentResources().then(() => {
           this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
             change: this._change,
-            patchNum,
+            patchNum: patchRange.patchNum,
           });
         });
         return;
       }
 
       this._changeNum = value.changeNum;
+      this._patchRange = patchRange;
       this.$.relatedChanges.clear();
 
       this._reload().then(() => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 57b3e76..59f4f10 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -672,6 +672,7 @@
           '_reloadPatchNumDependentResources',
           () => { return Promise.resolve(); });
       const relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear');
+      const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
 
       const value = {
         view: Gerrit.Nav.View.CHANGE,
@@ -689,26 +690,22 @@
       assert.isFalse(reloadStub.calledTwice);
       assert.isTrue(reloadPatchDependentStub.calledOnce);
       assert.isTrue(relatedClearSpy.calledOnce);
+      assert.isTrue(collapseStub.calledTwice);
     });
 
     test('reload entire page when patchRange doesnt change', () => {
-      const mockPatchRange = {patchNum: '1337', basePatchNum: 'PARENT'};
       const reloadStub = sandbox.stub(element, '_reload',
           () => { return Promise.resolve(); });
-      element._patchRange = {};
-      sandbox.stub(element, 'computeLatestPatchNum').returns('1337');
+      const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
       const value = {
         view: Gerrit.Nav.View.CHANGE,
       };
       element._paramsChanged(value);
       assert.isTrue(reloadStub.calledOnce);
-      assert.deepEqual(element._patchRange, mockPatchRange);
-
       element._initialLoadComplete = true;
-      element._patchRange = {};
       element._paramsChanged(value);
       assert.isTrue(reloadStub.calledTwice);
-      assert.deepEqual(element._patchRange, mockPatchRange);
+      assert.isTrue(collapseStub.calledTwice);
     });
 
     test('include base patch when not parent', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index b218f02..50bf0e4 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -208,7 +208,7 @@
             if="[[_fileListActionsVisible(_shownFiles.*, _maxFilesForBulkActions)]]">
           <gr-button link on-tap="_expandAllDiffs">Show diffs</gr-button>
           <span class="separator">/</span>
-          <gr-button link on-tap="_collapseAllDiffs">Hide diffs</gr-button>
+          <gr-button link on-tap="collapseAllDiffs">Hide diffs</gr-button>
         </template>
         <template is="dom-if"
             if="[[!_fileListActionsVisible(_shownFiles.*, _maxFilesForBulkActions)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 7164a16..088adc2 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -162,7 +162,7 @@
 
       this._loading = true;
 
-      this._collapseAllDiffs();
+      this.collapseAllDiffs();
       const promises = [];
 
       promises.push(this._getFiles().then(files => {
@@ -304,7 +304,7 @@
       this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths));
     },
 
-    _collapseAllDiffs() {
+    collapseAllDiffs() {
       this._showInlineDiffs = false;
       this._expandedFilePaths = [];
       this.$.diffCursor.handleDiffUpdate();
@@ -640,7 +640,7 @@
 
     _toggleInlineDiffs() {
       if (this._showInlineDiffs) {
-        this._collapseAllDiffs();
+        this.collapseAllDiffs();
       } else {
         this._expandAllDiffs();
       }
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index ff62845..28bb781 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -859,6 +859,24 @@
       assert.notInclude(element._expandedFilePaths, path);
     });
 
+    test('collapseAllDiffs', () => {
+      sandbox.stub(element, '_renderInOrder')
+          .returns(Promise.resolve());
+      const cursorUpdateStub = sandbox.stub(element.$.diffCursor,
+          'handleDiffUpdate');
+
+      const path = 'path/to/my/file.txt';
+      element.files = [{__path: path}];
+      element._expandedFilePaths = [path];
+      element._showInlineDiffs = true;
+
+      element.collapseAllDiffs();
+      flushAsynchronousOperations();
+      assert.equal(element._expandedFilePaths.length, 0);
+      assert.isFalse(element._showInlineDiffs);
+      assert.isTrue(cursorUpdateStub.calledOnce);
+    });
+
     test('_expandedPathsChanged', done => {
       sandbox.stub(element, '_reviewFile');
       const path = 'path/to/my/file.txt';
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index c7a3815..fe4f7cf 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -219,6 +219,12 @@
       // makes assumptions that work for the GWT UI, but not PolyGerrit,
       // so we'll just disable it altogether for now.
       delete linkObj.target;
+
+      // Becasue the "my menu" links may be arbitrary URLs, we don't know
+      // whether they correspond to any client routes. Mark all such links as
+      // external.
+      linkObj.external = true;
+
       return linkObj;
     },
 
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
index e4cc7bd..3f45bbf 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -58,9 +58,9 @@
         {url: 'https://awesometown.com/#hashyhash'},
         {url: 'url', target: '_blank'},
       ].map(element._fixMyMenuItem), [
-        {url: '/q/owner:self+is:draft'},
-        {url: 'https://awesometown.com/#hashyhash'},
-        {url: 'url'},
+        {url: '/q/owner:self+is:draft', external: true},
+        {url: 'https://awesometown.com/#hashyhash', external: true},
+        {url: 'url', external: true},
       ]);
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
index 2d48d36..21aa6cb 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
@@ -55,7 +55,16 @@
   };
 
   GrEtagDecorator.prototype.getCachedPayload = function(url) {
-    return this._payloadCache.get(url);
+    let payload = this._payloadCache.get(url);
+
+    if (typeof payload === 'object') {
+      // Note: For the sake of cache transparency, deep clone the response
+      // object so that cache hits are not equal object references. Some code
+      // expects every network response to deserialize to a fresh object.
+      payload = JSON.parse(JSON.stringify(payload));
+    }
+
+    return payload;
   };
 
   GrEtagDecorator.prototype._truncateCache = function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
index 8be2352..40e639e 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
@@ -84,7 +84,7 @@
     });
 
     test('getCachedPayload', () => {
-      const payload = {};
+      const payload = 'payload';
       etag.collect('/foo', fakeRequest('bar'), payload);
       assert.strictEqual(etag.getCachedPayload('/foo'), payload);
       etag.collect('/foo', fakeRequest('bar', 304), 'garbage');
@@ -92,5 +92,25 @@
       etag.collect('/foo', fakeRequest('bar', 200), 'new payload');
       assert.strictEqual(etag.getCachedPayload('/foo'), 'new payload');
     });
+
+    test('getCachedPayload does not preserve object equality', () => {
+      const payload = {foo: 'bar'};
+      etag.collect('/foo', fakeRequest('bar'), payload);
+      assert.deepEqual(etag.getCachedPayload('/foo'), payload);
+      assert.notStrictEqual(etag.getCachedPayload('/foo'), payload);
+      etag.collect('/foo', fakeRequest('bar', 304), {foo: 'baz'});
+      assert.deepEqual(etag.getCachedPayload('/foo'), payload);
+      assert.notStrictEqual(etag.getCachedPayload('/foo'), payload);
+      etag.collect('/foo', fakeRequest('bar', 200), {foo: 'bar baz'});
+      assert.deepEqual(etag.getCachedPayload('/foo'), {foo: 'bar baz'});
+      assert.notStrictEqual(etag.getCachedPayload('/foo'), {foo: 'bar baz'});
+    });
+
+    test('getCachedPayload clones the response deeply', () => {
+      const payload = {foo: {bar: 'baz'}};
+      etag.collect('/foo', fakeRequest('bar'), payload);
+      assert.deepEqual(etag.getCachedPayload('/foo'), payload);
+      assert.notStrictEqual(etag.getCachedPayload('/foo').foo, payload.foo);
+    });
   });
 </script>