Navigate directly when change ID, number, or commit hash is searched

When a change ID, number, or commit hash is a complete match for the
search string, navigate directly to the corresponding change.

In some cases there will be multiple results for a truncated hash
search. In that case, still display the search screen.

Feature: Issue 5680
Change-Id: Icaa3dab3edab9f60b8dcda6ef71a75980e4b7127
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index fc9b26f..b2e8051 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -14,6 +14,11 @@
 (function() {
   'use strict';
 
+  var LookupQueryPatterns = {
+    CHANGE_ID: /^\s*i?[0-9a-f]{8,40}\s*$/i,
+    CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g,
+  };
+
   Polymer({
     is: 'gr-change-list-view',
 
@@ -55,7 +60,10 @@
       /**
        * Currently active query.
        */
-      _query: String,
+      _query: {
+        type: String,
+        value: '',
+      },
 
       /**
        * Offset of currently visible query results.
@@ -104,6 +112,15 @@
         this._changesPerPage = prefs.changes_per_page;
         return this._getChanges();
       }.bind(this)).then(function(changes) {
+        if (this._query && changes.length === 1) {
+          for (var query in LookupQueryPatterns) {
+            if (LookupQueryPatterns.hasOwnProperty(query) &&
+                this._query.match(LookupQueryPatterns[query])) {
+              page.show('/c/' + changes[0]._number);
+              return;
+            }
+          }
+        }
         this._changes = changes;
         this._loading = false;
       }.bind(this));
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
index d99f251..59edf4c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -22,6 +22,7 @@
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
 
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-change-list-view.html">
 
 <test-fixture id="basic">
@@ -31,6 +32,9 @@
 </test-fixture>
 
 <script>
+  var CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
+  var COMMIT_HASH = '12345678';
+
   suite('gr-change-list-view tests', function() {
     var element;
     var sandbox;
@@ -38,6 +42,9 @@
     setup(function() {
       stub('gr-rest-api-interface', {
         getLoggedIn: function() { return Promise.resolve(false); },
+        getChanges: function(num, query) {
+          return Promise.resolve([]);
+        },
       });
       element = fixture('basic');
       sandbox = sinon.sandbox.create();
@@ -122,5 +129,62 @@
       element._handlePreviousPage();
       assert.isTrue(showStub.called);
     });
+
+    suite('query based navigation', function() {
+      test('Searching for a change ID redirects to change', function(done) {
+        sandbox.stub(element, '_getChanges')
+            .returns(Promise.resolve([{_number: 1}]));
+        sandbox.stub(page, 'show', function(url) {
+          assert.equal(url, '/c/1');
+          done();
+        });
+
+        element.params = {view: 'gr-change-list-view', query: CHANGE_ID};
+      });
+
+      test('Searching for a change num redirects to change', function(done) {
+        sandbox.stub(element, '_getChanges')
+            .returns(Promise.resolve([{_number: 1}]));
+        sandbox.stub(page, 'show', function(url) {
+          assert.equal(url, '/c/1');
+          done();
+        });
+
+        element.params = {view: 'gr-change-list-view', query: '1'};
+      });
+
+      test('Commit hash redirects to change', function(done) {
+        sandbox.stub(element, '_getChanges')
+            .returns(Promise.resolve([{_number: 1}]));
+        sandbox.stub(page, 'show', function(url) {
+          assert.equal(url, '/c/1');
+          done();
+        });
+
+        element.params = {view: 'gr-change-list-view', query: COMMIT_HASH};
+      });
+
+      test('Searching for an invalid change ID searches', function() {
+        sandbox.stub(element, '_getChanges')
+            .returns(Promise.resolve([]));
+        var stub = sandbox.stub(page, 'show');
+
+        element.params = {view: 'gr-change-list-view', query: CHANGE_ID};
+        flushAsynchronousOperations();
+
+        assert.isFalse(stub.called);
+      });
+
+      test('Change ID with multiple search results searches', function() {
+        sandbox.stub(element, '_getChanges')
+            .returns(Promise.resolve([{}, {}]));
+        var stub = sandbox.stub(page, 'show');
+
+        element.params = {view: 'gr-change-list-view', query: CHANGE_ID};
+        flushAsynchronousOperations();
+
+        assert.isFalse(stub.called);
+      });
+    });
   });
 </script>