Show download links as a tabbed panel

Users can now select which link they prefer to see, and only that
one is displayed in the UI panel.  This reduces the vertical space
we need to show the different command permutations that we support
to download a patch set from a change.

If the user is signed-in, their most recent preference is stored
in the database as part of their user account, so future displays
will default back to that type of link again.

Change-Id: I6bc9c21ad4f9b1e8124530d016aea3c57c2a1bcb
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index 626c25b3..c552d8e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -50,7 +50,6 @@
   String changeTypeCell();
   String changeid();
   String closedstate();
-  String command();
   String commentCell();
   String commentEditorPanel();
   String commentHolder();
@@ -81,6 +80,13 @@
   String diffTextHunkHeader();
   String diffTextINSERT();
   String diffTextNoLF();
+  String downloadLink();
+  String downloadLink_Active();
+  String downloadLinkListCell();
+  String downloadLinkCopyLabel();
+  String downloadLinkHeader();
+  String downloadLinkHeaderGap();
+  String downloadLinkList();
   String drafts();
   String emptySection();
   String errorDialog();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index 26ffe36..6dab540 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -47,4 +47,6 @@
 
   String accountNotFound(String who);
   String changeNotVisibleTo(String who);
+
+  String anonymousDownload(String protocol);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index 8f3def1..e25e9fa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -28,3 +28,5 @@
 
 accountNotFound = {0} is not a registered user.
 changeNotVisibleTo = {0} cannot access the change.
