Add diff between patchset N vs N - 1 on uploaded messages

Add a button on the Change Log entry for new uploaded patchset N
messages which shows the diff between N - 1 and N.

Issue: Bug 13496
Change-Id: I2b3314f205c9ccd320c1132779b4e6c469dc9a30
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index ba92227..f9a3baa 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -39,12 +39,14 @@
   VotingRangeInfo,
   NumericChangeId,
   ChangeMessageId,
+  PatchSetNum,
 } from '../../../types/common';
 import {CommentThread} from '../../../utils/comment-util';
 import {hasOwnProperty} from '../../../utils/common-util';
 import {appContext} from '../../../services/app-context';
 import {pluralize} from '../../../utils/string-util';
 import {fireEvent} from '../../../utils/event-util';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 
 const PATCH_SET_PREFIX_PATTERN = /^(?:Uploaded\s*)?(?:P|p)atch (?:S|s)et \d+:\s*(.*)/;
 const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?[.]?$/;
@@ -271,13 +273,32 @@
     return this._patchsetCommentSummary(commentThreads);
   }
 
+  _isNewPatchsetTag(tag?: ReviewInputTag) {
+    return tag?.endsWith(':newPatchSet') || tag?.endsWith(':newWipPatchSet');
+  }
+
+  _handleViewPatchsetDiff(e: Event) {
+    if (!this.message || !this.change) return;
+    const match = this.message.message.match(/Uploaded patch set (\d+)./);
+    if (!match || match.length < 1) return;
+    const patchNum = Number(match[1]);
+    if (isNaN(patchNum)) throw new Error('invalid patchnum in message');
+    GerritNav.navigateToChange(
+      this.change,
+      patchNum as PatchSetNum,
+      (patchNum === 1 ? 'PARENT' : patchNum - 1) as PatchSetNum
+    );
+    // stop propagation to stop message expansion
+    e.stopPropagation();
+  }
+
   _computeMessageContent(
     content = '',
     tag: ReviewInputTag = '' as ReviewInputTag,
     isExpanded: boolean
   ) {
-    const isNewPatchSet =
-      tag.endsWith(':newPatchSet') || tag.endsWith(':newWipPatchSet');
+    const isNewPatchSet = this._isNewPatchsetTag(tag);
+
     const lines = content.split('\n');
     const filteredLines = lines.filter(line => {
       if (!isExpanded && line.startsWith('>')) {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
index 64fc384..815d8bb 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
@@ -111,13 +111,16 @@
       right: var(--spacing-l);
       top: var(--spacing-m);
     }
-    .dateContainer .patchset {
+    .dateContainer gr-button {
       margin-right: var(--spacing-m);
       color: var(--deemphasized-text-color);
     }
     .dateContainer .patchset:before {
       content: 'Patchset ';
     }
+    .dateContainer .patchsetDiffButton {
+      margin-right: var(--spacing-m);
+    }
     span.date {
       color: var(--deemphasized-text-color);
     }
@@ -276,6 +279,11 @@
         </div>
       </template>
       <span class="dateContainer">
+        <template is="dom-if" if="[[_isNewPatchsetTag(message.tag)]]">
+          <gr-button on-click="_handleViewPatchsetDiff" link>
+            View Diff
+          </gr-button>
+        </template>
         <template is="dom-if" if="[[message._revision_number]]">
           <span class="patchset">[[message._revision_number]]</span>
         </template>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
index 8825d15..8ed49e0 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-message.js';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 const basicFixture = fixtureFromElement('gr-message');
 
@@ -228,6 +229,45 @@
       assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id});
     });
 
+    suite('uploaded patchset X message navigates to X - 1 vs  X', () => {
+      let navStub;
+      setup(() => {
+        element.change = {changeNum: 12345};
+        navStub = sinon.stub(GerritNav, 'navigateToChange');
+      });
+
+      test('Patchset 1 navigates to Base', () => {
+        element.message = {
+          message: 'Uploaded patch set 1.',
+        };
+        element._handleViewPatchsetDiff(new MouseEvent('click'));
+        assert.isTrue(navStub.calledWithExactly({changeNum: 12345}, 1,
+            'PARENT'));
+      });
+
+      test('Patchset X navigates to X vs X - 1', () => {
+        element.message = {
+          message: 'Uploaded patch set 2.',
+        };
+        element._handleViewPatchsetDiff(new MouseEvent('click'));
+        assert.isTrue(navStub.calledWithExactly({changeNum: 12345}, 2, 1));
+
+        element.message = {
+          message: 'Uploaded patch set 200.',
+        };
+        element._handleViewPatchsetDiff(new MouseEvent('click'));
+        assert.isTrue(navStub.calledWithExactly({changeNum: 12345}, 200, 199));
+      });
+
+      test('invalid patchset does not cause navigation', () => {
+        element.message = {
+          message: 'Uploaded patch set XYZ.',
+        };
+        element._handleViewPatchsetDiff(new MouseEvent('click'));
+        assert.isFalse(navStub.called);
+      });
+    });
+
     suite('compute messages', () => {
       test('empty', () => {
         assert.equal(element._computeMessageContent('', '', true), '');