Display git pull download URLs on patch sets

This change shows "git pull ..." command lines for fetching
a patch set down off Gerrit using standard Git tools, rather
than relying upon the "repo download" command.  It also makes
the usage of "repo download" more optional by not showing the
repo download command line by default.

If a project is not readable by anonymous users then we offer
an SSH URL instead of an anonymous git:// URL.

Bug: GERRIT-55
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index dfdd903..a17b531 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -47,6 +47,34 @@
 entity in Gerrit maps to a local Git repository by creating
 the path string "${git_pase_path}/${project_name}.git".
 
+gitweb_url
+~~~~~~~~~~
+
+Optional URL of an affiliated gitweb service.
+
+* link:config-gitweb.html[Gitweb Integration]
+
+git_daemon_url
+~~~~~~~~~~~~~~
+
+Optional base URL for repositories available over the anonymous git
+protocol.  For example, set this to `git://mirror.example.com/base/`
+to have Gerrit display patch set download URLs in the UI.  Gerrit
+automatically appends the project name onto the end of the URL.
+
+By default NULL, as the git daemon must be configured externally
+by the system administrator, and might not even be running on the
+same host as Gerrit.
+
+use_repo_download
+~~~~~~~~~~~~~~~~~
+
+If set to `Y`, Gerrit advertises patch set downloads with the
+`repo download` command, assuming that all projects managed by this
+instance are generally worked on with the repo multi-repository tool.
+
+By default, `N`, as not all instances will deploy repo.
+
 gerrit_git_name
 ~~~~~~~~~~~~~~~
 
diff --git a/appdist/src/main/sql/upgrade003_004.sql b/appdist/src/main/sql/upgrade003_004.sql
new file mode 100644
index 0000000..064fdbc
--- /dev/null
+++ b/appdist/src/main/sql/upgrade003_004.sql
@@ -0,0 +1,10 @@
+-- Upgrade: schema_version 3 to 4
+--
+ALTER TABLE system_config ADD use_repo_download CHAR(1);
+UPDATE system_config SET use_repo_download = 'N';
+ALTER TABLE system_config ALTER COLUMN use_repo_download SET DEFAULT 'N';
+ALTER TABLE system_config ALTER COLUMN use_repo_download SET NOT NULL;
+
+ALTER TABLE system_config ADD git_daemon_url VARCHAR(255);
+
+UPDATE schema_version SET version_nbr = 4;
diff --git a/appjar/src/main/java/com/google/gerrit/client/changes/ChangeDetailServiceImpl.java b/appjar/src/main/java/com/google/gerrit/client/changes/ChangeDetailServiceImpl.java
index 4b195a6..c988254 100644
--- a/appjar/src/main/java/com/google/gerrit/client/changes/ChangeDetailServiceImpl.java
+++ b/appjar/src/main/java/com/google/gerrit/client/changes/ChangeDetailServiceImpl.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.client.reviewdb.PatchSet;
 import com.google.gerrit.client.reviewdb.ReviewDb;
 import com.google.gerrit.client.rpc.BaseServiceImplementation;
+import com.google.gerrit.client.rpc.Common;
 import com.google.gerrit.client.rpc.NoSuchEntityException;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtorm.client.OrmException;
@@ -37,8 +38,19 @@
         }
         assertCanRead(change);
 
+        final boolean anon;
+        if (Common.getAccountId() == null) {
+          // Safe assumption, this wouldn't be allowed if it wasn't.
+          //
+          anon = true;
+        } else {
+          // Ask if the anonymous user can read this project; even if
+          // we can that doesn't mean the anonymous user could.
+          //
+          anon = canRead(null, change.getDest().getParentKey());
+        }
         final ChangeDetail d = new ChangeDetail();
-        d.load(db, new AccountInfoCacheFactory(db), change);
+        d.load(db, new AccountInfoCacheFactory(db), change, anon);
         return d;
       }
     });
diff --git a/appjar/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/appjar/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index 5afd248..1b78738 100644
--- a/appjar/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/appjar/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -25,7 +25,6 @@
   String patchSetHeader(int id);
   String loadingPatchSet(int id);
   String patchSetAction(String action, int id);
-  String repoDownload(String project, int change, int ps);
 
   String patchTableComments(@PluralCount int count);
   String patchTableDrafts(@PluralCount int count);
diff --git a/appjar/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/appjar/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index aac5174..395d817 100644
--- a/appjar/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/appjar/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -6,7 +6,6 @@
 patchSetHeader = Patch Set {0}
 loadingPatchSet = Loading Patch Set {0} ...
 patchSetAction = {0} Patch Set {1}
