Merge changes Id3047574,I70d0981e

* changes:
  ChangeScreen2: provide commands for downloading a patch set
  /accounts/self/preferences REST API
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
index 2de4608..06fad36 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -29,6 +29,10 @@
  * accounts.
  */
 public class AccountApi {
+  public static RestApi self() {
+    return new RestApi("/accounts/").view("self");
+  }
+
   /** Retrieve the username */
   public static void getUsername(String account, AsyncCallback<NativeString> cb) {
     new RestApi("/accounts/").id(account).view("username").get(cb);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
index 44e4456..ddb2c7f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
@@ -117,6 +117,7 @@
   private UpdateAvailableBar updateAvailable;
   private boolean openReplyBox;
 
+  @UiField HTMLPanel headerLine;
   @UiField Style style;
   @UiField ToggleButton star;
   @UiField Reload reload;
@@ -145,6 +146,7 @@
   @UiField FileTable files;
   @UiField FlowPanel history;
 
+  @UiField Button download;
   @UiField Button reply;
   @UiField Button expandAll;
   @UiField Button collapseAll;
@@ -152,6 +154,7 @@
   @UiField QuickApprove quickApprove;
   private ReplyAction replyAction;
   private EditMessageAction editMessageAction;
+  private DownloadAction downloadAction;
 
   public ChangeScreen2(Change.Id changeId, String revision, boolean openReplyBox) {
     this.changeId = changeId;
@@ -243,14 +246,22 @@
     }
   }
 
-  private void initEditMessageAction() {
-    NativeMap<ActionInfo> actions = changeInfo.revision(revision).actions();
+  private void renderDownload(ChangeInfo info, String revision) {
+    downloadAction = new DownloadAction(
+        info.legacy_id(),
+        info.project(),
+        info.revision(revision),
+        style, headerLine, download);
+  }
+
+  private void initEditMessageAction(ChangeInfo info, String revision) {
+    NativeMap<ActionInfo> actions = info.revision(revision).actions();
     if (actions != null && actions.containsKey("message")) {
       editMessage.setVisible(true);
       editMessageAction = new EditMessageAction(
-          changeInfo.legacy_id(),
+          info.legacy_id(),
           revision,
-          changeInfo.revision(revision).commit().message(),
+          info.revision(revision).commit().message(),
           style,
           editMessage,
           reply);
@@ -324,6 +335,11 @@
     StarredChanges.toggleStar(changeId, e.getValue());
   }
 
+  @UiHandler("download")
+  void onDownload(ClickEvent e) {
+    downloadAction.show();
+  }
+
   @UiHandler("revisionList")
   void onChangeRevision(ChangeEvent e) {
     int idx = revisionList.getSelectedIndex();
@@ -580,6 +596,7 @@
     renderOwner(info);
     renderReviewers(info);
     renderActionTextDate(info);
+    renderDownload(info, revision);
     renderRevisions(info);
     renderHistory(info);
     actions.display(info, revision);
@@ -598,7 +615,7 @@
     quickApprove.set(info, revision);
 
     if (Gerrit.isSignedIn()) {
-      initEditMessageAction();
+      initEditMessageAction(info, revision);
       replyAction = new ReplyAction(info, revision, style, reply);
       if (topic.canEdit()) {
         keysAction.add(new KeyCommand(0, 't', Util.C.keyEditTopic()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
index cea072b..ee0c74a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
@@ -83,10 +83,27 @@
       padding-left: 5px;
     }
 
-    .revisionList {
+    .popdown {
       position: absolute;
       top: 2px;
-      right: 10px;
+      right: 0;
+    }
+    .popdown button {
+      cursor: pointer;
+      height: 25px;
+      border: none;
+      border-left: 1px solid #fff;
+      background-color: trimColor;
+      margin-right: 0;
+      padding-left: 0;
+      padding-right: 0;
+    }
+    .popdown button div {
+      padding-left: 6px;
+      padding-right: 6px;
+    }
+    .popdown button div:after {
+      content: " \25bc";
     }
 
     .headerTable {
@@ -201,7 +218,7 @@
   </ui:style>
 
   <g:HTMLPanel>
-    <div class='{style.headerLine}'>
+    <g:HTMLPanel styleName='{style.headerLine}' ui:field='headerLine'>
       <div class='{style.idBlock}'>
         <c:StarIcon ui:field='star' styleName='{style.star}'/>
         <div class='{style.idLine}'>
@@ -235,10 +252,16 @@
           <div><ui:msg>Edit Message</ui:msg></div>
         </g:Button>
       </div>
-      <div class='{style.revisionList}' ui:field='revisionParent'>
-        <ui:msg>Revision <g:ListBox ui:field='revisionList'/></ui:msg>
+
+      <div class='{style.popdown}'>
+        <div ui:field='revisionParent' style='display: inline-block;'>
+          <ui:msg>Revision <g:ListBox ui:field='revisionList'/></ui:msg>
+        </div>
+        <g:Button ui:field='download' styleName=''>
+          <div><ui:msg>Download</ui:msg></div>
+        </g:Button>
       </div>
-    </div>
+    </g:HTMLPanel>
 
     <table class='{style.headerTable}'>
       <tr>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
new file mode 100644
index 0000000..91ff99c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2013 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.change;
+
+import com.google.gerrit.client.changes.ChangeInfo.FetchInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+
+class DownloadAction extends RightSidePopdownAction {
+  private final DownloadBox downloadBox;
+
+  DownloadAction(
+      Change.Id changeId,
+      String project,
+      RevisionInfo revision,
+      ChangeScreen2.Style style,
+      UIObject relativeTo,
+      Widget downloadButton) {
+    super(style, relativeTo, downloadButton);
+    this.downloadBox = new DownloadBox(
+        revision.has_fetch()
+            ? revision.fetch()
+            : NativeMap.<FetchInfo> create(),
+        revision.name(),
+        project,
+        new PatchSet.Id(changeId, revision._number()));
+  }
+
+  Widget getWidget() {
+    return downloadBox;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
new file mode 100644
index 0000000..9ee6e3e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
@@ -0,0 +1,241 @@
+// Copyright (C) 2013 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.change;
+
+import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme.REPO_DOWNLOAD;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountApi;
+import com.google.gerrit.client.changes.ChangeInfo.FetchInfo;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.dom.client.AnchorElement;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+
+class DownloadBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, DownloadBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private final NativeMap<FetchInfo> fetch;
+  private final String revision;
+  private final String project;
+  private final PatchSet.Id psId;
+
+  @UiField ListBox scheme;
+  @UiField CopyableLabel checkout;
+  @UiField CopyableLabel cherryPick;
+  @UiField CopyableLabel pull;
+  @UiField AnchorElement patchBase64;
+  @UiField AnchorElement patchZip;
+  @UiField Element repoSection;
+  @UiField CopyableLabel repoDownload;
+
+  DownloadBox(NativeMap<FetchInfo> fetch, String revision,
+      String project, PatchSet.Id psId) {
+    this.fetch = fetch;
+    this.revision = revision;
+    this.project = project;
+    this.psId = psId;
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  @Override
+  protected void onLoad() {
+    if (scheme.getItemCount() == 0) {
+      renderScheme(fetch);
+    }
+  }
+
+  @UiHandler("scheme")
+  void onScheme(ChangeEvent event) {
+    renderCommands();
+
+    if (Gerrit.isSignedIn()) {
+      saveScheme();
+    }
+  }
+
+  private void renderCommands() {
+    FetchInfo info = fetch.get(scheme.getValue(scheme.getSelectedIndex()));
+    checkout(info);
+    cherryPick(info);
+    pull(info);
+    patch(info);
+    repo(info);
+  }
+
+  private void checkout(FetchInfo info) {
+    checkout.setText(
+        "git fetch " + info.url() + " " + info.ref()
+        + " && git checkout FETCH_HEAD");
+  }
+
+  private void cherryPick(FetchInfo info) {
+    cherryPick.setText(
+        "git fetch " + info.url() + " " + info.ref()
+        + " && git cherry-pick FETCH_HEAD");
+  }
+
+  private void pull(FetchInfo info) {
+    pull.setText("git pull " + info.url() + " " + info.ref());
+  }
+
+  private void patch(FetchInfo info) {
+    String id = revision.substring(0, 7);
+    patchBase64.setInnerText(id + ".diff.base64");
+    patchBase64.setHref(new RestApi("/changes/")
+      .id(psId.getParentKey().get())
+      .view("revisions")
+      .id(revision)
+      .view("patch")
+      .addParameterTrue("download")
+      .url());
+
+    patchZip.setInnerText(id + ".diff.zip");
+    patchZip.setHref(new RestApi("/changes/")
+      .id(psId.getParentKey().get())
+      .view("revisions")
+      .id(revision)
+      .view("patch")
+      .addParameterTrue("zip")
+      .url());
+  }
+
+  private void repo(FetchInfo info) {
+    if (Gerrit.getConfig().getDownloadSchemes().contains(REPO_DOWNLOAD)) {
+      UIObject.setVisible(repoSection, true);
+      repoDownload.setText("repo download "
+          + project
+          + " " + psId.getParentKey().get() + "/" + psId.get());
+    }
+  }
+
+  private void renderScheme(NativeMap<FetchInfo> fetch) {
+    for (String id : fetch.keySet()) {
+      FetchInfo info = fetch.get(id);
+      String u = info.url();
+      int css = u.indexOf("://");
+      if (css > 0) {
+        int s = u.indexOf('/', css + 3);
+        if (s > 0) {
+          u = u.substring(0, s + 1);
+        }
+      }
+      scheme.addItem(u, id);
+    }
+
+    int select = 0;
+    String find = getUserPreference();
+    if (find != null) {
+      for (int i = 0; i < scheme.getItemCount(); i++) {
+        if (find.equals(scheme.getValue(i))) {
+          select = i;
+          break;
+        }
+      }
+    }
+
+    scheme.setSelectedIndex(select);
+    renderCommands();
+  }
+
+  private static String getUserPreference() {
+    if (Gerrit.isSignedIn()) {
+      DownloadScheme pref =
+          Gerrit.getUserAccount().getGeneralPreferences().getDownloadUrl();
+      if (pref != null) {
+        switch (pref) {
+          case ANON_GIT:
+            return "git";
+          case HTTP:
+          case ANON_HTTP:
+            return "http";
+          case SSH:
+          case ANON_SSH:
+            return "ssh";
+          default:
+            return null;
+        }
+      }
+    }
+    return null;
+  }
+
+  private void saveScheme() {
+    DownloadScheme scheme = getSelectedScheme();
+    AccountGeneralPreferences pref =
+        Gerrit.getUserAccount().getGeneralPreferences();
+
+    if (scheme != null && scheme != pref.getDownloadUrl()) {
+      pref.setDownloadUrl(scheme);
+      PreferenceInput in = PreferenceInput.create();
+      in.download_scheme(scheme);
+      AccountApi.self().view("preferences")
+          .post(in, new AsyncCallback<JavaScriptObject>() {
+            @Override
+            public void onSuccess(JavaScriptObject result) {
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+          });
+    }
+  }
+
+  private DownloadScheme getSelectedScheme() {
+    String id = scheme.getValue(scheme.getSelectedIndex());
+    if ("git".equals(id)) {
+      return DownloadScheme.ANON_GIT;
+    } else if ("http".equals(id)) {
+      return DownloadScheme.HTTP;
+    } else if ("ssh".equals(id)) {
+      return DownloadScheme.SSH;
+    }
+    return null;
+  }
+
+  private static class PreferenceInput extends JavaScriptObject {
+    static PreferenceInput create() {
+      return createObject().cast();
+    }
+
+    final void download_scheme(DownloadScheme s) {
+      download_scheme0(s.name());
+    }
+
+    private final native void download_scheme0(String n) /*-{
+      this.download_scheme = n;
+    }-*/;
+
+    protected PreferenceInput() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.ui.xml
new file mode 100644
index 0000000..4490982
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.ui.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:c='urn:import:com.google.gwtexpui.clippy.client'>
+  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+  <ui:style>
+    @external .gwt-TextBox;
+
+    .downloadBox {
+      min-width: 580px;
+      margin: 5px;
+    }
+
+    .table {
+      border-spacing: 0;
+      width: 100%;
+    }
+    .table th {
+      text-align: left;
+      font-weight: normal;
+      white-space: nowrap;
+      max-height: 18px;
+      width: 80px;
+      padding-right: 5px;
+    }
+
+    .scheme {
+      float: right;
+    }
+
+    .clippy {
+      font-size: smaller;
+      font-family: monospace;
+    }
+    .clippy span {
+      width: 500px;
+      white-space: nowrap;
+      display: inline-block;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+    .clippy .gwt-TextBox {
+      padding: 0;
+      margin: 0;
+      border: 0;
+      max-height: 18px;
+      width: 500px;
+    }
+    .clippy div {
+      float: right;
+    }
+  </ui:style>
+  <g:HTMLPanel styleName='{style.downloadBox}'>
+    <table class='{style.table}'>
+      <tr>
+        <th><ui:msg>Checkout</ui:msg></th>
+        <td><c:CopyableLabel ui:field='checkout' styleName='{style.clippy}'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Cherry Pick</ui:msg></th>
+        <td><c:CopyableLabel ui:field='cherryPick' styleName='{style.clippy}'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Pull</ui:msg></th>
+        <td><c:CopyableLabel ui:field='pull' styleName='{style.clippy}'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Patch File</ui:msg></th>
+        <td><a ui:field='patchZip'/> | <a ui:field='patchBase64'/></td>
+      </tr>
+      <tr ui:field='repoSection' style='display: NONE' aria-hidden='true'>
+        <th><ui:msg>repo</ui:msg></th>
+        <td><c:CopyableLabel ui:field='repoDownload' styleName='{style.clippy}'/></td>
+      </tr>
+      <tr>
+        <td colspan='2'>
+          <g:ListBox ui:field='scheme' styleName='{style.scheme}'/>
+        </td>
+      </tr>
+    </table>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java
new file mode 100644
index 0000000..f73f418
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2013 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.change;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+abstract class RightSidePopdownAction {
+  private final ChangeScreen2.Style style;
+  private final Widget button;
+  private final UIObject relativeTo;
+  private PopupPanel popup;
+
+  RightSidePopdownAction(
+      ChangeScreen2.Style style,
+      UIObject relativeTo,
+      Widget button) {
+    this.style = style;
+    this.relativeTo = relativeTo;
+    this.button = button;
+  }
+
+  abstract Widget getWidget();
+
+  void show() {
+    if (popup != null) {
+      popup.hide();
+      return;
+    }
+
+    final PluginSafePopupPanel p = new PluginSafePopupPanel(true) {
+      @Override
+      public void setPopupPosition(int left, int top) {
+        top -= Document.get().getBodyOffsetTop();
+
+        int w = Window.getScrollLeft() + Window.getClientWidth();
+        int r = relativeTo.getAbsoluteLeft() + relativeTo.getOffsetWidth();
+        int right = w - r;
+        Style style = getElement().getStyle();
+        style.clearProperty("left");
+        style.setPropertyPx("right", right);
+        style.setPropertyPx("top", top);
+      }
+    };
+    p.setStyleName(style.replyBox());
+    p.addAutoHidePartner(button.getElement());
+    p.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        if (popup == p) {
+          popup = null;
+        }
+      }
+    });
+    p.add(getWidget());
+    p.showRelativeTo(relativeTo);
+    GlobalKey.dialog(p);
+    popup = p;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
index c0c5312..5b57b2b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -211,6 +211,9 @@
     public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
     public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
 
+    public final native boolean has_fetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
+    public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
+
     public static void sortRevisionInfoByNumber(JsArray<RevisionInfo> list) {
       Collections.sort(Natives.asList(list), new Comparator<RevisionInfo>() {
         @Override
@@ -224,6 +227,14 @@
     }
   }
 
+  public static class FetchInfo extends JavaScriptObject {
+    public final native String url() /*-{ return this.url }-*/;
+    public final native String ref() /*-{ return this.ref }-*/;
+
+    protected FetchInfo () {
+    }
+  }
+
   public static class CommitInfo extends JavaScriptObject {
     public final native String commit() /*-{ return this.commit; }-*/;
     public final native JsArray<CommitInfo> parents() /*-{ return this.parents; }-*/;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
new file mode 100644
index 0000000..733c49d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2013 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.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class GetPreferences implements RestReadView<AccountResource> {
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  GetPreferences(Provider<CurrentUser> self) {
+    this.self = self;
+  }
+
+  @Override
+  public PreferenceInfo apply(AccountResource rsrc) throws AuthException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("restricted to administrator");
+    }
+    return new PreferenceInfo(rsrc.getUser().getAccount()
+        .getGeneralPreferences());
+  }
+
+  static class PreferenceInfo {
+    final String kind = "gerritcodereview#preferences";
+
+    short changesPerPage;
+    Boolean showSiteHeader;
+    Boolean useFlashClipboard;
+    DownloadScheme downloadScheme;
+    DownloadCommand downloadCommand;
+    Boolean copySelfOnEmail;
+    DateFormat dateFormat;
+    TimeFormat timeFormat;
+    Boolean reversePatchSetOrder;
+    Boolean showUsernameInReviewCategory;
+    Boolean relativeDateInChangeTable;
+    CommentVisibilityStrategy commentVisibilityStrategy;
+    DiffView diffView;
+
+    PreferenceInfo(AccountGeneralPreferences p) {
+      changesPerPage = p.getMaximumPageSize();
+      showSiteHeader = p.isShowSiteHeader() ? true : null;
+      useFlashClipboard = p.isUseFlashClipboard() ? true : null;
+      downloadScheme = p.getDownloadUrl();
+      downloadCommand = p.getDownloadCommand();
+      copySelfOnEmail = p.isCopySelfOnEmails() ? true : null;
+      dateFormat = p.getDateFormat();
+      timeFormat = p.getTimeFormat();
+      reversePatchSetOrder = p.isReversePatchSetOrder() ? true : null;
+      showUsernameInReviewCategory = p.isShowUsernameInReviewCategory() ? true : null;
+      relativeDateInChangeTable = p.isRelativeDateInChangeTable() ? true : null;
+      commentVisibilityStrategy = p.getCommentVisibilityStrategy();
+      diffView = p.getDiffView();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
index fe3086a..79a0089 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -59,6 +59,8 @@
     get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class);
     child(ACCOUNT_KIND, "capabilities").to(Capabilities.class);
     get(ACCOUNT_KIND, "groups").to(GetGroups.class);
+    get(ACCOUNT_KIND, "preferences").to(GetPreferences.class);
+    post(ACCOUNT_KIND, "preferences").to(SetPreferences.class);
     get(ACCOUNT_KIND, "preferences.diff").to(GetDiffPreferences.class);
     put(ACCOUNT_KIND, "preferences.diff").to(SetDiffPreferences.class);
     get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
new file mode 100644
index 0000000..8e40b2e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -0,0 +1,139 @@
+// Copyright (C) 2013 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.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.SetPreferences.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+public class SetPreferences implements RestModifyView<AccountResource, Input> {
+  static class Input {
+    Short changesPerPage;
+    Boolean showSiteHeader;
+    Boolean useFlashClipboard;
+    DownloadScheme downloadScheme;
+    DownloadCommand downloadCommand;
+    Boolean copySelfOnEmail;
+    DateFormat dateFormat;
+    TimeFormat timeFormat;
+    Boolean reversePatchSetOrder;
+    Boolean showUsernameInReviewCategory;
+    Boolean relativeDateInChangeTable;
+    CommentVisibilityStrategy commentVisibilityStrategy;
+    DiffView diffView;
+  }
+
+  private final Provider<CurrentUser> self;
+  private final AccountCache cache;
+  private final ReviewDb db;
+
+  @Inject
+  SetPreferences(Provider<CurrentUser> self, AccountCache cache, ReviewDb db) {
+    this.self = self;
+    this.cache = cache;
+    this.db = db;
+  }
+
+  @Override
+  public GetPreferences.PreferenceInfo apply(AccountResource rsrc, Input i)
+      throws AuthException, ResourceNotFoundException, OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("restricted to administrator");
+    }
+    if (i == null) {
+      i = new Input();
+    }
+
+    Account.Id accountId = rsrc.getUser().getAccountId();
+    AccountGeneralPreferences p;
+    db.accounts().beginTransaction(accountId);
+    try {
+      Account a = db.accounts().get(accountId);
+      if (a == null) {
+        throw new ResourceNotFoundException();
+      }
+
+      p = a.getGeneralPreferences();
+      if (p == null) {
+        p = new AccountGeneralPreferences();
+        a.setGeneralPreferences(p);
+      }
+
+      if (i.changesPerPage != null) {
+        p.setMaximumPageSize(i.changesPerPage);
+      }
+      if (i.showSiteHeader != null) {
+        p.setShowSiteHeader(i.showSiteHeader);
+      }
+      if (i.useFlashClipboard != null) {
+        p.setUseFlashClipboard(i.useFlashClipboard);
+      }
+      if (i.downloadScheme != null) {
+        p.setDownloadUrl(i.downloadScheme);
+      }
+      if (i.downloadCommand != null) {
+        p.setDownloadCommand(i.downloadCommand);
+      }
+      if (i.copySelfOnEmail != null) {
+        p.setCopySelfOnEmails(i.copySelfOnEmail);
+      }
+      if (i.dateFormat != null) {
+        p.setDateFormat(i.dateFormat);
+      }
+      if (i.timeFormat != null) {
+        p.setTimeFormat(i.timeFormat);
+      }
+      if (i.reversePatchSetOrder != null) {
+        p.setReversePatchSetOrder(i.reversePatchSetOrder);
+      }
+      if (i.showUsernameInReviewCategory != null) {
+        p.setShowUsernameInReviewCategory(i.showUsernameInReviewCategory);
+      }
+      if (i.relativeDateInChangeTable != null) {
+        p.setRelativeDateInChangeTable(i.relativeDateInChangeTable);
+      }
+      if (i.commentVisibilityStrategy != null) {
+        p.setCommentVisibilityStrategy(i.commentVisibilityStrategy);
+      }
+      if (i.diffView != null) {
+        p.setDiffView(i.diffView);
+      }
+
+      db.accounts().update(Collections.singleton(a));
+      db.commit();
+      cache.evict(accountId);
+    } finally {
+      db.rollback();
+    }
+    return new GetPreferences.PreferenceInfo(p);
+  }
+}