Merge branch 'stable-2.14' into stable-2.15

* stable-2.14:
  Fix access path propagation on git/ssh protocol

Change-Id: Ia38a54d210fb53dcb36a364f885cb1f2e17abf23
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
index b9a98b9..c747c3e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -33,8 +33,6 @@
   @Argument(index = 0, metaVar = "PROJECT.git", required = true, usage = "project name")
   protected ProjectControl projectControl;
 
-  @Inject private SshScope sshScope;
-
   @Inject private GitRepositoryManager repoManager;
 
   @Inject private SshSession session;
@@ -53,28 +51,25 @@
   @Override
   public void start(Environment env) {
     Context ctx = context.subContext(newSession(), context.getCommandLine());
-    final Context old = sshScope.set(ctx);
-    try {
-      startThread(
-          new ProjectCommandRunnable() {
-            @Override
-            public void executeParseCommand() throws Exception {
-              parseCommandLine();
-            }
+    startThreadWithContext(
+        ctx,
+        new ProjectCommandRunnable() {
+          @Override
+          public void executeParseCommand() throws Exception {
+            parseCommandLine();
+          }
 
-            @Override
-            public void run() throws Exception {
-              AbstractGitCommand.this.service();
-            }
+          @Override
+          public void run() throws Exception {
+            AbstractGitCommand.this.service();
+          }
 
-            @Override
-            public Project.NameKey getProjectName() {
-              return projectControl.getProjectState().getNameKey();
-            }
-          });
-    } finally {
-      sshScope.set(old);
-    }
+          @Override
+          public Project.NameKey getProjectName() {
+            Project project = projectControl.getProjectState().getProject();
+            return project.getNameKey();
+          }
+        });
   }
 
   private SshSession newSession() {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index 788b33f..c881b87 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -49,6 +49,7 @@
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.nio.charset.Charset;
+import java.util.Optional;
 import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.atomic.AtomicReference;
@@ -260,6 +261,38 @@
   }
 
   /**
+   * Spawn a function into its own thread with the provided context.
+   *
+   * <p>Typically this should be invoked within {@link Command#start(Environment)}, such as:
+   *
+   * <pre>
+   * startThreadWithContext(SshScope.Context context, new CommandRunnable() {
+   *   public void run() throws Exception {
+   *     runImp();
+   *   }
+   * });
+   * </pre>
+   *
+   * <p>If the function throws an exception, it is translated to a simple message for the client, a
+   * non-zero exit code, and the stack trace is logged.
+   *
+   * @param thunk the runnable to execute on the thread, performing the command's logic.
+   */
+  protected void startThreadWithContext(SshScope.Context context, CommandRunnable thunk) {
+    final TaskThunk tt = new TaskThunk(thunk, Optional.ofNullable(context));
+
+    if (isAdminHighPriorityCommand()) {
+      // Admin commands should not block the main work threads (there
+      // might be an interactive shell there), nor should they wait
+      // for the main work threads.
+      //
+      new Thread(tt, tt.toString()).start();
+    } else {
+      task.set(executor.submit(tt));
+    }
+  }
+
+  /**
    * Spawn a function into its own thread.
    *
    * <p>Typically this should be invoked within {@link Command#start(Environment)}, such as:
@@ -277,18 +310,8 @@
    *
    * @param thunk the runnable to execute on the thread, performing the command's logic.
    */
-  protected void startThread(CommandRunnable thunk) {
-    final TaskThunk tt = new TaskThunk(thunk);
-
-    if (isAdminHighPriorityCommand()) {
-      // Admin commands should not block the main work threads (there
-      // might be an interactive shell there), nor should they wait
-      // for the main work threads.
-      //
-      new Thread(tt, tt.toString()).start();
-    } else {
-      task.set(executor.submit(tt));
-    }
+  protected void startThread(final CommandRunnable thunk) {
+    startThreadWithContext(null, thunk);
   }
 
   private boolean isAdminHighPriorityCommand() {
@@ -413,18 +436,21 @@
 
   private final class TaskThunk implements CancelableRunnable, ProjectRunnable {
     private final CommandRunnable thunk;
+    private final Context taskContext;
     private final String taskName;
+
     private Project.NameKey projectName;
 
-    private TaskThunk(CommandRunnable thunk) {
+    private TaskThunk(CommandRunnable thunk, Optional<Context> oneOffContext) {
       this.thunk = thunk;
       this.taskName = getTaskName();
+      this.taskContext = oneOffContext.orElse(context);
     }
 
     @Override
     public void cancel() {
       synchronized (this) {
-        final Context old = sshScope.set(context);
+        final Context old = sshScope.set(taskContext);
         try {
           onExit(STATUS_CANCEL);
         } finally {
@@ -439,7 +465,7 @@
         final Thread thisThread = Thread.currentThread();
         final String thisName = thisThread.getName();
         int rc = 0;
-        final Context old = sshScope.set(context);
+        final Context old = sshScope.set(taskContext);
         try {
           context.started = TimeUtil.nowMs();
           thisThread.setName("SSH " + taskName);