-repoDownload = repo download {0} {1}/{2}
 
 patchTableComments = {0} comments
 patchTableDrafts = {0} drafts
diff --git a/appjar/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java b/appjar/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java
index 92640fe..f80dbd2 100644
--- a/appjar/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java
+++ b/appjar/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java
@@ -24,13 +24,16 @@
 import com.google.gerrit.client.reviewdb.Account;
 import com.google.gerrit.client.reviewdb.ApprovalCategory;
 import com.google.gerrit.client.reviewdb.ApprovalCategoryValue;
+import com.google.gerrit.client.reviewdb.Branch;
 import com.google.gerrit.client.reviewdb.PatchSet;
 import com.google.gerrit.client.reviewdb.PatchSetInfo;
+import com.google.gerrit.client.reviewdb.Project;
 import com.google.gerrit.client.reviewdb.UserIdentity;
 import com.google.gerrit.client.rpc.Common;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.DomUtil;
 import com.google.gerrit.client.ui.RefreshListener;
+import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.ClickListener;
 import com.google.gwt.user.client.ui.Composite;
@@ -129,9 +132,7 @@
     final PatchSetInfo info = detail.getInfo();
     displayUserIdentity(R_AUTHOR, info.getAuthor());
     displayUserIdentity(R_COMMITTER, info.getCommitter());
-    infoTable.setText(R_DOWNLOAD, 1, Util.M.repoDownload(changeDetail
-        .getChange().getDest().getParentKey().get(), changeDetail.getChange()
-        .getChangeId(), patchSet.getPatchSetId()));
+    displayDownload();
 
 
     patchTable = new PatchTable();
@@ -162,6 +163,67 @@
     body.add(patchTable);
   }
 
