Show revert status chip if revert was created

If a revert of the change was created, determined by scanning the
change messages, then show a "Revert Created" chip next to the
Merged status.
Clicking the chip redirects to the revert created.
The sha of the revert commit is shown in the Change summary section
hidden by default(similar to the Merged As information).

This change does not determine if the revert was submitted or not.
This will be done in a follow up change.

Screenshot: https://imgur.com/a/FZ2adWQ
Change-Id: Ia562e7e09119443f34360d87bf95bb028ccad186
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index be502f7..363f6e5 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -54,6 +54,7 @@
   TAG_SET_ASSIGNEE = 'autogenerated:gerrit:setAssignee',
   TAG_UNSET_ASSIGNEE = 'autogenerated:gerrit:deleteAssignee',
   TAG_MERGED = 'autogenerated:gerrit:merged',
+  TAG_REVERT = 'autogenerated:gerrit:revert',
 }
 
 /**
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index 8b25e5d..106a67c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -85,6 +85,10 @@
   AutocompleteQuery,
   AutocompleteSuggestion,
 } from '../../shared/gr-autocomplete/gr-autocomplete';
+import {
+  getRevertCommitHash,
+  isRevertCreated,
+} from '../../../utils/message-util';
 
 const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
 
@@ -587,6 +591,16 @@
     return rev.commit;
   }
 
+  _showRevertCreatedAs(change?: ParsedChangeInfo) {
+    if (!change) return false;
+    return isRevertCreated(change.messages);
+  }
+
+  _computeRevertCommit(change?: ParsedChangeInfo) {
+    if (!change) return undefined;
+    return {commit: getRevertCommitHash(change.messages)};
+  }
+
   _computeShowAllLabelText(showAllSections: boolean) {
     if (showAllSections) {
       return 'Show less';
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index b68b21b..cf829c3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -353,6 +353,20 @@
         </span>
       </section>
     </template>
+    <template is="dom-if" if="[[_showRevertCreatedAs(change)]]">
+      <section
+        class$="[[_computeDisplayState(_showAllSections, change, _SECTION.REVERT_CREATED_AS)]]"
+      >
+        <span class="title">Revert Created As</span>
+        <span class="value">
+          <gr-commit-info
+            change="[[change]]"
+            commit-info="[[_computeRevertCommit(change)]]"
+            server-config="[[serverConfig]]"
+          ></gr-commit-info>
+        </span>
+      </section>
+    </template>
     <section
       class$="topic [[_computeDisplayState(_showAllSections, change, _SECTION.TOPIC)]]"
     >
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 8f8b2b8..1f076ba 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
@@ -1572,17 +1572,6 @@
     return GerritNav.getUrlForChange(change);
   }
 
-  _computeShowCommitInfo(
-    changeStatuses: string[],
-    current_revision: RevisionInfo
-  ) {
-    return (
-      changeStatuses.length === 1 &&
-      changeStatuses[0] === 'Merged' &&
-      current_revision
-    );
-  }
-
   _computeChangeIdClass(displayChangeId: string) {
     return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : '';
   }
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 ccca679..bb99529 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
@@ -368,6 +368,7 @@
           <div class="changeStatuses">
             <template is="dom-repeat" items="[[_changeStatuses]]" as="status">
               <gr-change-status
+                change="[[_change]]"
                 max-width="100"
                 status="[[status]]"
               ></gr-change-status>
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
index 3ba12b43..cc2f650 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
@@ -81,6 +81,18 @@
     });
   }
 
+  computeCommitLink(
+    webLink?: string,
+    change?: ChangeInfo,
+    commitInfo?: CommitInfo,
+    serverConfig?: ServerInfo
+  ) {
+    if (webLink) return webLink;
+    const hash = this._computeShortHash(change, commitInfo, serverConfig);
+    if (hash === undefined) return '';
+    return GerritNav.getUrlForSearchQuery(hash);
+  }
+
   _computeShortHash(
     change?: ChangeInfo,
     commitInfo?: CommitInfo,
@@ -90,7 +102,7 @@
       return '';
     }
 
-    const {name} = this._getWeblink(change, commitInfo, serverConfig) || {};
-    return name;
+    const weblink = this._getWeblink(change, commitInfo, serverConfig);
+    return weblink?.name ?? '';
   }
 }
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.ts
index df0bb4a..65ca8b5 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.ts
@@ -24,14 +24,12 @@
     }
   </style>
   <div class="container">
-    <template is="dom-if" if="[[_showWebLink]]">
-      <a target="_blank" rel="noopener" href$="[[_webLink]]"
-        >[[_computeShortHash(change, commitInfo, serverConfig)]]</a
-      >
-    </template>
-    <template is="dom-if" if="[[!_showWebLink]]">
-      [[_computeShortHash(change, commitInfo, serverConfig)]]
-    </template>
+    <a
+      target="_blank"
+      rel="noopener"
+      href$="[[computeCommitLink(_webLink, change, commitInfo, serverConfig)]]"
+      >[[_computeShortHash(change, commitInfo, serverConfig)]]</a
+    >
     <gr-copy-clipboard
       has-tooltip=""
       button-title="Copy full SHA to clipboard"
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
index 1fd019f..f18f121 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
@@ -19,6 +19,10 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-change-status_html';
 import {customElement, property} from '@polymer/decorators';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {getRevertCommitHash} from '../../../utils/message-util';
+import {ChangeInfo} from '../../../types/common';
+import {ParsedChangeInfo} from '../../../types/types';
 
 enum ChangeStates {
   MERGED = 'Merged',
@@ -26,6 +30,7 @@
   MERGE_CONFLICT = 'Merge Conflict',
   WIP = 'WIP',
   PRIVATE = 'Private',
+  REVERT_CREATED = 'Revert Created',
 }
 
 const WIP_TOOLTIP =
@@ -51,6 +56,9 @@
   @property({type: Boolean, reflectToAttribute: true})
   flat = false;
 
+  @property({type: Object})
+  change?: ChangeInfo | ParsedChangeInfo;
+
   @property({type: String, observer: '_updateChipDetails'})
   status?: ChangeStates;
 
@@ -68,6 +76,17 @@
     return str ? str.toLowerCase().replace(/\s/g, '-') : '';
   }
 
+  hasStatusLink(status: ChangeStates) {
+    return status === ChangeStates.REVERT_CREATED;
+  }
+
+  getStatusLink(change?: ParsedChangeInfo) {
+    if (!change) return;
+    const revertCommit = getRevertCommitHash(change.messages);
+    if (!revertCommit) return;
+    return GerritNav.getUrlForSearchQuery(revertCommit);
+  }
+
   _updateChipDetails(status?: ChangeStates, previousStatus?: ChangeStates) {
     if (previousStatus) {
       this.classList.remove(this._toClassName(previousStatus));
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
index 542d8be..379c3f3 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
@@ -52,6 +52,13 @@
       background-color: var(--status-ready);
       color: var(--status-ready);
     }
+    :host(.revert-created) .chip {
+      background-color: var(--status-revert-created);
+      color: var(--status-revert-created);
+    }
+    .status-link {
+      text-decoration: none;
+    }
     :host(.custom) .chip {
       background-color: var(--status-custom);
       color: var(--status-custom);
@@ -70,8 +77,17 @@
     title="[[tooltipText]]"
     max-width="40em"
   >
-    <div class="chip" aria-label$="Label: [[status]]">
-      [[_computeStatusString(status)]]
-    </div>
+    <template is="dom-if" if="[[hasStatusLink(status)]]">
+      <a class="status-link" href="[[getStatusLink(change)]]">
+        <div class="chip" aria-label$="Label: [[status]]">
+          [[_computeStatusString(status)]]
+        </div>
+      </a>
+    </template>
+    <template is="dom-if" if="[[!hasStatusLink(status)]]">
+      <div class="chip" aria-label$="Label: [[status]]">
+        [[_computeStatusString(status)]]
+      </div>
+    </template>
   </gr-tooltip-content>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.js
index 16fc664..4ef97c9 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.js
@@ -37,6 +37,7 @@
 
   test('WIP', () => {
     element.status = 'WIP';
+    flush();
     assert.equal(element.shadowRoot
         .querySelector('.chip').innerText, 'Work in Progress');
     assert.equal(element.tooltipText, WIP_TOOLTIP);
@@ -46,6 +47,7 @@
   test('WIP flat', () => {
     element.flat = true;
     element.status = 'WIP';
+    flush();
     assert.equal(element.shadowRoot
         .querySelector('.chip').innerText, 'WIP');
     assert.isDefined(element.tooltipText);
@@ -55,6 +57,7 @@
 
   test('merged', () => {
     element.status = 'Merged';
+    flush();
     assert.equal(element.shadowRoot
         .querySelector('.chip').innerText, element.status);
     assert.equal(element.tooltipText, '');
@@ -63,6 +66,7 @@
 
   test('abandoned', () => {
     element.status = 'Abandoned';
+    flush();
     assert.equal(element.shadowRoot
         .querySelector('.chip').innerText, element.status);
     assert.equal(element.tooltipText, '');
@@ -71,6 +75,7 @@
 
   test('merge conflict', () => {
     element.status = 'Merge Conflict';
+    flush();
     assert.equal(element.shadowRoot
         .querySelector('.chip').innerText, element.status);
     assert.equal(element.tooltipText, MERGE_CONFLICT_TOOLTIP);
@@ -79,6 +84,7 @@
 
   test('private', () => {
     element.status = 'Private';
+    flush();
     assert.equal(element.shadowRoot
         .querySelector('.chip').innerText, element.status);
     assert.equal(element.tooltipText, PRIVATE_TOOLTIP);
@@ -87,6 +93,7 @@
 
   test('active', () => {
     element.status = 'Active';
+    flush();
     assert.equal(element.shadowRoot
         .querySelector('.chip').innerText, element.status);
     assert.equal(element.tooltipText, '');
@@ -95,6 +102,7 @@
 
   test('ready to submit', () => {
     element.status = 'Ready to submit';
+    flush();
     assert.equal(element.shadowRoot
         .querySelector('.chip').innerText, element.status);
     assert.equal(element.tooltipText, '');
@@ -103,10 +111,12 @@
 
   test('updating status removes the previous class', () => {
     element.status = 'Private';
+    flush();
     assert.isTrue(element.classList.contains('private'));
     assert.isFalse(element.classList.contains('wip'));
 
     element.status = 'WIP';
+    flush();
     assert.isFalse(element.classList.contains('private'));
     assert.isTrue(element.classList.contains('wip'));
   });
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 838d82b..a81dcdf 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -253,6 +253,7 @@
     --status-wip: #795548;
     --status-private: var(--purple-500);
     --status-conflict: var(--red-600);
+    --status-revert-created: #e64a19;
     --status-active: var(--blue-700);
     --status-ready: var(--pink-800);
     --status-custom: var(--purple-900);
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index 4b824b2..1d20c86 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -151,6 +151,7 @@
       --status-wip: #bcaaa4;
       --status-private: var(--purple-200);
       --status-conflict: var(--red-300);
+      --status-revert-created: #ff8a65;
       --status-active: var(--blue-400);
       --status-ready: var(--pink-500);
       --status-custom: var(--purple-400);
diff --git a/polygerrit-ui/app/utils/change-metadata-util.ts b/polygerrit-ui/app/utils/change-metadata-util.ts
index eda988a..9692ab31 100644
--- a/polygerrit-ui/app/utils/change-metadata-util.ts
+++ b/polygerrit-ui/app/utils/change-metadata-util.ts
@@ -24,6 +24,7 @@
   SUBMITTED = 'Submitted',
   PARENT = 'Parent',
   MERGED_AS = 'Merged as',
+  REVERT_CREATED_AS = 'Revert Created as',
   STRATEGY = 'Strategy',
   UPDATED = 'Updated',
   CC = 'CC',
@@ -56,6 +57,7 @@
   ALWAYS_HIDE: [
     Metadata.PARENT,
     Metadata.MERGED_AS,
+    Metadata.REVERT_CREATED_AS,
     Metadata.STRATEGY,
     Metadata.UPDATED,
   ],
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index d7fb5d0..b2d23a0 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -24,6 +24,7 @@
   RelatedChangeAndCommitInfo,
 } from '../types/common';
 import {ParsedChangeInfo} from '../types/types';
+import {isRevertCreated} from './message-util';
 
 // This can be wrong! See WARNING above
 interface ChangeStatusesOptions {
@@ -134,6 +135,7 @@
   return change?.status === ChangeStatus.NEW;
 }
 
+// TODO(TS): use enum ChangeStates in gr-change-status
 export function changeStatuses(
   change: ChangeInfo,
   opt_options?: ChangeStatusesOptions
@@ -156,6 +158,9 @@
   if (change.is_private) {
     states.push('Private');
   }
+  if (isRevertCreated(change.messages)) {
+    states.push('Revert Created');
+  }
 
   // If there are any pre-defined statuses, only return those. Otherwise,
   // will determine the derived status.
diff --git a/polygerrit-ui/app/utils/message-util.ts b/polygerrit-ui/app/utils/message-util.ts
new file mode 100644
index 0000000..f2d7e6b
--- /dev/null
+++ b/polygerrit-ui/app/utils/message-util.ts
@@ -0,0 +1,32 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {MessageTag} from '../constants/constants';
+import {ChangeMessageInfo} from '../types/common';
+
+export function getRevertCommitHash(messages?: ChangeMessageInfo[]) {
+  const msg = messages?.find(m => m.tag === MessageTag.TAG_REVERT);
+  if (!msg) return undefined;
+  const REVERT_REGEX = /^Created a revert of this change as (.*)$/;
+  const commit = msg.message.match(REVERT_REGEX)?.[1];
+  if (!commit) throw new Error('revert commit not found');
+  return commit;
+}
+
+export function isRevertCreated(messages?: ChangeMessageInfo[]) {
+  return messages?.some(m => m.tag === MessageTag.TAG_REVERT);
+}