Allow a Link trailer to be used as an alternative change ID trailer

In some projects it may be desirable for the trailer to contain a link to
the Gerrit review page so that it is convenient to access the review page
starting from the commit message. The Link trailer is a standard trailer
used for inserting links in the commit message, which has been adopted by
the Linux kernel among other projects. For example, the Linux kernel has a
policy about linking to the mailing list archive with Link trailers:
https://www.kernel.org/doc/html/latest/maintainer/configure-git.html#creating-commit-links-to-lore-kernel-org

This change makes Gerrit interoperate well with the Link trailer. Specifically,
it teaches the Gerrit server to recognize trailers of the form:

    Link: https://gerrit-review.googlesource.com/id/I78e884a944cedb5144f661a057e4829c8f84e933

as well as the existing Change-Id trailer, teaches the server to recognize
/id/ as a search prefix for change IDs and modifies the commit-msg hook
to optionally add a Link trailer (using a server URL provided in the
property gerrit.reviewUrl) instead of the Change-Id trailer.

Change-Id: I78e884a944cedb5144f661a057e4829c8f84e933
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 1094809..38f7c06 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
@@ -78,7 +78,8 @@
   MISMATCH: 'mismatch',
   MISSING: 'missing',
 };
-const CHANGE_ID_REGEX_PATTERN = /^Change-Id\:\s(I[0-9a-f]{8,40})/gm;
+const CHANGE_ID_REGEX_PATTERN =
+  /^(Change-Id\:\s|Link:.*\/id\/)(I[0-9a-f]{8,40})/gm;
 
 const MIN_LINES_FOR_COMMIT_COLLAPSE = 30;
 const DEFAULT_NUM_FILES_SHOWN = 200;
@@ -1316,7 +1317,7 @@
     let changeIdArr;
 
     while (changeIdArr = CHANGE_ID_REGEX_PATTERN.exec(commitMessage)) {
-      changeId = changeIdArr[1];
+      changeId = changeIdArr[2];
     }
 
     if (changeId) {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 3ffd819..5261426 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -160,6 +160,8 @@
    */
   QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/,
 
+  CHANGE_ID_QUERY: /^\/id\/(I[0-9a-f]{40})$/,
+
   // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
   CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
   CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
@@ -1018,6 +1020,8 @@
 
     this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute');
 
+    this._mapRoute(RoutePattern.CHANGE_ID_QUERY, '_handleChangeIdQueryRoute');
+
     this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum');
 
     this._mapRoute(
@@ -1510,6 +1514,16 @@
     });
   }
 
+  _handleChangeIdQueryRoute(data: PageContextWithQueryMap) {
+    // TODO(pcc): This will need to indicate that this was a change ID query if
+    // standard queries gain the ability to search places like commit messages
+    // for change IDs.
+    this._setParams({
+      view: GerritNav.View.SEARCH,
+      query: data.params[0],
+    });
+  }
+
   _handleQueryLegacySuffixRoute(ctx: PageContextWithQueryMap) {
     this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
   }
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
index a931aac..5ee58c9 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
@@ -182,6 +182,7 @@
       '_handleBranchListFilterOffsetRoute',
       '_handleBranchListFilterRoute',
       '_handleBranchListOffsetRoute',
+      '_handleChangeIdQueryRoute',
       '_handleChangeNumberLegacyRoute',
       '_handleChangeRoute',
       '_handleCommentRoute',
@@ -739,6 +740,14 @@
       });
     });
 
+    test('_handleChangeIdQueryRoute', () => {
+      const data = {params: ['I0123456789abcdef0123456789abcdef01234567']};
+      assertDataToParams(data, '_handleChangeIdQueryRoute', {
+        view: GerritNav.View.SEARCH,
+        query: 'I0123456789abcdef0123456789abcdef01234567',
+      });
+    });
+
     suite('_handleRegisterRoute', () => {
       test('happy path', () => {
         const ctx = {params: ['/foo/bar']};