Merge "Merge doc and change searches."
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 8c488e4..af0daf4 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -897,7 +897,7 @@
 Default is "Submit all ${topicSize} changes of the same topic (${submitSize}
 changes including ancestors and other changes related by topic)".
 
-[[change.submitWholeTopic]]change.submitWholeTopic::
+[[change.submitWholeTopic]]change.submitWholeTopic (*Experimental*)::
 +
 Determines if the submit button submits the whole topic instead of
 just the current change.
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index e6bf2a4..13b6978 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -283,6 +283,7 @@
     private final ConsoleUI ui;
     private final AllProjectsConfig allProjectsConfig;
 
+    @Inject
     public MyInitStep(@PluginName String pluginName, ConsoleUI ui,
         AllProjectsConfig allProjectsConfig) {
       this.pluginName = pluginName;
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 4679158..07d3cc3 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -224,7 +224,7 @@
 * `ALL_REVISIONS`: describe all revisions, not just current.
 --
 
-[[download_commands]]
+[[download-commands]]
 --
 * `DOWNLOAD_COMMANDS`: include the `commands` field in the
   link:#fetch-info[FetchInfo] for revisions. Only valid when the
@@ -1104,7 +1104,7 @@
   blocked by Verified
 ----
 
-[[submitted_together]]
+[[submitted-together]]
 === Changes submitted together
 --
 'GET /changes/link:#change-id[\{change-id\}]/submitted_together'
@@ -2264,6 +2264,55 @@
   HTTP/1.1 204 No Content
 ----
 
+[[list-votes]]
+=== List Votes
+--
+'GET /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/'
+--
+
+Lists the votes for a specific reviewer of the change.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/ HTTP/1.0
+----
+
+As result a map is returned that maps the label name to the label value.
+The entries in the map are sorted by label name.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "Code-Review": -1,
+    "Verified": 1
+    "Work-In-Progress": 1,
+  }
+----
+
+[[delete-vote]]
+=== Delete Vote
+--
+'DELETE /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/link:#label-id[\{label-id\}]'
+--
+
+Deletes a single vote from a change. Note, that even when the last vote of
+a reviewer is removed the reviewer itself is still listed on the change.
+
+.Request
+----
+  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/votes/Code-Review HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
 [[revision-endpoints]]
 == Revision Endpoints
 
@@ -3747,6 +3796,10 @@
 === \{draft-id\}
 UUID of a draft comment.
 
+[[label-id]]
+=== \{label-id\}
+The name of the label.
+
 [[file-id]]
 \{file-id\}
 ~~~~~~~~~~~~
@@ -4254,7 +4307,7 @@
 |`commands`    |optional|
 The download commands for this patch set as a map that maps the command
 names to the commands. +
-Only set if link:#download_commands[download commands] are requested.
+Only set if link:#download-commands[download commands] are requested.
 |==========================
 
 [[file-info]]
diff --git a/ReleaseNotes/ReleaseNotes-2.12.txt b/ReleaseNotes/ReleaseNotes-2.12.txt
index 4bfe90d..1dfba72 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.txt
@@ -35,7 +35,7 @@
 This release includes the following new features. See the sections below for
 further details.
 
-* New "Submit Whole Topic" / "Submitted Together" workflow.
+* New change submission workflows, "Submit Whole Topic" and "Submitted Together".
 
 * Support for GPG Keys and signed pushes.
 
@@ -43,8 +43,8 @@
 New Features
 ------------
 
-New Change Submission Workflow
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+New Change Submission Workflows
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 * New "Submit Whole Topic" setting.
 +
@@ -53,7 +53,7 @@
 `change.submitWholeTopic`] setting is enabled, all changes belonging to the same
 topic will be submitted at the same time.
 +
-This setting is disabled by default.
+This setting should be considered experimental, and is disabled by default.
 
 * Submission of changes may include ancestors.
 +
@@ -297,7 +297,7 @@
 
 * Upgrade Jetty to 9.2.13.v20150730
 
-* Upgrade JGit to 4.1.0.201509280440-r
+* Upgrade JGit to 4.1.1.201511131810-r
 
 * Upgrade joda-time to 2.8
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 1598774..9788a5f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -26,6 +26,8 @@
 import static com.google.gerrit.server.project.Util.value;
 import static com.google.gerrit.testutil.GerritServerTests.isNoteDbTestEnabled;
 
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -46,7 +48,9 @@
 import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -63,6 +67,7 @@
 import java.util.EnumSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 
 @NoHttpd
 public class ChangeIT extends AbstractDaemonTest {
@@ -379,6 +384,115 @@
   }
 
   @Test
+  public void listVotes() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+
+    Map<String, Short> m = gApi.changes()
+        .id(r.getChangeId())
+        .reviewer(admin.getId().toString())
+        .votes();
+
+    assertThat(m).hasSize(1);
+    assertThat(m).containsEntry("Code-Review", new Short((short)2));
+
+    setApiUser(user);
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.dislike());
+
+    m = gApi.changes()
+        .id(r.getChangeId())
+        .reviewer(user.getId().toString())
+        .votes();
+
+    assertThat(m).hasSize(1);
+    assertThat(m).containsEntry("Code-Review", new Short((short)-1));
+  }
+
+  @Test
+  public void deleteVote() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+
+    setApiUser(user);
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.recommend());
+
+    setApiUser(admin);
+    gApi.changes()
+        .id(r.getChangeId())
+        .reviewer(admin.getId().toString())
+        .deleteVote("Code-Review");
+
+    Map<String, Short> m = gApi.changes()
+        .id(r.getChangeId())
+        .reviewer(admin.getId().toString())
+        .votes();
+
+    if (isNoteDbTestEnabled()) {
+      // When notedb is enabled each reviewer is explicitly recorded in the
+      // notedb and this record stays even when all votes of that user have been
+      // deleted, hence there is no dummy 0 approval left when a vote is
+      // deleted.
+      assertThat(m).isEmpty();
+    } else {
+      // When notedb is disabled there is a dummy 0 approval on the change so
+      // that the user is still returned as CC when all votes of that user have
+      // been deleted.
+      assertThat(m).containsEntry("Code-Review", new Short((short)0));
+    }
+
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+
+    assertThat(Iterables.getLast(c.messages).message).isEqualTo(
+        "Removed Code-Review+2 by Administrator <admin@example.com>\n");
+    if (isNoteDbTestEnabled()) {
+      // When notedb is enabled each reviewer is explicitly recorded in the
+      // notedb and this record stays even when all votes of that user have been
+      // deleted.
+      assertThat(getReviewers(c.reviewers.get(REVIEWER)))
+          .containsExactlyElementsIn(
+              ImmutableSet.of(admin.getId(), user.getId()));
+    } else {
+      // When notedb is disabled users that have only dummy 0 approvals on the
+      // change are returned as CC and not as REVIEWER.
+      assertThat(getReviewers(c.reviewers.get(REVIEWER)))
+          .containsExactlyElementsIn(ImmutableSet.of(user.getId()));
+      assertThat(getReviewers(c.reviewers.get(CC)))
+          .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
+    }
+  }
+
+  @Test
+  public void deleteVoteNotPermitted() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage("delete not permitted");
+    gApi.changes()
+        .id(r.getChangeId())
+        .reviewer(admin.getId().toString())
+        .deleteVote("Code-Review");
+  }
+
+  @Test
   public void createEmptyChange() throws Exception {
     ChangeInfo in = new ChangeInfo();
     in.branch = Constants.MASTER;
@@ -677,4 +791,15 @@
     assertThat(rev2.pushCertificate.certificate).isNull();
     assertThat(rev2.pushCertificate.key).isNull();
   }
