Merge "Fix copy & paste issue on diff list page"
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index 7402a37..766a835 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -34,7 +34,7 @@
 
 java_library(
     name = "ref_cache",
-    srcs = glob(["ReceivePackRefCache.java"]),
+    srcs = ["ReceivePackRefCache.java"],
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/entities",
diff --git a/java/com/google/gerrit/server/group/GroupResolver.java b/java/com/google/gerrit/server/group/GroupResolver.java
index a54f465..1aa265b 100644
--- a/java/com/google/gerrit/server/group/GroupResolver.java
+++ b/java/com/google/gerrit/server/group/GroupResolver.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
@@ -28,6 +29,8 @@
 
 @Singleton
 public class GroupResolver {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final GroupBackend groupBackend;
   private final GroupCache groupCache;
   private final GroupControl.Factory groupControlFactory;
@@ -81,36 +84,48 @@
    * @return the group, null if no group is found for the given group ID
    */
   public GroupDescription.Basic parseId(String id) {
+    logger.atFine().log("Parsing group %s", id);
+
     AccountGroup.UUID uuid = AccountGroup.uuid(id);
     if (groupBackend.handles(uuid)) {
+      logger.atFine().log("Group UUID %s is handled by a group backend", uuid.get());
       GroupDescription.Basic d = groupBackend.get(uuid);
       if (d != null) {
+        logger.atFine().log("Found group %s", d.getName());
         return d;
       }
     }
 
     // Might be a numeric AccountGroup.Id. -> Internal group.
     if (id.matches("^[1-9][0-9]*$")) {
+      logger.atFine().log("Group ID %s is a numeric ID", id);
       try {
         AccountGroup.Id groupId = AccountGroup.Id.parse(id);
         Optional<InternalGroup> group = groupCache.get(groupId);
         if (group.isPresent()) {
+          logger.atFine().log(
+              "Found internal group %s (UUID = %s)",
+              group.get().getName(), group.get().getGroupUUID().get());
           return new InternalGroupDescription(group.get());
         }
       } catch (IllegalArgumentException e) {
         // Ignored
+        logger.atFine().withCause(e).log("Parsing numeric group ID %s failed", id);
       }
     }
 
     // Might be a group name, be nice and accept unique names.
+    logger.atFine().log("Try finding a group with name %s", id);
     GroupReference ref = GroupBackends.findExactSuggestion(groupBackend, id);
     if (ref != null) {
       GroupDescription.Basic d = groupBackend.get(ref.getUUID());
       if (d != null) {
+        logger.atFine().log("Found group %s", d.getName());
         return d;
       }
     }
 
+    logger.atFine().log("Group %s not found", id);
     return null;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetPatch.java b/java/com/google/gerrit/server/restapi/change/GetPatch.java
index 31785be..66ccef3 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPatch.java
@@ -136,7 +136,9 @@
       } finally {
         if (close) {
           rw.close();
-          bin.close();
+          if (bin != null) {
+            bin.close();
+          }
         }
       }
     } finally {
diff --git a/plugins/gitiles b/plugins/gitiles
index 22b8e24..825ca06 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 22b8e242b5eaa9eae817b776bc862b096479ceaa
+Subproject commit 825ca06dddc9de89daa6b126dfc187fbeb25280c
diff --git a/plugins/webhooks b/plugins/webhooks
index 377fec1..570daca 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit 377fec17d2c0fc71cdfb1d12f502cfa1eba1fbd7
+Subproject commit 570dacacc64f7c01e0bc0f1301aa1d5a218cfc1b
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 9ef2461..5a7e58d 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -178,6 +178,8 @@
     _showAlert(text, opt_actionText, opt_actionCallback,
         opt_dismissOnNavigation) {
       if (this._alertElement) {
+        // do not override auth alerts
+        if (this._alertElement.type === 'AUTH') return;
         this._hideAlert();
       }
 
@@ -212,10 +214,14 @@
     }
 
     _showAuthErrorAlert(errorText, actionText) {
-      // TODO(viktard): close alert if it's not for auth error.
-      if (this._alertElement) { return; }
+      // hide any existing alert like `reload`
+      // as auth error should have the highest priority
+      if (this._alertElement) {
+        this._alertElement.hide();
+      }
 
       this._alertElement = this._createToastAlert();
+      this._alertElement.type = 'AUTH';
       this._alertElement.show(errorText, actionText,
           this._createLoginPopup.bind(this));
 
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index a0a5731..72f5d5b 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -52,6 +52,7 @@
         sandbox.stub(window, 'fetch')
             .returns(Promise.resolve({ok: true, status: 204}));
         element = fixture('basic');
+        element._authService.clearCache();
       });
 
       test('does not show auth error on 403 by default', done => {
@@ -184,7 +185,8 @@
         );
         assert.equal(window.fetch.callCount, 1);
         flush(() => {
-          // auth check again
+          // here needs two flush as there are two chanined
+          // promises on server-error handler and flush only flushes one
           assert.equal(window.fetch.callCount, 2);
           flush(() => {
             // auth-error fired
@@ -238,6 +240,97 @@
         });
       });
 
+      test('auth toast should dismiss existing toast', done => {
+        // starts with authed state
+        element.$.restAPI.getLoggedIn();
+        const toastSpy = sandbox.spy(element, '_createToastAlert');
+        const responseText = Promise.resolve('Authentication required\n');
+
+        // fake an alert
+        element.fire('show-alert', {message: 'test reload', action: 'reload'});
+        const toast = toastSpy.lastCall.returnValue;
+        assert.isOk(toast);
+        assert.include(
+            Polymer.dom(toast.root).textContent, 'test reload');
+
+        // fake auth
+        window.fetch.returns(Promise.resolve({status: 403}));
+        element.fire('server-error',
+            {response: {status: 403, text() { return responseText; }}}
+        );
+        assert.equal(window.fetch.callCount, 1);
+        flush(() => {
+          // here needs two flush as there are two chanined
+          // promises on server-error handler and flush only flushes one
+          assert.equal(window.fetch.callCount, 2);
+          flush(() => {
+            // toast
+            const toast = toastSpy.lastCall.returnValue;
+            assert.include(
+                Polymer.dom(toast.root).textContent, 'Credentails expired.');
+            assert.include(
+                Polymer.dom(toast.root).textContent, 'Refresh credentials');
+            done();
+          });
+        });
+      });
+
+      test('regular toast should dismiss regular toast', () => {
+        // starts with authed state
+        element.$.restAPI.getLoggedIn();
+        const toastSpy = sandbox.spy(element, '_createToastAlert');
+
+        // fake an alert
+        element.fire('show-alert', {message: 'test reload', action: 'reload'});
+        let toast = toastSpy.lastCall.returnValue;
+        assert.isOk(toast);
+        assert.include(
+            Polymer.dom(toast.root).textContent, 'test reload');
+
+        // new alert
+        element.fire('show-alert', {message: 'second-test', action: 'reload'});
+
+        toast = toastSpy.lastCall.returnValue;
+        assert.include(Polymer.dom(toast.root).textContent, 'second-test');
+      });
+
+      test('regular toast should not dismiss auth toast', done => {
+        // starts with authed state
+        element.$.restAPI.getLoggedIn();
+        const toastSpy = sandbox.spy(element, '_createToastAlert');
+        const responseText = Promise.resolve('Authentication required\n');
+
+        // fake auth
+        window.fetch.returns(Promise.resolve({status: 403}));
+        element.fire('server-error',
+            {response: {status: 403, text() { return responseText; }}}
+        );
+        assert.equal(window.fetch.callCount, 1);
+        flush(() => {
+          // here needs two flush as there are two chanined
+          // promises on server-error handler and flush only flushes one
+          assert.equal(window.fetch.callCount, 2);
+          flush(() => {
+            let toast = toastSpy.lastCall.returnValue;
+            assert.include(
+                Polymer.dom(toast.root).textContent, 'Credentails expired.');
+            assert.include(
+                Polymer.dom(toast.root).textContent, 'Refresh credentials');
+
+            // fake an alert
+            element.fire('show-alert', {
+              message: 'test-alert', action: 'reload',
+            });
+            toast = toastSpy.lastCall.returnValue;
+            assert.isOk(toast);
+            assert.include(
+                Polymer.dom(toast.root).textContent, 'Credentails expired.');
+
+            done();
+          });
+        });
+      });
+
       test('show alert', () => {
         const alertObj = {message: 'foo'};
         sandbox.stub(element, '_showAlert');
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
index 215f469..6a0769d 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -32,6 +32,8 @@
       return {
         text: String,
         actionText: String,
+        /** @type {?string} */
+        type: String,
         shown: {
           type: Boolean,
           value: true,