Merge branch 'stable-2.15' into stable-2.16

* stable-2.15:
  Allow to configure the number of hook execution workers

Change-Id: I1b7d98d9347a5d59c7316a1dc301211ccd967add
diff --git a/BUILD b/BUILD
index 4f332ac..cfdc295 100644
--- a/BUILD
+++ b/BUILD
@@ -1,4 +1,5 @@
-load("//tools/bzl:plugin.bzl", "gerrit_plugin")
+load("//tools/bzl:junit.bzl", "junit_tests")
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS", "PLUGIN_TEST_DEPS", "gerrit_plugin")
 
 gerrit_plugin(
     name = "hooks",
@@ -9,3 +10,13 @@
     ],
     resources = glob(["src/main/resources/**/*"]),
 )
+
+junit_tests(
+    name = "hooks_tests",
+    srcs = glob(["src/test/java/**/*.java"]),
+    tags = ["hooks"],
+    visibility = ["//visibility:public"],
+    deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [
+        ":hooks__plugin",
+    ],
+)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/CommitReceived.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/CommitReceived.java
index b03a77b..74a0817 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/CommitReceived.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/CommitReceived.java
@@ -51,7 +51,7 @@
     args.add("--project", projectName);
     args.add("--refname", refname);
     args.add("--uploader", receiveEvent.user.getNameEmail());
-    args.add("--uploader-username", receiveEvent.user.getUserName());
+    args.add("--uploader-username", receiveEvent.user.getUserName().orElse(null));
     args.add("--oldrev", old.name());
     args.add("--newrev", receiveEvent.commit.name());
     args.add("--cmdref", commandRef);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/HookArgs.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/HookArgs.java