+
+anonymousDownload = Anonymous {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandLink.java
new file mode 100644
index 0000000..1260819
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandLink.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2010 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.google.gerrit.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Accessibility;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtjsonrpc.client.VoidResult;
+
+abstract class DownloadCommandLink extends Anchor implements ClickHandler {
+  final AccountGeneralPreferences.DownloadCommand cmdType;
+
+  DownloadCommandLink(AccountGeneralPreferences.DownloadCommand cmdType,
+      String text) {
+    super(text);
+    this.cmdType = cmdType;
+    setStyleName(Gerrit.RESOURCES.css().downloadLink());
+    Accessibility.setRole(getElement(), Accessibility.ROLE_TAB);
+    addClickHandler(this);
+  }
+
+  @Override
+  public void onClick(ClickEvent event) {
+    event.preventDefault();
+    event.stopPropagation();
+
+    select();
+
+    if (Gerrit.isSignedIn()) {
+      // If the user is signed-in, remember this choice for future panels.
+      //
+      AccountGeneralPreferences pref =
+          Gerrit.getUserAccount().getGeneralPreferences();
+      pref.setDownloadCommand(cmdType);
+      com.google.gerrit.client.account.Util.ACCOUNT_SVC.changePreferences(pref,
+          new AsyncCallback<VoidResult>() {
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+
+            @Override
+            public void onSuccess(VoidResult result) {
+            }
+          });
+    }
+  }
+
+  void select() {
+    DownloadCommandPanel parent = (DownloadCommandPanel) getParent();
+    for (Widget w : parent) {
+      if (w != this && w instanceof DownloadCommandLink) {
+        w.removeStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
+      }
+    }
+    parent.setCurrentCommand(this);
+    addStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
+  }
+
+  abstract void setCurrentUrl(DownloadUrlLink link);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandPanel.java
new file mode 100644
index 0000000..3ecf527
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandPanel.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2010 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.google.gerrit.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gwt.user.client.ui.Accessibility;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+class DownloadCommandPanel extends FlowPanel {
+  private DownloadCommandLink currentCommand;
+  private DownloadUrlLink currentUrl;
+
+  DownloadCommandPanel() {
+    setStyleName(Gerrit.RESOURCES.css().downloadLinkList());
+    Accessibility.setRole(getElement(), Accessibility.ROLE_TABLIST);
+  }
+
+  boolean isEmpty() {
+    return getWidgetCount() == 0;
+  }
+
+  void select(AccountGeneralPreferences.DownloadCommand cmdType) {
+    DownloadCommandLink first = null;
+
+    for (Widget w : this) {
+      if (w instanceof DownloadCommandLink) {
+        final DownloadCommandLink d = (DownloadCommandLink) w;
+        if (first == null) {
+          first = d;
+        }
+        if (d.cmdType == cmdType) {
+          d.select();
+          return;
+        }
+      }
+    }
+
+    // If none matched the requested type, select the first in the
+    // group as that will at least give us an initial baseline.
+    if (first != null) {
+      first.select();
+    }
+  }
+
+  void setCurrentUrl(DownloadUrlLink link) {
+    currentUrl = link;
+    update();
+  }
+
+  void setCurrentCommand(DownloadCommandLink cmd) {
+    currentCommand = cmd;
+    update();
+  }
+
+  private void update() {
+    if (currentCommand != null && currentUrl != null) {
+      currentCommand.setCurrentUrl(currentUrl);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlLink.java
new file mode 100644
index 0000000..6f52ef9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlLink.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2010 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.google.gerrit.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Accessibility;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtjsonrpc.client.VoidResult;
+
+class DownloadUrlLink extends Anchor implements ClickHandler {
+  final AccountGeneralPreferences.DownloadUrl urlType;
+  final String urlData;
+
+  DownloadUrlLink(AccountGeneralPreferences.DownloadUrl urlType, String text,
+      String urlData) {
+    super(text);
+    this.urlType = urlType;
+    this.urlData = urlData;
+    setStyleName(Gerrit.RESOURCES.css().downloadLink());
+    Accessibility.setRole(getElement(), Accessibility.ROLE_TAB);
+    addClickHandler(this);
+  }
+
+  @Override
+  public void onClick(ClickEvent event) {
+    event.preventDefault();
+    event.stopPropagation();
+
+    select();
+
+    if (Gerrit.isSignedIn()) {
+      // If the user is signed-in, remember this choice for future panels.
+      //
+      AccountGeneralPreferences pref =
+          Gerrit.getUserAccount().getGeneralPreferences();
+      pref.setDownloadUrl(urlType);
+      com.google.gerrit.client.account.Util.ACCOUNT_SVC.changePreferences(pref,
+          new AsyncCallback<VoidResult>() {
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+
+            @Override
+            public void onSuccess(VoidResult result) {
+            }
+          });
+    }
+  }
+
+  void select() {
+    DownloadUrlPanel parent = (DownloadUrlPanel) getParent();
+    for (Widget w : parent) {
+      if (w != this && w instanceof DownloadUrlLink) {
+        w.removeStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
+      }
+    }
+    parent.setCurrentUrl(this);
+    addStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlPanel.java
new file mode 100644
index 0000000..783963b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlPanel.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2010 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.google.gerrit.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gwt.user.client.ui.Accessibility;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+class DownloadUrlPanel extends FlowPanel {
+  private final DownloadCommandPanel commandPanel;
+
+  DownloadUrlPanel(final DownloadCommandPanel commandPanel) {
+    this.commandPanel = commandPanel;
+    setStyleName(Gerrit.RESOURCES.css().downloadLinkList());
+    Accessibility.setRole(getElement(), Accessibility.ROLE_TABLIST);
+  }
+
+  boolean isEmpty() {
+    return getWidgetCount() == 0;
+  }
+
+  void select(AccountGeneralPreferences.DownloadUrl urlType) {
+    DownloadUrlLink first = null;
+
+    for (Widget w : this) {
+      if (w instanceof DownloadUrlLink) {
+        final DownloadUrlLink d = (DownloadUrlLink) w;
+        if (first == null) {
+          first = d;
+        }
+        if (d.urlType == urlType) {
+          d.select();
+          return;
+        }
+      }
+    }
+
+    // If none matched the requested type, select the first in the
+    // group as that will at least give us an initial baseline.
+    if (first != null) {
+      first.select();
+    }
+  }
+
+  void setCurrentUrl(DownloadUrlLink link) {
+    commandPanel.setCurrentUrl(link);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java
index c60e594..c2d1e96 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
@@ -29,6 +30,8 @@
 import com.google.gerrit.reviewdb.PatchSetInfo;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.UserIdentity;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadUrl;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -39,6 +42,7 @@
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.DisclosurePanel;
+import com.google.gwt.user.client.ui.FlexTable;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.InlineLabel;
@@ -92,7 +96,8 @@
     itfmt.addStyleName(R_CNT - 1, 0, Gerrit.RESOURCES.css().bottomheader());
     itfmt.addStyleName(R_AUTHOR, 1, Gerrit.RESOURCES.css().useridentity());
     itfmt.addStyleName(R_COMMITTER, 1, Gerrit.RESOURCES.css().useridentity());
-    itfmt.addStyleName(R_DOWNLOAD, 1, Gerrit.RESOURCES.css().command());
+    itfmt.addStyleName(R_DOWNLOAD, 1, Gerrit.RESOURCES.css()
+        .downloadLinkListCell());
 
     final PatchSetInfo info = detail.getInfo();
     displayUserIdentity(R_AUTHOR, info.getAuthor());
@@ -122,67 +127,58 @@
     final Branch.NameKey branchKey = changeDetail.getChange().getDest();
     final Project.NameKey projectKey = changeDetail.getChange().getProject();
     final String projectName = projectKey.get();
-    final FlowPanel downloads = new FlowPanel();
+    final CopyableLabel copyLabel = new CopyableLabel("");
+    final DownloadCommandPanel commands = new DownloadCommandPanel();
+    final DownloadUrlPanel urls = new DownloadUrlPanel(commands);
 
-    if (Gerrit.getConfig().isUseRepoDownload()) {
-      // This site prefers usage of the 'repo' tool, so suggest
-      // that for easy fetch.
-      //
-      final StringBuilder r = new StringBuilder();
-      r.append("repo download ");
+    copyLabel.setStyleName(Gerrit.RESOURCES.css().downloadLinkCopyLabel());
+
+    if (changeDetail.isAllowsAnonymous()
+        && Gerrit.getConfig().getGitDaemonUrl() != null) {
+      StringBuilder r = new StringBuilder();
+      r.append(Gerrit.getConfig().getGitDaemonUrl());
       r.append(projectName);
       r.append(" ");
-      r.append(changeDetail.getChange().getChangeId());
-      r.append("/");
-      r.append(patchSet.getPatchSetId());
-      downloads.add(new CopyableLabel(r.toString()));
+      r.append(patchSet.getRefName());
+      urls.add(new DownloadUrlLink(DownloadUrl.ANON_GIT, Util.M
+          .anonymousDownload("Git"), r.toString()));
     }
 
     if (changeDetail.isAllowsAnonymous()) {
-      if (Gerrit.getConfig().getGitDaemonUrl() != null) {
-        StringBuilder r = new StringBuilder();
-        r.append("git pull ");
-        r.append(Gerrit.getConfig().getGitDaemonUrl());
-        r.append(projectName);
-        r.append(" ");
-        r.append(patchSet.getRefName());
-        downloads.add(new CopyableLabel(r.toString()));
-      }
-
       StringBuilder r = new StringBuilder();
-      r.append("git pull ");
       r.append(GWT.getHostPageBaseURL());
       r.append("p/");
       r.append(projectName);
       r.append(" ");
       r.append(patchSet.getRefName());
-      downloads.add(new CopyableLabel(r.toString()));
+      urls.add(new DownloadUrlLink(DownloadUrl.ANON_HTTP, Util.M
+          .anonymousDownload("HTTP"), r.toString()));
+    }
 
-    } else if (Gerrit.isSignedIn()
+    if (Gerrit.getConfig().getSshdAddress() != null && Gerrit.isSignedIn()
         && Gerrit.getUserAccount().getUserName() != null
         && Gerrit.getUserAccount().getUserName().length() > 0) {
-      // The user is signed in and anonymous access isn't allowed.
-      //
-      if (Gerrit.getConfig().getSshdAddress() != null) {
-        String sshAddr = Gerrit.getConfig().getSshdAddress();
-        final StringBuilder r = new StringBuilder();
-        r.append("git pull ssh://");
-        r.append(Gerrit.getUserAccount().getUserName());
-        r.append("@");
-        if (sshAddr.startsWith("*:") || "".equals(sshAddr)) {
-          r.append(Window.Location.getHostName());
-        }
-        if (sshAddr.startsWith("*")) {
-          sshAddr = sshAddr.substring(1);
-        }
-        r.append(sshAddr);
-        r.append("/");
-        r.append(projectName);
-        r.append(" ");
-        r.append(patchSet.getRefName());
-        downloads.add(new CopyableLabel(r.toString()));
+      String sshAddr = Gerrit.getConfig().getSshdAddress();
+      final StringBuilder r = new StringBuilder();
+      r.append("ssh://");
+      r.append(Gerrit.getUserAccount().getUserName());
+      r.append("@");
+      if (sshAddr.startsWith("*:") || "".equals(sshAddr)) {
+        r.append(Window.Location.getHostName());
       }
+      if (sshAddr.startsWith("*")) {
+        sshAddr = sshAddr.substring(1);
+      }
+      r.append(sshAddr);
+      r.append("/");
+      r.append(projectName);
+      r.append(" ");
+      r.append(patchSet.getRefName());
+      urls.add(new DownloadUrlLink(DownloadUrl.SSH, "SSH", r.toString()));
+    }
 
+    if (Gerrit.isSignedIn() && Gerrit.getUserAccount().getUserName() != null
+        && Gerrit.getUserAccount().getUserName().length() > 0) {
       String base = GWT.getHostPageBaseURL();
       int p = base.indexOf("://");
       int s = base.indexOf('/', p + 3);
@@ -195,7 +191,6 @@
       }
 
       final StringBuilder r = new StringBuilder();
-      r.append("git pull ");
       r.append(base.substring(0, p + 3));
       r.append(Gerrit.getUserAccount().getUserName());
       r.append('@');
@@ -205,10 +200,74 @@
       r.append(projectName);
       r.append(" ");
       r.append(patchSet.getRefName());
-      downloads.add(new CopyableLabel(r.toString()));
+      urls.add(new DownloadUrlLink(DownloadUrl.HTTP, "HTTP", r.toString()));
     }
 
-    infoTable.setWidget(R_DOWNLOAD, 1, downloads);
+    if (Gerrit.getConfig().isUseRepoDownload()) {
+      // This site prefers usage of the 'repo' tool, so suggest
+      // that for easy fetch.
+      //
+      final StringBuilder r = new StringBuilder();
+      r.append("repo download ");
+      r.append(projectName);
+      r.append(" ");
+      r.append(changeDetail.getChange().getChangeId());
+      r.append("/");
+      r.append(patchSet.getPatchSetId());
+      final String cmd = r.toString();
+      commands.add(new DownloadCommandLink(DownloadCommand.REPO_DOWNLOAD,
+          "repo download") {
+        @Override
+        void setCurrentUrl(DownloadUrlLink link) {
+          urls.setVisible(false);
+          copyLabel.setText(cmd);
+        }
+      });
+    }
+
+    if (!urls.isEmpty()) {
+      commands.add(new DownloadCommandLink(DownloadCommand.PULL, "pull") {
+        @Override
+        void setCurrentUrl(DownloadUrlLink link) {
+          urls.setVisible(true);
+          copyLabel.setText("git pull " + link.urlData);
+        }
+      });
+      commands.add(new DownloadCommandLink(DownloadCommand.CHERRY_PICK,
+          "cherry-pick") {
+        @Override
+        void setCurrentUrl(DownloadUrlLink link) {
+          urls.setVisible(true);
+          copyLabel.setText("git fetch " + link.urlData
+              + " && git cherry-pick FETCH_HEAD");
+        }
+      });
+    }
+
+    final FlowPanel fp = new FlowPanel();
+    if (!commands.isEmpty()) {
+      final AccountGeneralPreferences pref;
+      if (Gerrit.isSignedIn()) {
+        pref = Gerrit.getUserAccount().getGeneralPreferences();
+      } else {
+        pref = new AccountGeneralPreferences();
+        pref.resetToDefaults();
+      }
+      commands.select(pref.getDownloadCommand());
+      urls.select(pref.getDownloadUrl());
+
+      FlowPanel p = new FlowPanel();
+      p.setStyleName(Gerrit.RESOURCES.css().downloadLinkHeader());
+      p.add(commands);
+      final InlineLabel glue = new InlineLabel();
+      glue.setStyleName(Gerrit.RESOURCES.css().downloadLinkHeaderGap());
+      p.add(glue);
+      p.add(urls);
+
+      fp.add(p);
+      fp.add(copyLabel);
+    }
+    infoTable.setWidget(R_DOWNLOAD, 1, fp);
   }
 
   private void displayUserIdentity(final int row, final UserIdentity who) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index bb6e225..ed5d1ed 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -647,6 +647,7 @@
   padding-left: 20px;
   font-size: 8pt;
 }
+
 .patchSetLink {
   padding-left: 0.5em;
   font-size: 8pt;
@@ -685,7 +686,6 @@
 
 .infoTable td.header {
   background-color: trim-color;
-  border-left: 1px solid white;
   font-weight: normal;
   padding: 2px 4px 0 6px;
   font-style: italic;
@@ -748,7 +748,6 @@
 }
 
 .infoBlock td.header {
-  border-bottom: 1px solid white;
   background-color: trim-color;
   font-style: italic;
   text-align: right;
@@ -775,12 +774,6 @@
   display: inline;
 }
 
-.infoBlock td.command {
-  white-space: pre;
-  font-family: mono-font;
-  font-size: 12px;
-}
-
 .infoBlock td.useridentity {
   white-space: nowrap;
 }
@@ -827,6 +820,46 @@
   white-space: nowrap;
 }
 
+td.downloadLinkListCell {
+  padding: 0px;
+}
+.downloadLinkHeader {
+  background: trim-color;
+  white-space: nowrap;
+  border-bottom: 1px solid black;
+}
+.downloadLinkHeaderGap {
+  margin-left: 5em;
+}
+.downloadLinkList {
+  display: inline;
+  white-space: nowrap;
+}
+.downloadLink {
+  color: black;
+  text-decoration: none;
+  white-space: nowrap;
+  background: trim-color;
+  border-right: 1px solid black;
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+}
+a:hover.downloadLink {
+  color: black;
+}
+.downloadLink_Active {
+  background: #ffffcc;
+}
+.downloadLinkCopyLabel {
+  white-space: pre;
+  font-family: mono-font;
+  font-size: 12px;
+  margin-left: 0.5em;
+  margin-right: 0.5em;
+}
+.downloadLinkCopyLabel .gwt-TextBox {
+  width: 30em;
+}
 
 /** SideBySideScreen **/
 .sideBySideScreenSideBySideTable {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java
index 6af89ce..f7756fc 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java
@@ -34,6 +34,16 @@
   /** Valid choices for the page size. */
   public static final short[] PAGESIZE_CHOICES = {10, 25, 50, 100};
 
+  /** Preferred URL type to download a change. */
+  public static enum DownloadUrl {
+    ANON_GIT, ANON_HTTP, ANON_SSH, HTTP, SSH;
+  }
+
+  /** Preferred method to download a change. */
+  public static enum DownloadCommand {
+    REPO_DOWNLOAD, PULL, CHERRY_PICK;
+  }
+
   /** Default number of lines of context when viewing a patch. */
   @Column(id = 1)
   protected short defaultContext;
@@ -50,6 +60,14 @@
   @Column(id = 4)
   protected boolean useFlashClipboard;
 
+  /** Type of download URL the user prefers to use. */
+  @Column(id = 5, length = 20, notNull = false)
+  protected String downloadUrl;
+
+  /** Type of download command the user prefers to use. */
+  @Column(id = 6, length = 20, notNull = false)
+  protected String downloadCommand;
+
   public AccountGeneralPreferences() {
   }
 
@@ -87,10 +105,42 @@
     useFlashClipboard = b;
   }
 
+  public DownloadUrl getDownloadUrl() {
+    if (downloadUrl == null) {
+      return null;
+    }
+    return DownloadUrl.valueOf(downloadUrl);
+  }
+
+  public void setDownloadUrl(DownloadUrl url) {
+    if (url != null) {
+      downloadUrl = url.name();
+    } else {
+      downloadUrl = null;
+    }
+  }
+
+  public DownloadCommand getDownloadCommand() {
+    if (downloadCommand == null) {
+      return null;
+    }
+    return DownloadCommand.valueOf(downloadCommand);
+  }
+
+  public void setDownloadCommand(DownloadCommand cmd) {
+    if (cmd != null) {
+      downloadCommand = cmd.name();
+    } else {
+      downloadCommand = null;
+    }
+  }
+
   public void resetToDefaults() {
     defaultContext = DEFAULT_CONTEXT;
     maximumPageSize = DEFAULT_PAGESIZE;
     showSiteHeader = true;
     useFlashClipboard = true;
+    downloadUrl = null;
+    downloadCommand = null;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 4368674..2c26104 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  private static final Class<? extends SchemaVersion> C = Schema_28.class;
+  private static final Class<? extends SchemaVersion> C = Schema_29.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_29.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_29.java
new file mode 100644
index 0000000..37920bf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_29.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2010 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.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class Schema_29 extends SchemaVersion {
+  @Inject
+  Schema_29(Provider<Schema_28> prior) {
+    super(prior);
+  }
+}