+  private void displayDownload() {
+    final Branch.NameKey branchKey = changeDetail.getChange().getDest();
+    final Project.NameKey projectKey = branchKey.getParentKey();
+    final String projectName = projectKey.get();
+
+    final StringBuilder r = new StringBuilder();
+
+    if (Common.getGerritConfig().isUseRepoDownload()) {
+      // This site prefers usage of the 'repo' tool, so suggest
+      // that for easy fetch.
+      //
+      if (r.length() > 0) {
+        r.append("\n");
+      }
+      r.append("repo download ");
+      r.append(projectName);
+      r.append(" ");
+      r.append(changeDetail.getChange().getChangeId());
+      r.append("/");
+      r.append(patchSet.getPatchSetId());
+    }
+
+    if (changeDetail.isAllowsAnonymous()
+        && Common.getGerritConfig().getGitDaemonUrl() != null) {
+      // Anonymous Git is claimed to be available, and this project
+      // isn't secured. The anonymous Git daemon will be much more
+      // efficient than our own SSH daemon, so prefer offering it.
+      //
+      if (r.length() > 0) {
+        r.append("\n");
+      }
+      r.append("git pull ");
+      r.append(Common.getGerritConfig().getGitDaemonUrl());
+      r.append(projectName);
+      r.append(" ");
+      r.append(patchSet.getRefName());
+    } else if (Gerrit.isSignedIn() && Gerrit.getUserAccount() != null
+        && Gerrit.getUserAccount().getSshUserName() != null
+        && Gerrit.getUserAccount().getSshUserName().length() > 0) {
+      // The user is signed in and anonymous access isn't allowed.
+      // Use our SSH daemon URL as its the only way they can get
+      // to the project (that we know of anyway).
+      //
+      if (r.length() > 0) {
+        r.append("\n");
+      }
+      r.append("git pull ssh://");
+      r.append(Gerrit.getUserAccount().getSshUserName());
+      r.append("@");
+      r.append(Window.Location.getHostName());
+      r.append(":");
+      r.append(Common.getGerritConfig().getSshdPort());
+      r.append("/");
+      r.append(projectName);
+      r.append(" ");
+      r.append(patchSet.getRefName());
+    }
+
+    infoTable.setText(R_DOWNLOAD, 1, r.toString());
+  }
+
   private void displayUserIdentity(final int row, final UserIdentity who) {
     if (who == null) {
       infoTable.clearCell(row, 1);
diff --git a/appjar/src/main/java/com/google/gerrit/client/data/ChangeDetail.java b/appjar/src/main/java/com/google/gerrit/client/data/ChangeDetail.java
index faaf2a4..25e8b31 100644
--- a/appjar/src/main/java/com/google/gerrit/client/data/ChangeDetail.java
+++ b/appjar/src/main/java/com/google/gerrit/client/data/ChangeDetail.java
@@ -40,6 +40,7 @@
 /** Detail necessary to display {@link ChangeScreen}. */
 public class ChangeDetail {
   protected AccountInfoCache accounts;
+  protected boolean allowsAnonymous;
   protected Change change;
   protected List<ChangeInfo> dependsOn;
   protected List<ChangeInfo> neededBy;
@@ -55,11 +56,12 @@
   }
 
   public void load(final ReviewDb db, final AccountInfoCacheFactory acc,
-      final Change c) throws OrmException {
+      final Change c, final boolean allowAnon) throws OrmException {
     change = c;
     final Account.Id owner = change.getOwner();
     acc.want(owner);
 
+    allowsAnonymous = allowAnon;
     patchSets = db.patchSets().byChange(change.getId()).toList();
     messages = db.changeMessages().byChange(change.getId()).toList();
     for (final ChangeMessage m : messages) {
@@ -171,6 +173,10 @@
     return accounts;
   }
 
+  public boolean isAllowsAnonymous() {
+    return allowsAnonymous;
+  }
+
   public Change getChange() {
     return change;
   }
diff --git a/appjar/src/main/java/com/google/gerrit/client/data/GerritConfig.java b/appjar/src/main/java/com/google/gerrit/client/data/GerritConfig.java
index 29a6eb1..fb45a48 100644
--- a/appjar/src/main/java/com/google/gerrit/client/data/GerritConfig.java
+++ b/appjar/src/main/java/com/google/gerrit/client/data/GerritConfig.java
@@ -30,6 +30,8 @@
   protected int sshdPort;
   protected boolean useContributorAgreements;
   protected SystemConfig.LoginType loginType;
+  protected boolean useRepoDownload;
+  protected String gitDaemonUrl;
   private transient Map<ApprovalCategory.Id, ApprovalType> byCategoryId;
 
   public GerritConfig() {
@@ -124,4 +126,23 @@
     }
     return byCategoryId.get(id);
   }
+
+  public boolean isUseRepoDownload() {
+    return useRepoDownload;
+  }
+
+  public void setUseRepoDownload(final boolean r) {
+    useRepoDownload = r;
+  }
+
+  public String getGitDaemonUrl() {
+    return gitDaemonUrl;
+  }
+
+  public void setGitDaemonUrl(String url) {
+    if (url != null && !url.endsWith("/")) {
+      url += "/";
+    }
+    gitDaemonUrl = url;
+  }
 }
diff --git a/appjar/src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java b/appjar/src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java
index a5e2232..44e4e3e 100644
--- a/appjar/src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java
+++ b/appjar/src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java
@@ -21,7 +21,7 @@
 
 /** The review service database schema. */
 public interface ReviewDb extends Schema {
-  public static final int VERSION = 3;
+  public static final int VERSION = 4;
 
   @Relation
   SchemaVersionAccess schemaVersion();
diff --git a/appjar/src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java b/appjar/src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java
index e8f4651..49aeeb5 100644
--- a/appjar/src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java
+++ b/appjar/src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java
@@ -93,6 +93,14 @@
   @Column(notNull = false)
   public String gitwebUrl;
 
+  /**
+   * Optional URL of the anonymous git daemon for project access.
+   * <p>
+   * For example: <code>git://host/base/</code>
+   */
+  @Column(notNull = false)
+  public String gitDaemonUrl;
+
   /** Local filesystem location all projects reside within. */
   @Column(notNull = false)
   public transient String gitBasePath;
@@ -136,6 +144,10 @@
   @Column
   public int sshdPort;
 
+  /** Should Gerrit advertise 'repo download' for patch sets? */
+  @Column
+  public boolean useRepoDownload;
+
   /** Identity of the administration group; those with full access. */
   @Column
   public AccountGroup.Id adminGroupId;
diff --git a/appjar/src/main/java/com/google/gerrit/server/GerritServer.java b/appjar/src/main/java/com/google/gerrit/server/GerritServer.java
index eca8e78..21883a4 100644
--- a/appjar/src/main/java/com/google/gerrit/server/GerritServer.java
+++ b/appjar/src/main/java/com/google/gerrit/server/GerritServer.java
@@ -427,6 +427,8 @@
     r.setCanonicalUrl(getCanonicalURL());
     r.setSshdPort(sConfig.sshdPort);
     r.setUseContributorAgreements(sConfig.useContributorAgreements);
+    r.setGitDaemonUrl(sConfig.gitDaemonUrl);
+    r.setUseRepoDownload(sConfig.useRepoDownload);
     r.setLoginType(sConfig.getLoginType());
     if (sConfig.gitwebUrl != null) {
       r.setGitwebLink(new GitwebLink(sConfig.gitwebUrl));