Use commit IDs for download commands when change refs are hidden

Git has a configuration option to hide refs from the initial
advertisement (uploadpack.hideRefs). This option can be used to hide
the change refs from the client. As consequence fetching changes by
change ref is not working anymore. However by setting
uploadpack.allowTipSha1InWant to true fetching changes by commit ID is
possible. Adapt the git download commands so that they use the commit
ID instead of the change ref when a project is configured like this.
If tools use the download commands that are returned by the Gerrit
server, switching on this configuration should be transparent for
them.

Example git configuration in a project:

  [uploadpack]
    hideRefs = refs/changes/
    hideRefs = refs/cache-automerge/
    allowTipSha1InWant = true

The download commands only check for hidden change refs if this is
explicitly enabled by setting download.checkForHiddenChangeRefs in the
gerrit.config to true. This is because for servers that do not have
any projects with hidden change refs this check is unneeded. The
documentation of this new config option will be added in Gerrit core,
since for historic reasons all download configuration parameters are
described there and it wouldn't make sense to document a single
parameter in another place.

Bug: Issue 175
Change-Id: I0f1f68c856f23ffeddd0e833bf45b31549cfd6f2
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/download/command/CheckoutCommand.java b/src/main/java/com/googlesource/gerrit/plugins/download/command/CheckoutCommand.java
index b415813..65ca417 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/download/command/CheckoutCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/download/command/CheckoutCommand.java
@@ -17,12 +17,18 @@
 import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand.CHECKOUT;
 
 import com.google.gerrit.server.config.DownloadConfig;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.Config;