index 91d3696..87d4291 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/HookArgs.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/HookArgs.java
@@ -18,20 +18,22 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.UrlFormatter;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 class HookArgs {
   interface Factory {
@@ -39,22 +41,22 @@
   }
 
   final IdentifiedUser.GenericFactory identifiedUserFactory;
-  final Provider<String> urlProvider;
   final HookMetrics metrics;
   final GitRepositoryManager gitManager;
   final SitePaths sitePaths;
+  final DynamicItem<UrlFormatter> urlFormatter;
 
   private final List<String> args;
 
   @Inject
   HookArgs(
       IdentifiedUser.GenericFactory identifiedUserFactory,
-      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      DynamicItem<UrlFormatter> urlFormatter,
       HookMetrics metrics,
       GitRepositoryManager gitManager,
       SitePaths sitePaths) {
     this.identifiedUserFactory = identifiedUserFactory;
-    this.urlProvider = urlProvider;
+    this.urlFormatter = urlFormatter;
     this.metrics = metrics;
     this.gitManager = gitManager;
     this.sitePaths = sitePaths;
@@ -97,9 +99,14 @@
 
   public void addUrl(ChangeInfo change) {
     args.add("--change-url");
-    String url = urlProvider.get();
-    if (change != null && url != null) {
-      args.add(url + change._number);
+    if (change != null) {
+      Optional<UrlFormatter> uf = Optional.ofNullable(urlFormatter.get());
+      args.add(
+          uf.flatMap(
+                  f ->
+                      f.getChangeViewUrl(
+                          new Project.NameKey(change.project), new Change.Id(change._number)))
+              .orElse(""));
     } else {
       args.add("");
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/HookExecutor.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/HookExecutor.java
index 42d4e75..0232f9b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/HookExecutor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/HookExecutor.java
@@ -1,8 +1,10 @@
 package com.googlesource.gerrit.plugins.hooks;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
 import com.google.inject.Inject;
 import java.lang.Thread.UncaughtExceptionHandler;
 import java.nio.file.Files;
@@ -13,18 +15,13 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class HookExecutor implements LifecycleListener {
-  private static final Logger log = LoggerFactory.getLogger(HookExecutor.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final UncaughtExceptionHandler LOG_UNCAUGHT_EXCEPTION =
-      new UncaughtExceptionHandler() {
-        @Override
-        public void uncaughtException(Thread t, Throwable e) {
-          log.error("HookExecutor thread {} threw exception", t.getName(), e);
-        }
-      };
+      (t, e) ->
+          logger.atSevere().withCause(e).log("HookExecutor thread %s threw exception", t.getName());
 
   private final ExecutorService threadPool;
   private final int timeout;
@@ -33,11 +30,12 @@
   HookExecutor(@GerritServerConfig Config config) {
     this.timeout = config.getInt("hooks", "syncHookTimeout", 30);
     this.threadPool =
-        Executors.newCachedThreadPool(
-            new ThreadFactoryBuilder()
-                .setNameFormat("SyncHook-%d")
-                .setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION)
-                .build());
+        new LoggingContextAwareExecutorService(
+            Executors.newCachedThreadPool(
+                new ThreadFactoryBuilder()
+                    .setNameFormat("SyncHook-%d")
+                    .setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION)
+                    .build()));
   }
 
   HookResult submit(Path hook, HookArgs args) {
@@ -46,7 +44,7 @@
 
   HookResult submit(String projectName, Path hook, HookArgs args) {
     if (!Files.exists(hook)) {
-      log.debug("Hook file not found: {}", hook);
+      logger.atFine().log("Hook file not found: %s", hook);
       return null;
     }
     HookTask.Sync hookTask = new HookTask.Sync(projectName, hook, args);
@@ -58,10 +56,10 @@
       return task.get(timeout, TimeUnit.SECONDS);
     } catch (TimeoutException e) {
       message = "Synchronous hook timed out " + hook;
-      log.error(message);
+      logger.atSevere().log(message);
     } catch (Exception e) {
       message = "Error running hook " + hook;
-      log.error(message, e);
+      logger.atSevere().withCause(e).log(message);
     }
     task.cancel(true);
     hookTask.cancel();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/HookFactory.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/HookFactory.java
index f3d8293..afd189d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/HookFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/HookFactory.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
@@ -23,12 +24,10 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class HookFactory {
-  private static final Logger log = LoggerFactory.getLogger(HookFactory.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final HookQueue queue;
   private final HookExecutor syncHookExecutor;
@@ -54,13 +53,13 @@
     } else {
       this.hooksPath = sitePaths.hooks_dir;
     }
-    log.info("hooks.path: {}", this.hooksPath);
+    logger.atInfo().log("hooks.path: %s", this.hooksPath);
   }
 
   private Path getHookPath(String configName, String defaultName) {
     String v = config.getString("hooks", null, configName);
     Path hookPath = hooksPath.resolve(firstNonNull(v, defaultName));
-    log.info("hooks.{} resolved to {}", configName, hookPath);
+    logger.atInfo().log("hooks.%s resolved to %s", configName, hookPath);
     return hookPath;
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/HookQueue.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/HookQueue.java
index 86c457d..50ba975 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/HookQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/HookQueue.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.hooks;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.WorkQueue;
@@ -22,11 +23,9 @@
 import java.nio.file.Path;
 import java.util.concurrent.ScheduledExecutorService;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class HookQueue implements LifecycleListener {
-  private static final Logger log = LoggerFactory.getLogger(HookQueue.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final WorkQueue workQueue;
   private final int executorThreads;
@@ -45,7 +44,7 @@
 
   void submit(String projectName, Path hook, HookArgs args) {
     if (!Files.exists(hook)) {
-      log.debug("Hook file not found: {}", hook.toAbsolutePath());
+      logger.atFine().log("Hook file not found: %s", hook.toAbsolutePath());
       return;
     }
     queue.submit(new HookTask.Async(projectName, hook, args));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/HookTask.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/HookTask.java
index b6654da..8b06a24 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/HookTask.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/HookTask.java
@@ -16,6 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.reviewdb.client.Project;
@@ -29,11 +30,9 @@
 import java.util.Map;
 import java.util.concurrent.Callable;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class HookTask {
-  private static final Logger log = LoggerFactory.getLogger(HookTask.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Path sitePath;
   private final String projectName;
@@ -113,25 +112,25 @@
     } catch (InterruptedException iex) {
       // InterruptedException - timeout or cancel
       args.metrics.timeout(name);
-      log.error("hook[{}] timed out: {}", name, iex.getMessage());
+      logger.atSevere().log("hook[%s] timed out: %s", name, iex.getMessage());
     } catch (Throwable err) {
       args.metrics.error(name);
-      log.error("Error running hook {}", hook.toAbsolutePath(), err);
+      logger.atSevere().withCause(err).log("Error running hook %s", hook.toAbsolutePath());
     }
 
     if (result != null) {
       int exitValue = result.getExitValue();
       if (exitValue != 0) {
-        log.error("hook[{}] exited with error status: {}", name, exitValue);
+        logger.atSevere().log("hook[%s] exited with error status: %d", name, exitValue);
       }
 
-      if (log.isDebugEnabled()) {
+      if (logger.atFine().isEnabled()) {
         try (BufferedReader br = new BufferedReader(new StringReader(result.getOutput()))) {
           br.lines()
               .filter(s -> !s.isEmpty())
-              .forEach(line -> log.debug("hook[{}] output: {}", name, line));
+              .forEach(line -> logger.atFine().log("hook[%s] output: %s", name, line));
         } catch (IOException iox) {
-          log.error("Error writing hook [{}] output", name, iox);
+          logger.atSevere().withCause(iox).log("Error writing hook [%s] output", name);
         }
       }
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/RefUpdate.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/RefUpdate.java
index 7eedfa5..90a1b93 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/RefUpdate.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/RefUpdate.java
@@ -49,7 +49,7 @@
     HookArgs args = hookFactory.createArgs();
     args.add("--project", projectName);
     args.add("--uploader", refEvent.user.getNameEmail());
-    args.add("--uploader-username", refEvent.user.getUserName());
+    args.add("--uploader-username", refEvent.user.getUserName().orElse(null));
     args.add("--oldrev", getObjectId(refEvent.command.getOldId()).getName());
     args.add("--newrev", getObjectId(refEvent.command.getNewId()).getName());
     args.add("--refname", refEvent.command.getRefName());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/Submit.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/Submit.java
index 8e2f1e0..e383200 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/Submit.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/Submit.java
@@ -51,7 +51,7 @@
     args.add("--project", projectName);
     args.add("--branch", destBranch.get());
     args.add("--submitter", caller.getNameEmail());
-    args.add("--submitter-username", caller.getUserName());
+    args.add("--submitter-username", caller.getUserName().orElse(null));
     args.add("--patchset", patchSetId.get());
     args.add("--commit", commit.getId().name());
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/hooks/HooksIT.java b/src/test/java/com/googlesource/gerrit/plugins/hooks/HooksIT.java
new file mode 100644
index 0000000..bcf2d20
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/hooks/HooksIT.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2019 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.
+
+package com.googlesource.gerrit.plugins.hooks;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestPlugin;
+import org.junit.Test;
+
+@NoHttpd
+@TestPlugin(name = "hooks", sysModule = "com.googlesource.gerrit.plugins.hooks.Module")
+public class HooksIT extends LightweightPluginDaemonTest {
+
+  @Test
+  public void doNothing() throws Exception {}
+}