+
+
+  private static Iterable<Account.Id> getReviewers(
+      Collection<AccountInfo> r) {
+    return Iterables.transform(r, new Function<AccountInfo, Account.Id>() {
+      @Override
+      public Account.Id apply(AccountInfo account) {
+        return new Account.Id(account._accountId);
+      }
+    });
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index ce07098..d927231 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -58,6 +58,19 @@
    */
   RevisionApi revision(String id) throws RestApiException;
 
+  /**
+   * Look up the reviewer of the change.
+   * <p>
+   * @param id ID of the account, can be a string of the format
+   *     "Full Name <mail@example.com>", just the email address, a full name
+   *     if it is unique, an account ID, a user name or 'self' for the
+   *     calling user.
+   * @return API for accessing the reviewer.
+   * @throws RestApiException if id is not account ID or is a user that isn't
+   *     known to be a reviewer for this change.
+   */
+  ReviewerApi reviewer(String id) throws RestApiException;
+
   void abandon() throws RestApiException;
   void abandon(AbandonInput in) throws RestApiException;
 
@@ -177,6 +190,11 @@
     }
 
     @Override
+    public ReviewerApi reviewer(String id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public RevisionApi revision(String id) throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
new file mode 100644
index 0000000..11b670d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2014 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.extensions.api.changes;
+
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+import java.util.Map;
+
+public interface ReviewerApi {
+
+  Map<String, Short> votes() throws RestApiException;
+  void deleteVote(String label) throws RestApiException;
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index cace7ad..b1dbcd6 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -31,6 +31,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.HashSet;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
@@ -84,6 +85,16 @@
     return allLabels().keySet();
   }
 
+  public final Set<Integer> removableReviewerIds() {
+    Set<Integer> removable = new HashSet<>();
+    if (removableReviewers() != null) {
+      for (AccountInfo a : Natives.asList(removableReviewers())) {
+        removable.add(a._accountId());
+      }
+    }
+    return removable;
+  }
+
   public final native String id() /*-{ return this.id; }-*/;
   public final native String project() /*-{ return this.project; }-*/;
   public final native String branch() /*-{ return this.branch; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
index 4139348..bba63c9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
@@ -48,20 +48,26 @@
 /** Displays a table of label and reviewer scores. */
 class Labels extends Grid {
   private static final String DATA_ID = "data-id";
-  private static final String REMOVE;
+  private static final String DATA_VOTE = "data-vote";
+  private static final String REMOVE_REVIEWER;
+  private static final String REMOVE_VOTE;
 
   static {
-    REMOVE = DOM.createUniqueId().replace('-', '_');
-    init(REMOVE);
+    REMOVE_REVIEWER = DOM.createUniqueId().replace('-', '_');
+    REMOVE_VOTE = DOM.createUniqueId().replace('-', '_');
+    init(REMOVE_REVIEWER, REMOVE_VOTE);
   }
 
-  private static final native void init(String r) /*-{
+  private static final native void init(String r, String v) /*-{
     $wnd[r] = $entry(function(e) {
-      @com.google.gerrit.client.change.Labels::onRemove(Lcom/google/gwt/dom/client/NativeEvent;)(e)
+      @com.google.gerrit.client.change.Labels::onRemoveReviewer(Lcom/google/gwt/dom/client/NativeEvent;)(e)
+    });
+    $wnd[v] = $entry(function(e) {
+      @com.google.gerrit.client.change.Labels::onRemoveVote(Lcom/google/gwt/dom/client/NativeEvent;)(e)
     });
   }-*/;
 
-  private static void onRemove(NativeEvent event) {
+  private static void onRemoveReviewer(NativeEvent event) {
     Integer user = getDataId(event);
     if (user != null) {
       final ChangeScreen screen = ChangeScreen.get(event);
@@ -77,6 +83,23 @@
     }
   }
 
+  private static void onRemoveVote(NativeEvent event) {
+    Integer user = getDataId(event);
+    String vote = getVoteId(event);
+    if (user != null && vote != null) {
+      final ChangeScreen screen = ChangeScreen.get(event);
+      ChangeApi.vote(screen.getChangeId().get(), user, vote).delete(
+          new GerritCallback<JavaScriptObject>() {
+            @Override
+            public void onSuccess(JavaScriptObject result) {
+              if (screen.isCurrentView()) {
+                Gerrit.display(PageLinks.toChange(screen.getChangeId()));
+              }
+            }
+          });
+    }
+  }
+
   private static Integer getDataId(NativeEvent event) {
     Element e = event.getEventTarget().cast();
     while (e != null) {
@@ -89,6 +112,18 @@
     return null;
   }
 
+  private static String getVoteId(NativeEvent event) {
+    Element e = event.getEventTarget().cast();
+    while (e != null) {
+      String v = e.getAttribute(DATA_VOTE);
+      if (!v.isEmpty()) {
+        return v;
+      }
+      e = e.getParentElement();
+    }
+    return null;
+  }
+
   private ChangeScreen.Style style;
 
   void init(ChangeScreen.Style style) {
@@ -97,6 +132,7 @@
 
   void set(ChangeInfo info) {
     List<String> names = new ArrayList<>(info.labels());
+    Set<Integer> removable = info.removableReviewerIds();
     Collections.sort(names);
 
     resize(names.size(), 2);
@@ -106,14 +142,14 @@
       LabelInfo label = info.label(name);
       setText(row, 0, name);
       if (label.all() != null) {
-        setWidget(row, 1, renderUsers(label));
+        setWidget(row, 1, renderUsers(label, removable));
       }
       getCellFormatter().setStyleName(row, 0, style.labelName());
       getCellFormatter().addStyleName(row, 0, getStyleForLabel(label));
     }
   }
 
-  private Widget renderUsers(LabelInfo label) {
+  private Widget renderUsers(LabelInfo label, Set<Integer> removable) {
     Map<Integer, List<ApprovalInfo>> m = new HashMap<>(4);
     int approved = 0;
     int rejected = 0;
@@ -150,8 +186,8 @@
         html.setStyleName(style.label_reject());
       }
       html.append(val).append(" ");
-      html.append(formatUserList(style, m.get(v),
-          Collections.<Integer> emptySet(), null));
+      html.append(formatUserList(style, m.get(v), removable,
+          label.name(), null));
       html.closeSpan();
     }
     return html.toBlockWidget();
@@ -198,6 +234,7 @@
   static SafeHtml formatUserList(ChangeScreen.Style style,
       Collection<? extends AccountInfo> in,
       Set<Integer> removable,
+      String label,
       Map<Integer, VotableInfo> votable) {
     List<AccountInfo> users = new ArrayList<>(in);
     Collections.sort(users, new Comparator<AccountInfo>() {
@@ -257,6 +294,9 @@
           .setAttribute(DATA_ID, ai._accountId())
           .setAttribute("title", getTitle(ai, votableCategories))
           .setStyleName(style.label_user());
+      if (label != null) {
+        html.setAttribute(DATA_VOTE, label);
+      }
       if (img != null) {
         html.openElement("img")
             .setStyleName(style.avatar())
@@ -271,10 +311,15 @@
       }
       html.append(name);
       if (removable.contains(ai._accountId())) {
-        html.openElement("button")
-            .setAttribute("title", Util.M.removeReviewer(name))
-            .setAttribute("onclick", REMOVE + "(event)")
-            .append("×")
+        html.openElement("button");
+        if (label != null) {
+          html.setAttribute("title", Util.M.removeVote(label))
+              .setAttribute("onclick", REMOVE_VOTE + "(event)");
+        } else {
+          html.setAttribute("title", Util.M.removeReviewer(name))
+              .setAttribute("onclick", REMOVE_REVIEWER + "(event)");
+        }
+        html.append("×")
             .closeElement("button");
       }
       html.closeSpan();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index 7d2415c..ae648dc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -51,7 +51,6 @@
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -214,20 +213,13 @@
       cc.remove(i);
     }
     cc.remove(info.owner()._accountId());
-
-    Set<Integer> removable = new HashSet<>();
-    if (info.removableReviewers() != null) {
-      for (AccountInfo a : Natives.asList(info.removableReviewers())) {
-        removable.add(a._accountId());
-      }
-    }
-
+    Set<Integer> removable = info.removableReviewerIds();
     Map<Integer, VotableInfo> votable = votable(info);
 
     SafeHtml rHtml = Labels.formatUserList(style,
-        r.values(), removable, votable);
+        r.values(), removable, null, votable);
     SafeHtml ccHtml = Labels.formatUserList(style,
-        cc.values(), removable, votable);
+        cc.values(), removable, null, votable);
 
     reviewersText.setInnerSafeHtml(rHtml);
     ccText.setInnerSafeHtml(ccHtml);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index 9cafeec..8314e3e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -155,6 +155,10 @@
         .addParameter("n", n);
   }
 
+  public static RestApi vote(int id, int reviewer, String vote) {
+    return reviewer(id, reviewer).view("votes").id(vote);
+  }
+
   public static RestApi reviewer(int id, int reviewer) {
     return change(id).view("reviewers").id(reviewer);
   }
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 95f2824..eb09657 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
@@ -43,6 +43,7 @@
 
   String removeHashtag(String name);
   String removeReviewer(String fullName);
+  String removeVote(String label);
   String messageWrittenOn(String date);
 
   String renamedFrom(String sourcePath);
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 9e58c9f..b3c3980 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
@@ -24,6 +24,7 @@
 
 removeHashtag = Remove hashtag {0}
 removeReviewer = Remove reviewer {0}
+removeVote = Remove vote {0}
 messageWrittenOn = on {0}
 
 renamedFrom = renamed from {0}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 2036f83..c6eece2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -136,28 +136,25 @@
     }
 
     Resource r = cache.getIfPresent(p);
-    if (r == null && maybeStream(p, req, rsp)) {
+    try {
+      if (r == null) {
+        if (maybeStream(p, req, rsp)) {
+          return; // Bypass cache for large resource.
+        }
+        r = cache.get(p, newLoader(p));
+      }
+      if (refresh && r.isStale(p, this)) {
+        cache.invalidate(p);
+        r = cache.get(p, newLoader(p));
+      }
+    } catch (ExecutionException e) {
+      log.warn("Cannot load static resource " + req.getPathInfo(), e);
+      CacheHeaders.setNotCacheable(rsp);
+      rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
       return;
     }
-
-    if (r == null) {
-      Callable<Resource> loader = newLoader(p);
-      try {
-        r = cache.get(p, loader);
-        if (refresh && r.isStale(p, this)) {
-          cache.invalidate(p);
-          r = cache.get(p, loader);
-        }
-      } catch (ExecutionException | IOException e) {
-        log.warn("Cannot load static resource " + req.getPathInfo(), e);
-        CacheHeaders.setNotCacheable(rsp);
-        rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
-        return;
-      }
-    }
-
     if (r == Resource.NOT_FOUND) {
-      notFound(rsp);
+      notFound(rsp); // Cached not found response.
       return;
     }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
index 8db3125..dfe0403 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
-import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.inject.Injector;
 import com.google.inject.Key;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index b7617a2..d7f8cde 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.api.changes.RevertInput;
+import com.google.gerrit.extensions.api.changes.ReviewerApi;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -46,6 +47,7 @@
 import com.google.gerrit.server.change.PutTopic;
 import com.google.gerrit.server.change.Restore;
 import com.google.gerrit.server.change.Revert;
+import com.google.gerrit.server.change.Reviewers;
 import com.google.gerrit.server.change.Revisions;
 import com.google.gerrit.server.change.SubmittedTogether;
 import com.google.gerrit.server.change.SuggestReviewers;
@@ -68,7 +70,9 @@
 
   private final Provider<CurrentUser> user;
   private final Changes changeApi;
+  private final Reviewers reviewers;
   private final Revisions revisions;
+  private final ReviewerApiImpl.Factory reviewerApi;
   private final RevisionApiImpl.Factory revisionApi;
   private final Provider<SuggestReviewers> suggestReviewers;
   private final ChangeResource change;
@@ -90,7 +94,9 @@
   @Inject
   ChangeApiImpl(Provider<CurrentUser> user,
       Changes changeApi,
+      Reviewers reviewers,
       Revisions revisions,
+      ReviewerApiImpl.Factory reviewerApi,
       RevisionApiImpl.Factory revisionApi,
       Provider<SuggestReviewers> suggestReviewers,
       Abandon abandon,
@@ -111,7 +117,9 @@
     this.user = user;
     this.changeApi = changeApi;
     this.revert = revert;
+    this.reviewers = reviewers;
     this.revisions = revisions;
+    this.reviewerApi = reviewerApi;
     this.revisionApi = revisionApi;
     this.suggestReviewers = suggestReviewers;
     this.abandon = abandon;
@@ -156,6 +164,16 @@
   }
 
   @Override
+  public ReviewerApi reviewer(String id) throws RestApiException {
+    try {
+      return reviewerApi.create(
+          reviewers.parse(change, IdString.fromDecoded(id)));
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot parse reviewer", e);
+    }
+  }
+
+  @Override
   public void abandon() throws RestApiException {
     abandon(new AbandonInput());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
index a5e584e..228dad6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
@@ -27,5 +27,6 @@
     factory(DraftApiImpl.Factory.class);
     factory(RevisionApiImpl.Factory.class);
     factory(FileApiImpl.Factory.class);
+    factory(ReviewerApiImpl.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
new file mode 100644
index 0000000..49b3432
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2014 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.api.changes;
+
+import com.google.gerrit.extensions.api.changes.ReviewerApi;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.change.DeleteVote;
+import com.google.gerrit.server.change.ReviewerResource;
+import com.google.gerrit.server.change.VoteResource;
+import com.google.gerrit.server.change.Votes;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.Map;
+
+public class ReviewerApiImpl implements ReviewerApi {
+  interface Factory {
+    ReviewerApiImpl create(ReviewerResource r);
+  }
+
+  private final ReviewerResource reviewer;
+  private final Votes.List listVotes;
+  private final DeleteVote deleteVote;
+
+  @Inject
+  ReviewerApiImpl(Votes.List listVotes,
+      DeleteVote deleteVote,
+      @Assisted ReviewerResource reviewer) {
+    this.listVotes = listVotes;
+    this.deleteVote = deleteVote;
+    this.reviewer = reviewer;
+  }
+
+  @Override
+  public Map<String, Short> votes() throws RestApiException {
+    try {
+      return listVotes.apply(reviewer);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot list votes", e);
+    }
+  }
+
+  @Override
+  public void deleteVote(String label) throws RestApiException {
+    try {
+      deleteVote.apply(new VoteResource(reviewer, label), null);
+    } catch (UpdateException e) {
+      throw new RestApiException("Cannot delete vote", e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
new file mode 100644
index 0000000..b3e59b9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2014 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.change;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.DeleteVote.Input;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.Collections;
+
+@Singleton
+public class DeleteVote implements RestModifyView<VoteResource, Input> {
+  public static class Input {
+  }
+
+  private final Provider<ReviewDb> db;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final ApprovalsUtil approvalsUtil;
+  private final ChangeMessagesUtil cmUtil;
+  private final IdentifiedUser.GenericFactory userFactory;
+
+  @Inject
+  DeleteVote(Provider<ReviewDb> db,
+      BatchUpdate.Factory batchUpdateFactory,
+      ApprovalsUtil approvalsUtil,
+      ChangeMessagesUtil cmUtil,
+      IdentifiedUser.GenericFactory userFactory) {
+    this.db = db;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.approvalsUtil = approvalsUtil;
+    this.cmUtil = cmUtil;
+    this.userFactory = userFactory;
+  }
+
+  @Override
+  public Response<?> apply(VoteResource rsrc, Input input)
+      throws RestApiException, UpdateException {
+    ReviewerResource r = rsrc.getReviewer();
+    ChangeControl ctl = r.getControl();
+    Change change = r.getChange();
+    try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
+          change.getProject(), ctl.getUser().asIdentifiedUser(),
+          TimeUtil.nowTs())) {
+      bu.addOp(change.getId(),
+          new Op(r.getUser().getAccountId(), rsrc.getLabel()));
+      bu.execute();
+    }
+
+    return Response.none();
+  }
+
+  private class Op extends BatchUpdate.Op {
+    private final Account.Id accountId;
+    private final String label;
+
+    private Op(Account.Id accountId, String label) {
+      this.accountId = accountId;
+      this.label = label;
+    }
+
+    @Override
+    public void updateChange(ChangeContext ctx)
+        throws OrmException, AuthException, ResourceNotFoundException {
+      IdentifiedUser user = ctx.getUser().asIdentifiedUser();
+      Change change = ctx.getChange();
+      ChangeControl ctl = ctx.getChangeControl();
+      PatchSet.Id psId = change.currentPatchSetId();
+
+      PatchSetApproval psa = null;
+      StringBuilder msg = new StringBuilder();
+      for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
+            ctx.getDb(), ctl, psId, accountId)) {
+        if (ctl.canRemoveReviewer(a)) {
+          if (a.getLabel().equals(label)) {
+            msg.append("Removed ")
+                .append(a.getLabel()).append(formatLabelValue(a.getValue()))
+                .append(" by ").append(userFactory.create(user.getAccountId())
+                    .getNameEmail())
+                .append("\n");
+            psa = a;
+            a.setValue((short)0);
+            ctx.getChangeUpdate().setPatchSetId(psId);
+            ctx.getChangeUpdate().removeApproval(label);
+            break;
+          }
+        } else {
+          throw new AuthException("delete not permitted");
+        }
+      }
+      if (psa == null) {
+        throw new ResourceNotFoundException();
+      }
+      ChangeUtil.bumpRowVersionNotLastUpdatedOn(change.getId(), ctx.getDb());
+      ctx.getDb().patchSetApprovals().update(Collections.singleton(psa));
+
+      if (msg.length() > 0) {
+        ChangeMessage changeMessage =
+            new ChangeMessage(new ChangeMessage.Key(change.getId(),
+                ChangeUtil.messageUUID(ctx.getDb())),
+                user.getAccountId(),
+                ctx.getWhen(),
+                change.currentPatchSetId());
+        changeMessage.setMessage(msg.toString());
+        cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(),
+            changeMessage);
+      }
+    }
+  }
+
+  private static String formatLabelValue(short value) {
+    if (value > 0) {
+      return "+" + value;
+    } else {
+      return Short.toString(value);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index 9c588ed..0ef8b51 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.server.change.FileResource.FILE_KIND;
 import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
 import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
+import static com.google.gerrit.server.change.VoteResource.VOTE_KIND;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
@@ -37,6 +38,7 @@
     bind(DraftComments.class);
     bind(Comments.class);
     bind(Files.class);
+    bind(Votes.class);
 
     DynamicMap.mapOf(binder(), CHANGE_KIND);
     DynamicMap.mapOf(binder(), COMMENT_KIND);
@@ -45,6 +47,7 @@
     DynamicMap.mapOf(binder(), REVIEWER_KIND);
     DynamicMap.mapOf(binder(), REVISION_KIND);
     DynamicMap.mapOf(binder(), CHANGE_EDIT_KIND);
+    DynamicMap.mapOf(binder(), VOTE_KIND);
 
     get(CHANGE_KIND).to(GetChange.class);
     get(CHANGE_KIND, "detail").to(GetDetail.class);
@@ -73,6 +76,8 @@
     child(CHANGE_KIND, "reviewers").to(Reviewers.class);
     get(REVIEWER_KIND).to(GetReviewer.class);
     delete(REVIEWER_KIND).to(DeleteReviewer.class);
+    child(REVIEWER_KIND, "votes").to(Votes.class);
+    delete(VOTE_KIND).to(DeleteVote.class);
 
     child(CHANGE_KIND, "revisions").to(Revisions.class);
     get(REVISION_KIND, "actions").to(GetRevisionActions.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/VoteResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/VoteResource.java
new file mode 100644
index 0000000..4dfaff0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/VoteResource.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2014 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.change;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class VoteResource implements RestResource {
+  public static final TypeLiteral<RestView<VoteResource>> VOTE_KIND =
+      new TypeLiteral<RestView<VoteResource>>() {};
+
+  private final ReviewerResource reviewer;
+  private final String label;
+
+  public VoteResource(ReviewerResource reviewer, String label) {
+    this.reviewer = reviewer;
+    this.label = label;
+  }
+
+  public ReviewerResource getReviewer() {
+    return reviewer;
+  }
+
+  public String getLabel() {
+    return label;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
new file mode 100644
index 0000000..a86ce7f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2014 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.change;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+@Singleton
+public class Votes implements ChildCollection<ReviewerResource, VoteResource> {
+  private final DynamicMap<RestView<VoteResource>> views;
+  private final List list;
+
+  @Inject
+  Votes(DynamicMap<RestView<VoteResource>> views,
+      List list) {
+    this.views = views;
+    this.list = list;
+  }
+
+  @Override
+  public DynamicMap<RestView<VoteResource>> views() {
+    return views;
+  }
+
+  @Override
+  public RestView<ReviewerResource> list() throws AuthException {
+    return list;
+  }
+
+  @Override
+  public VoteResource parse(ReviewerResource reviewer, IdString id)
+      throws ResourceNotFoundException, OrmException, AuthException {
+    return new VoteResource(reviewer, id.get());
+  }
+
+  @Singleton
+  public static class List implements RestReadView<ReviewerResource> {
+    private final Provider<ReviewDb> db;
+    private final ApprovalsUtil approvalsUtil;
+
+    @Inject
+    List(Provider<ReviewDb> db,
+        ApprovalsUtil approvalsUtil) {
+      this.db = db;
+      this.approvalsUtil = approvalsUtil;
+    }
+
+    @Override
+    public Map<String, Short> apply(ReviewerResource rsrc) throws OrmException {
+      Map<String, Short> votes = new TreeMap<>();
+      Iterable<PatchSetApproval> byPatchSetUser = approvalsUtil.byPatchSetUser(
+          db.get(),
+          rsrc.getControl(),
+          rsrc.getChange().currentPatchSetId(),
+          rsrc.getUser().getAccountId());
+      for (PatchSetApproval psa : byPatchSetUser) {
+        votes.put(psa.getLabel(), psa.getValue());
+      }
+      return votes;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index a2d71fb..e809e67 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -888,6 +888,7 @@
             break;
 
           case PATH_CONFLICT:
+          case REBASE_MERGE_CONFLICT:
           case MANUAL_RECURSIVE_MERGE:
           case CANNOT_CHERRY_PICK_ROOT:
           case NOT_FAST_FORWARD:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java
index 19e23b7..faf3776 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
@@ -37,6 +38,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashSet;
@@ -97,25 +99,29 @@
     public List<Change> load(Project.NameKey key) throws Exception {
       try (Repository repo = repoManager.openRepository(key);
           ManualRequestContext ctx = requestContext.open()) {
-        ReviewDb db = ctx.getReviewDbProvider().get();
-        Map<String, Ref> refs =
-            repo.getRefDatabase().getRefs(RefNames.REFS_CHANGES);
-        Set<Change.Id> ids = new LinkedHashSet<>();
-        for (Ref r : refs.values()) {
-          Change.Id id = Change.Id.fromRef(r.getName());
-          if (id != null) {
-            ids.add(id);
-          }
-        }
-        List<Change> changes = new ArrayList<>(ids.size());
-         // A batch size of N may overload get(Iterable), so use something smaller,
-         // but still >1.
-        for (List<Change.Id> batch : Iterables.partition(ids, 30)) {
-          Iterables.addAll(changes, db.changes().get(batch));
-        }
-        return changes;
+        return scan(repo, ctx.getReviewDbProvider().get());
       }
     }
 
   }
+
+  public static List<Change> scan(Repository repo, ReviewDb db)
+      throws OrmException, IOException {
+    Map<String, Ref> refs =
+        repo.getRefDatabase().getRefs(RefNames.REFS_CHANGES);
+    Set<Change.Id> ids = new LinkedHashSet<>();
+    for (Ref r : refs.values()) {
+      Change.Id id = Change.Id.fromRef(r.getName());
+      if (id != null) {
+        ids.add(id);
+      }
+    }
+    List<Change> changes = new ArrayList<>(ids.size());
+    // A batch size of N may overload get(Iterable), so use something smaller,
+    // but still >1.
+    for (List<Change.Id> batch : Iterables.partition(ids, 30)) {
+      Iterables.addAll(changes, db.changes().get(batch));
+    }
+    return changes;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
index 31fbb40..9d573e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
@@ -69,6 +69,11 @@
 
   @Override
   public void onGitReferenceUpdated(final Event event) {
+    if (event.getRefName().startsWith(RefNames.REFS_CHANGES)
+        || event.getRefName().startsWith(RefNames.REFS_DRAFT_COMMENTS)
+        || event.getRefName().startsWith(RefNames.REFS_USERS)) {
+      return;
+    }
     Futures.transformAsync(
         executor.submit(new GetChanges(event)),
         new AsyncFunction<List<Change>, List<Void>>() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
index 1bc6b05..53af8ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
@@ -33,11 +33,11 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.ChangeCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
+import com.google.gerrit.server.git.ScanningChangeCacheImpl;
 import com.google.gerrit.server.patch.PatchListLoader;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.SchemaFactory;
@@ -112,7 +112,6 @@
   }
 
   private final SchemaFactory<ReviewDb> schemaFactory;
-  private final ChangeCache changeCache;
   private final ChangeData.Factory changeDataFactory;
   private final GitRepositoryManager repoManager;
   private final ListeningExecutorService executor;
@@ -126,14 +125,12 @@
 
   @Inject
   SiteIndexer(SchemaFactory<ReviewDb> schemaFactory,
-      ChangeCache changeCache,
       ChangeData.Factory changeDataFactory,
       GitRepositoryManager repoManager,
       @IndexExecutor(BATCH) ListeningExecutorService executor,
       ChangeIndexer.Factory indexerFactory,
       @GerritServerConfig Config config) {
     this.schemaFactory = schemaFactory;
-    this.changeCache = changeCache;
     this.changeDataFactory = changeDataFactory;
     this.repoManager = repoManager;
     this.executor = executor;
@@ -241,7 +238,7 @@
         try (Repository repo = repoManager.openRepository(project);
             ReviewDb db = schemaFactory.open()) {
           Map<String, Ref> refs = repo.getRefDatabase().getRefs(ALL);
-          for (Change c : changeCache.get(project)) {
+          for (Change c : ScanningChangeCacheImpl.scan(repo, db)) {
             Ref r = refs.get(c.currentPatchSetId().toRefName());
             if (r != null) {
               byId.put(r.getObjectId(), changeDataFactory.create(db, c));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java
index cfafb37..4e76325 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java
@@ -14,14 +14,17 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.common.base.Joiner;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -44,6 +47,7 @@
 import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedSet;
 
 public class Schema_108 extends SchemaVersion {
   private final GitRepositoryManager repoManager;
@@ -59,7 +63,7 @@
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
     ui.message("Listing all changes ...");
     SetMultimap<Project.NameKey, Change.Id> openByProject =
-        getOpenChangesByProject(db);
+        getOpenChangesByProject(db, ui);
     ui.message("done");
 
     ui.message("Updating groups for open changes ...");
@@ -140,7 +144,9 @@
   }
 
   private SetMultimap<Project.NameKey, Change.Id> getOpenChangesByProject(
-      ReviewDb db) throws OrmException {
+      ReviewDb db, UpdateUI ui) throws OrmException {
+    SortedSet<NameKey> projects = repoManager.list();
+    SortedSet<NameKey> nonExistentProjects = Sets.newTreeSet();
     SetMultimap<Project.NameKey, Change.Id> openByProject =
         HashMultimap.create();
     for (Change c : db.changes().all()) {
@@ -149,10 +155,22 @@
         continue;
       }
 
-      // The old "submitted" state is not supported anymore
-      // (thus status is null) but it was an opened state and needs
-      // to be migrated as such
-      openByProject.put(c.getProject(), c.getId());
+      NameKey projectKey = c.getProject();
+      if (!projects.contains(projectKey)) {
+        nonExistentProjects.add(projectKey);
+      } else {
+        // The old "submitted" state is not supported anymore
+        // (thus status is null) but it was an opened state and needs
+        // to be migrated as such
+        openByProject.put(projectKey, c.getId());
+      }
+    }
+
+    if (!nonExistentProjects.isEmpty()) {
+      ui.message("Detected open changes referring to the following non-existent projects:");
+      ui.message(Joiner.on(", ").join(nonExistentProjects));
+      ui.message("It is highly recommended to remove\n"
+          + "the obsolete open changes, comments and patch-sets from your DB.\n");
     }
     return openByProject;
   }
diff --git a/lib/jgit/BUCK b/lib/jgit/BUCK
index 9cdfbf4..837d347 100644
--- a/lib/jgit/BUCK
+++ b/lib/jgit/BUCK
@@ -1,13 +1,13 @@
 include_defs('//lib/maven.defs')
 
 REPO = MAVEN_CENTRAL # Leave here even if set to MAVEN_CENTRAL.
-VERS = '4.1.0.201509280440-r'
+VERS = '4.1.1.201511131810-r'
 
 maven_jar(
   name = 'jgit',
   id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
-  bin_sha1 = '6410f290b796184df95d321a18d4c3665ba542a8',
-  src_sha1 = 'a681c59ec854b3e55f3abff8442e3e4ece31fa70',
+  bin_sha1 = '17cf7d0cb7da8bbcc16e43f0ab5dbfc43b5cb539',
+  src_sha1 = 'e65acfcdca36ae3cbdcbd5496648612eebe54be7',
   license = 'jgit',
   repository = REPO,
   unsign = True,
@@ -22,7 +22,7 @@
 maven_jar(
   name = 'jgit-servlet',
   id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
-  sha1 = '2f86ccebd5b5e0837757d35f63f04471432c13b8',
+  sha1 = '3bbeaab1ddfd87319c4938145c9c2c276ec8de17',
   license = 'jgit',
   repository = REPO,
   deps = [':jgit'],
@@ -36,7 +36,7 @@
 maven_jar(
   name = 'jgit-archive',
   id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
-  sha1 = 'd6d0ec8d77cea3b7efeac9789140f8373c10454b',
+  sha1 = 'aae0cc90ab568182bdd1cb5a32bee5b44780e1a7',
   license = 'jgit',
   repository = REPO,
   deps = [':jgit',
@@ -53,7 +53,7 @@
 maven_jar(
   name = 'junit',
   id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
-  sha1 = '69ef53175d9f150bc4072f8f5ba9046bf14cc55f',
+  sha1 = '63fd8d96849c7c20d5c70c91575ff8dda12718e0',
   license = 'DO_NOT_DISTRIBUTE',
   repository = REPO,
   unsign = True,
diff --git a/polygerrit-ui/app/elements/gr-account-dropdown.html b/polygerrit-ui/app/elements/gr-account-dropdown.html
new file mode 100644
index 0000000..fbc9a68
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-account-dropdown.html
@@ -0,0 +1,61 @@
+<!--
+Copyright (C) 2015 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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-account-dropdown">
+  <template>
+    <style>
+      :not(.loggedIn):not(.loggedOut) .loginButton,
+      :not(.loggedIn):not(.loggedOut) .logoutButton,
+      .loggedIn .loginButton,
+      .loggedOut .logoutButton {
+        display: none;
+      }
+    </style>
+    <div class$="[[_computeContainerClass(account)]]">
+      <a class="loginButton" href="/login" on-tap="_loginTapHandler">Login</a>
+      <a class="logoutButton" href="/logout">Logout</a>
+    </div>
+  </template>
+  <script>
+  (function() {
+    'use strict';
+
+    Polymer({
+      is: 'gr-account-dropdown',
+
+      properties: {
+        account: Object,
+      },
+
+      _loginTapHandler: function(e) {
+        e.preventDefault();
+        page('/login/' + encodeURIComponent(
+            window.location.pathname + window.location.hash));
+      },
+
+      _computeContainerClass: function(account) {
+        if (Object.keys(account).length == 0) {
+          return 'loggedOut';
+        }
+        return 'loggedIn';
+      },
+
+    });
+  })();
+  </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-account-manager.html b/polygerrit-ui/app/elements/gr-account-manager.html
new file mode 100644
index 0000000..10b6052
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-account-manager.html
@@ -0,0 +1,60 @@
+<!--
+Copyright (C) 2015 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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-account-manager">
+  <template>
+    <iron-ajax id="xhr"
+        auto
+        url="/accounts/self/detail"
+        json-prefix=")]}'"></iron-ajax>
+  </template>
+  <script>
+  (function() {
+    'use strict';
+
+    Polymer({
+      is: 'gr-account-manager',
+
+      hostAttributes: {
+        hidden: true
+      },
+
+      properties: {
+        account: {
+          type: Object,
+          notify: true,
+        },
+      },
+
+      listeners: {
+        'xhr.response': '_handleResponse',
+        'xhr.error': '_handleResponse',
+      },
+
+      _handleResponse: function(e, req) {
+        if (req.status >= 200 && req.status < 300) {
+          this.account = req.response;
+        } else {
+          this.account = {};
+        }
+      },
+
+    });
+  })();
+  </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 3007585..9f1ed1a 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -16,6 +16,8 @@
 
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../styles/app-theme.html">
+<link rel="import" href="gr-account-dropdown.html">
+<link rel="import" href="gr-account-manager.html">
 <link rel="import" href="gr-change-list-view.html">
 <link rel="import" href="gr-change-view.html">
 <link rel="import" href="gr-diff-view.html">
@@ -56,7 +58,7 @@
       .bigTitle:hover {
         text-decoration: underline;
       }
-      .searchContainer {
+      .headerRightItems {
         display: flex;
         flex: 1;
         justify-content: flex-end;
@@ -64,11 +66,18 @@
       gr-search-bar {
         width: 500px;
       }
+      gr-account-dropdown {
+        align-items: center;
+        display: flex;
+        margin-left: var(--default-horizontal-margin);
+      }
     </style>
+    <gr-account-manager account="{{account}}"></gr-account-manager>
     <header role="banner">
       <a href="/" class="bigTitle">PolyGerrit</a>
-      <div class="searchContainer">
+      <div class="headerRightItems">
         <gr-search-bar value="{{params.query}}" role="search"></gr-search-bar>
+        <gr-account-dropdown account="[[account]]"></gr-account-dropdown>
       </div>
     </header>
     <main>
@@ -92,6 +101,7 @@
       is: 'gr-app',
 
       properties: {
+        account: Object,
         constrained: {
           type: Boolean,
           value: false,
diff --git a/polygerrit-ui/app/elements/gr-diff-view.html b/polygerrit-ui/app/elements/gr-diff-view.html
index 2e0e0d0..874cbd0 100644
--- a/polygerrit-ui/app/elements/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/gr-diff-view.html
@@ -23,41 +23,41 @@
       :host {
         background-color: var(--view-background-color);
         display: block;
-        margin: 1em 1.25rem;
       }
       h3 {
-        border-bottom: 1px solid #eee;
-        padding: .75em 1em;
+        margin-top: 1em;
+        padding: .75em var(--default-horizontal-margin);
       }
       .mainContainer {
-        max-width: 100%;
-        overflow: auto;
+        border-bottom: 1px solid #eee;
+        border-collapse: collapse;
+        border-top: 1px solid #eee;
+        width: 100%;
+      }
+      .diffNumbers,
+      .diffContent {
+        vertical-align: top;
       }
       .diffContainer {
-        display: flex;
         font-family: 'Source Code Pro', monospace;
         white-space: pre;
       }
       .diffNumbers {
-        background-color: #ddd;
+        background-color: #eee;
         color: #666;
         padding: 0 .75em;
         text-align: right;
       }
       .diffContent {
-        border-right: 1px solid #ddd;
-        min-width: calc(80ch + 2px);
-        overflow-x: auto;
-        padding-left: 2px;
-        width: calc(80ch + 2px);
+        min-width: 80ch;
+        max-width: 120ch;
+        overflow: hidden;
       }
       .diffContainer.leftOnly .diffContent,
       .diffContainer.rightOnly .diffContent {
         overflow: visible;
       }
-      .diffContainer.leftOnly .right {
-        display: none;
-      }
+      .diffContainer.leftOnly .right,
       .diffContainer.rightOnly .left {
         display: none;
       }
@@ -129,14 +129,14 @@
       <a href$="[[_computeChangePath(_changeNum)]]">[[_changeNum]]</a><span>:</span>
       <span>[[_change.subject]]</span> — <span>[[params.path]]</span>
     </h3>
-    <div class="mainContainer">
-      <div class="diffContainer" id="diffContainer">
-        <div class="diffNumbers left" id="leftDiffNumbers"></div>
-        <div class="diffContent left" id="leftDiffContent"></div>
-        <div class="diffNumbers right" id="rightDiffNumbers"></div>
-        <div class="diffContent right" id="rightDiffContent"></div>
-      </div>
-    </div>
+    <table class="mainContainer">
+      <tr class="diffContainer" id="diffContainer">
+        <td class="diffNumbers left" id="leftDiffNumbers"></td>
+        <td class="diffContent left" id="leftDiffContent"></td>
+        <td class="diffNumbers right" id="rightDiffNumbers"></td>
+        <td class="diffContent right" id="rightDiffContent"></td>
+      </tr>
+    </table>
   </template>
   <script>
   (function() {
diff --git a/polygerrit-ui/app/scripts/app.js b/polygerrit-ui/app/scripts/app.js
index 7014074..7f3e05c 100644
--- a/polygerrit-ui/app/scripts/app.js
+++ b/polygerrit-ui/app/scripts/app.js
@@ -18,10 +18,10 @@
   // See https://github.com/Polymer/polymer/issues/1381
   window.addEventListener('WebComponentsReady', function() {
     // Middleware
-    function scrollToTop(ctx, next) {
+    page(function(ctx, next) {
       document.body.scrollTop = 0;
       next();
-    }
+    });
 
     // Routes.
     page('/', function() {
@@ -33,19 +33,19 @@
       app.params = data.params;
     }
 
-    page('/q/:query,:offset', scrollToTop, queryHandler);
-    page('/q/:query', scrollToTop, queryHandler);
+    page('/q/:query,:offset', queryHandler);
+    page('/q/:query', queryHandler);
 
-    page(/^\/(\d+)\/?/, scrollToTop, function(ctx) {
+    page(/^\/(\d+)\/?/, function(ctx) {
       page.redirect('/c/' + ctx.params[0]);
     });
 
-    page('/c/:changeNum', scrollToTop, function(data) {
+    page('/c/:changeNum', function(data) {
       app.route = 'gr-change-view';
       app.params = data.params;
     });
 
-    page(/^\/c\/(\d+)\/((\d+)(\.\.(\d+))?)\/(.+)/, scrollToTop, function(ctx) {
+    page(/^\/c\/(\d+)\/((\d+)(\.\.(\d+))?)\/(.+)/, function(ctx) {
       app.route = 'gr-diff-view';
       var params = {
         changeNum: ctx.params[0],
diff --git a/polygerrit-ui/bower.json b/polygerrit-ui/bower.json
index a50f1cc..e3c3cfc 100644
--- a/polygerrit-ui/bower.json
+++ b/polygerrit-ui/bower.json
@@ -12,18 +12,7 @@
   },
   "devDependencies": {
     "web-component-tester": "*",
-    "iron-test-helpers": "PolymerElements/iron-test-helpers#^1.0",
+    "iron-test-helpers": "PolymerElements/iron-test-helpers#~1.0.6",
     "test-fixture": "PolymerElements/test-fixture#^1.0.0"
-    "polymer": "Polymer/polymer#^1.2.1",
-    "page": "visionmedia/page.js#~1.6.4",
-    "iron-ajax": "PolymerElements/iron-ajax#~1.0.9",
-    "iron-a11y-keys": "PolymerElements/iron-a11y-keys#~1.0.3",
-    "iron-input": "PolymerElements/iron-input#~1.0.6"
-  },
-  "devDependencies": {
-    "web-component-tester": "*",
-    "iron-test-helpers": "PolymerElements/iron-test-helpers#~1.0",
-    "test-fixture": "PolymerElements/test-fixture#^1.0.0",
-    "iron-test-helpers": "PolymerElements/iron-test-helpers#~1.0.6"
   }
 }
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 008fe1d..29b81f4 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -68,6 +68,7 @@
 		return
 	}
 	defer res.Body.Close()
+	w.WriteHeader(res.StatusCode)
 	if _, err := io.Copy(w, res.Body); err != nil {
 		log.Println("Error copying response to ResponseWriter:", err)
 		return