+
 class CheckoutCommand extends GitDownloadCommand {
   @Inject
-  CheckoutCommand(DownloadConfig downloadConfig) {
-    super(downloadConfig, CHECKOUT);
+  CheckoutCommand(@GerritServerConfig Config cfg,
+      DownloadConfig downloadConfig,
+      GitRepositoryManager repoManager) {
+    super(cfg, downloadConfig, CHECKOUT, repoManager);
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/download/command/CherryPickCommand.java b/src/main/java/com/googlesource/gerrit/plugins/download/command/CherryPickCommand.java
index 5490848..cde5a26 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/download/command/CherryPickCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/download/command/CherryPickCommand.java
@@ -17,12 +17,18 @@
 import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand.CHERRY_PICK;
 
 import com.google.gerrit.server.config.DownloadConfig;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.Config;
+
 class CherryPickCommand extends GitDownloadCommand {
   @Inject
-  CherryPickCommand(DownloadConfig downloadConfig) {
-    super(downloadConfig, CHERRY_PICK);
+  CherryPickCommand(@GerritServerConfig Config cfg,
+      DownloadConfig downloadConfig,
+      GitRepositoryManager repoManager) {
+    super(cfg, downloadConfig, CHERRY_PICK, repoManager);
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/download/command/FormatPatchCommand.java b/src/main/java/com/googlesource/gerrit/plugins/download/command/FormatPatchCommand.java
index 67e1d17..57dfe25 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/download/command/FormatPatchCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/download/command/FormatPatchCommand.java
@@ -17,12 +17,18 @@
 import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand.FORMAT_PATCH;
 
 import com.google.gerrit.server.config.DownloadConfig;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.Config;
+
 class FormatPatchCommand extends GitDownloadCommand {
   @Inject
-  FormatPatchCommand(DownloadConfig downloadConfig) {
-    super(downloadConfig, FORMAT_PATCH);
+  FormatPatchCommand(@GerritServerConfig Config cfg,
+      DownloadConfig downloadConfig,
+      GitRepositoryManager repoManager) {
+    super(cfg, downloadConfig, FORMAT_PATCH, repoManager);
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/download/command/GitDownloadCommand.java b/src/main/java/com/googlesource/gerrit/plugins/download/command/GitDownloadCommand.java
index a290856..e929bff 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/download/command/GitDownloadCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/download/command/GitDownloadCommand.java
@@ -19,21 +19,49 @@
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.DownloadConfig;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
 
 import com.googlesource.gerrit.plugins.download.scheme.RepoScheme;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.URIish;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.net.URISyntaxException;
+import java.util.Arrays;
 
 abstract class GitDownloadCommand extends DownloadCommand {
-  private final boolean commandAllowed;
+  private static final Logger log =
+      LoggerFactory.getLogger(GitDownloadCommand.class);
 
-  GitDownloadCommand(
-      DownloadConfig downloadConfig, AccountGeneralPreferences.DownloadCommand cmd) {
+  private static final String DOWNLOAD = "download";
+  private static final String UPLOADPACK = "uploadpack";
+  private static final String KEY_ALLOW_TIP_SHA1_IN_WANT = "allowTipSha1InWant";
+  private static final String KEY_CHECK_FOR_HIDDEN_CHANGE_REFS = "checkForHiddenChangeRefs";
+  private static final String KEY_HIDE_REFS = "hideRefs";
+
+  private final boolean commandAllowed;
+  private final GitRepositoryManager repoManager;
+  private final boolean checkForHiddenChangeRefs;
+
+  GitDownloadCommand(@GerritServerConfig Config cfg,
+      DownloadConfig downloadConfig,
+      AccountGeneralPreferences.DownloadCommand cmd,
+      GitRepositoryManager repoManager) {
     this.commandAllowed = downloadConfig.getDownloadCommands().contains(cmd)
         || downloadConfig.getDownloadCommands().contains(DEFAULT_DOWNLOADS);
+    this.repoManager = repoManager;
+    this.checkForHiddenChangeRefs =
+        cfg.getBoolean(DOWNLOAD, KEY_CHECK_FOR_HIDDEN_CHANGE_REFS, false);
   }
 
   @Override
@@ -42,7 +70,12 @@
     if (commandAllowed && isRecognizedScheme(scheme)) {
       String url = scheme.getUrl(project);
       if (url != null && isValidUrl(url)) {
-        return getCommand(url, ref);
+        if (checkForHiddenChangeRefs) {
+          ref = resolveRef(project, ref);
+        }
+        if (ref != null) {
+          return getCommand(url, ref);
+        }
       }
     }
     return null;
@@ -61,5 +94,37 @@
     }
   }
 
+  private String resolveRef(String project, String ref) {
+    if (project.startsWith("$") || ref.startsWith("$")) {
+      // No real value but placeholders are being used.
+      return ref;
+    }
+
+    try (Repository repo = repoManager.openRepository(new Project.NameKey(project))) {
+      Config cfg = repo.getConfig();
+      if (cfg.getBoolean(UPLOADPACK, KEY_ALLOW_TIP_SHA1_IN_WANT, false)
+          && Arrays.asList(cfg.getStringList(UPLOADPACK, null, KEY_HIDE_REFS))
+              .contains(RefNames.REFS_CHANGES)) {
+        ObjectId id = repo.resolve(ref);
+        if (id != null) {
+          return id.name();
+        } else {
+          log.error(String.format("Cannot resolve ref %s in project %s.", ref,
+              project));
+          return null;
+        }
+      } else {
+        return ref;
+      }
+    } catch (RepositoryNotFoundException e) {
+      log.error(String.format("Missing project: %s",  project), e);
+      return null;
+    } catch (IOException e) {
+      log.error(
+          String.format("Failed to lookup project %s from cache.", project), e);
+      return null;
+    }
+  }
+
   abstract String getCommand(String url, String ref);
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/download/command/PullCommand.java b/src/main/java/com/googlesource/gerrit/plugins/download/command/PullCommand.java
index b70ff56..b16211a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/download/command/PullCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/download/command/PullCommand.java
@@ -17,12 +17,18 @@
 import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand.PULL;
 
 import com.google.gerrit.server.config.DownloadConfig;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.Config;
+
 class PullCommand extends GitDownloadCommand {
   @Inject
-  PullCommand(DownloadConfig downloadConfig) {
-    super(downloadConfig, PULL);
+  PullCommand(@GerritServerConfig Config cfg,
+      DownloadConfig downloadConfig,
+      GitRepositoryManager repoManager) {
+    super(cfg, downloadConfig, PULL, repoManager);
   }
 
   @Override
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 1d66c5f..3e76bdb 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -65,3 +65,25 @@
 
 	If `download.scheme` is not specified, SSH, HTTP and Anonymous HTTP
 	downloads are allowed.
+
+<a id="download.checkForHiddenChangeRefs">download.checkForHiddenChangeRefs</a>
+:	Whether the download commands should be adapted when the change
+	refs are hidden.
+
+	Git has a configuration option to hide refs from the initial
+	advertisement (`uploadpack.hideRefs`). This option can be used to
+	hide the change refs from the client. As consequence fetching
+	changes by change ref does not work anymore. However by setting
+	`uploadpack.allowTipSha1InWant` to `true` fetching changes by
+	commit ID is possible. If `download.checkForHiddenChangeRefs` is
+	set to `true` the git download commands use the commit ID instead
+	of the change ref when a project is configured like this.
+
+	Example git configuration on a project:
+
+		[uploadpack]
+		  hideRefs = refs/changes/
+		  hideRefs = refs/cache-automerge/
+		  allowTipSha1InWant = true
+
+	By default `false`.