Merge "Use allow/block/deny methods to improve test readability"
diff --git a/.buckconfig b/.buckconfig
index df8bfdc..f0422c9 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -1,8 +1,10 @@
 [alias]
   all = //:all
   api = //:api
-  api_deploy = //tools/maven:deploy
-  api_install = //tools/maven:install
+  api_deploy = //tools/maven:api_deploy
+  api_install = //tools/maven:api_install
+  war_deploy = //tools/maven:war_deploy
+  war_install = //tools/maven:war_install
   chrome = //:chrome
   docs = //Documentation:html
   firefox = //:firefox
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index 91cc585..2bcf5c2 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -129,18 +129,30 @@
   buck-out/gen/gerrit-plugin-api/plugin-api-javadoc.jar
 ----
 
-Install {extension,plugin,gwt}-api and gerrit.war to the local maven repository:
+Install {extension,plugin,gwt}-api to the local maven repository:
 
 ----
   buck build api_install
 ----
 
-Deploy {extension,plugin,gwt}-api and gerrit.war to the remote maven repository:
+Deploy {extension,plugin,gwt}-api to the remote maven repository:
 
 ----
   buck build api_deploy
 ----
 
+Install gerrit.war to the local maven repository:
+
+----
+  buck build war_install
+----
+
+Deploy gerrit.war to the remote maven repository:
+
+----
+  buck build war_deploy
+----
+
 The type of the repo is induced from the Gerrit version name, i.e.
 
 * `2.9-SNAPSHOT`: snapshot repo
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 9c00467..1af54c7 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -156,6 +156,12 @@
 
 * The WAR file to upload is `buck-out/gen/release.war`
 * Upload WAR to the storage bucket via `https://cloud.google.com/console` (manual via web browser)
+* Push the WAR file to the Maven storage bucket:
++
+----
+  buck build war_deploy
+----
+
 
 
 [[push-stable]]
diff --git a/Documentation/images/user-review-ui-change-screen-commit-info-merge-commit.png b/Documentation/images/user-review-ui-change-screen-commit-info-merge-commit.png
index 75cd60c..097637e 100644
--- a/Documentation/images/user-review-ui-change-screen-commit-info-merge-commit.png
+++ b/Documentation/images/user-review-ui-change-screen-commit-info-merge-commit.png
Binary files differ
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 d656e1e..a01525a 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
@@ -15,8 +15,12 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -28,6 +32,8 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeStatus;
 import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -37,6 +43,8 @@
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.EnumSet;
+import java.util.List;
 import java.util.Set;
 
 @NoHttpd
@@ -164,4 +172,88 @@
     assertEquals(in.branch, info.branch);
     assertEquals(in.subject, info.subject);
   }
+
+  @Test
+  public void queryChangesNoQuery() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = createChange();
+    List<ChangeInfo> results = gApi.changes().query().get();
+    assertEquals(2, results.size());
+    assertEquals(r2.getChangeId(), results.get(0).changeId);
+    assertEquals(r1.getChangeId(), results.get(1).changeId);
+  }
+
+  @Test
+  public void queryChangesNoResults() throws Exception {
+    createChange();
+    List<ChangeInfo> results = gApi.changes().query("status:open").get();
+    assertEquals(1, results.size());
+    results = gApi.changes().query("status:closed").get();
+    assertTrue(results.isEmpty());
+  }
+
+  @Test
+  public void queryChangesOneTerm() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = createChange();
+    List<ChangeInfo> results = gApi.changes().query("status:open").get();
+    assertEquals(2, results.size());
+    assertEquals(r2.getChangeId(), results.get(0).changeId);
+    assertEquals(r1.getChangeId(), results.get(1).changeId);
+  }
+
+  @Test
+  public void queryChangesMultipleTerms() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    createChange();
+    List<ChangeInfo> results = gApi.changes()
+        .query("status:open " + r1.getChangeId())
+        .get();
+    assertEquals(r1.getChangeId(), Iterables.getOnlyElement(results).changeId);
+  }
+
+  @Test
+  public void queryChangesLimit() throws Exception {
+    createChange();
+    PushOneCommit.Result r2 = createChange();
+    List<ChangeInfo> results = gApi.changes().query().withLimit(1).get();
+    assertEquals(1, results.size());
+    assertEquals(r2.getChangeId(), Iterables.getOnlyElement(results).changeId);
+  }
+
+  @Test
+  public void queryChangesStart() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    createChange();
+    List<ChangeInfo> results = gApi.changes().query().withStart(1).get();
+    assertEquals(r1.getChangeId(), Iterables.getOnlyElement(results).changeId);
+  }
+
+  @Test
+  public void queryChangesNoOptions() throws Exception {
+    PushOneCommit.Result r = createChange();
+    ChangeInfo result = Iterables.getOnlyElement(
+        gApi.changes().query(r.getChangeId()).get());
+    assertNull(result.labels);
+    assertNull(result.messages);
+    assertNull(result.revisions);
+    assertNull(result.actions);
+  }
+
+  @Test
+  public void queryChangesOptions() throws Exception {
+    PushOneCommit.Result r = createChange();
+    ChangeInfo result = Iterables.getOnlyElement(gApi.changes()
+        .query(r.getChangeId())
+        .withOptions(EnumSet.allOf(ListChangesOption.class))
+        .get());
+    assertEquals("Code-Review",
+        Iterables.getOnlyElement(result.labels.keySet()));
+    assertEquals(1, result.messages.size());
+    assertFalse(result.actions.isEmpty());
+
+    RevisionInfo rev = Iterables.getOnlyElement(result.revisions.values());
+    assertEquals(r.getPatchSetId().get(), rev._number);
+    assertFalse(rev.actions.isEmpty());
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
new file mode 100644
index 0000000..70a0a60
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
@@ -0,0 +1,231 @@
+// 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.acceptance.server.change;
+
+import static com.google.gerrit.acceptance.GitUtil.add;
+import static com.google.gerrit.acceptance.GitUtil.amendCommit;
+import static com.google.gerrit.acceptance.GitUtil.createCommit;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.GitUtil.rm;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil.Commit;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class PatchListCacheIT extends AbstractDaemonTest {
+  private static String SUBJECT_1 = "subject 1";
+  private static String SUBJECT_2 = "subject 2";
+  private static String SUBJECT_3 = "subject 3";
+  private static String FILE_A = "a.txt";
+  private static String FILE_B = "b.txt";
+  private static String FILE_C = "c.txt";
+  private static String FILE_D = "d.txt";
+
+  @Inject
+  private PatchListCache patchListCache;
+
+  @Test
+  public void listPatchesAgainstBase() throws GitAPIException, IOException,
+      PatchListNotAvailableException, OrmException, RestApiException {
+    add(git, FILE_D, "4");
+    createCommit(git, admin.getIdent(), SUBJECT_1);
+    pushHead(git, "refs/heads/master", false);
+
+    // Change 1, 1 (+FILE_A, -FILE_D)
+    add(git, FILE_A, "1");
+    rm(git, FILE_D);
+    Commit c = createCommit(git, admin.getIdent(), SUBJECT_2);
+    pushHead(git, "refs/for/master", false);
+
+    // Compare Change 1,1 with Base (+FILE_A, -FILE_D)
+    List<PatchListEntry> entries = getCurrentPatches(c.getChangeId());
+    assertEquals(3, entries.size());
+    assertAdded(Patch.COMMIT_MSG, entries.get(0));
+    assertAdded(FILE_A, entries.get(1));
+    assertDeleted(FILE_D, entries.get(2));
+
+    // Change 1,2 (+FILE_A, +FILE_B, -FILE_D)
+    add(git, FILE_B, "2");
+    c = amendCommit(git, admin.getIdent(), SUBJECT_2, c.getChangeId());
+    pushHead(git, "refs/for/master", false);
+    entries = getCurrentPatches(c.getChangeId());
+
+    // Compare Change 1,2 with Base (+FILE_A, +FILE_B, -FILE_D)
+    assertEquals(4, entries.size());
+    assertAdded(Patch.COMMIT_MSG, entries.get(0));
+    assertAdded(FILE_A, entries.get(1));
+    assertAdded(FILE_B, entries.get(2));
+    assertDeleted(FILE_D, entries.get(3));
+  }
+
+  @Test
+  public void listPatchesAgainstBaseWithRebase() throws GitAPIException,
+      IOException, PatchListNotAvailableException, OrmException,
+      RestApiException {
+    add(git, FILE_D, "4");
+    createCommit(git, admin.getIdent(), SUBJECT_1);
+    pushHead(git, "refs/heads/master", false);
+
+    // Change 1,1 (+FILE_A, -FILE_D)
+    add(git, FILE_A, "1");
+    rm(git, FILE_D);
+    Commit c = createCommit(git, admin.getIdent(), SUBJECT_2);
+    pushHead(git, "refs/for/master", false);
+    List<PatchListEntry> entries = getCurrentPatches(c.getChangeId());
+    assertEquals(3, entries.size());
+    assertAdded(Patch.COMMIT_MSG, entries.get(0));
+    assertAdded(FILE_A, entries.get(1));
+    assertDeleted(FILE_D, entries.get(2));
+
+    // Change 2,1 (+FILE_B)
+    git.reset().setMode(ResetType.HARD).setRef("HEAD~1").call();
+    add(git, FILE_B, "2");
+    createCommit(git, admin.getIdent(), SUBJECT_3);
+    pushHead(git, "refs/for/master", false);
+
+    // Change 1,2 (+FILE_A, -FILE_D))
+    git.cherryPick().include(c.getCommit()).call();
+    pushHead(git, "refs/for/master", false);
+
+    // Compare Change 1,2 with Base (+FILE_A, -FILE_D))
+    entries = getCurrentPatches(c.getChangeId());
+    assertEquals(3, entries.size());
+    assertAdded(Patch.COMMIT_MSG, entries.get(0));
+    assertAdded(FILE_A, entries.get(1));
+    assertDeleted(FILE_D, entries.get(2));
+  }
+
+  @Test
+  public void listPatchesAgainstOtherPatchSet() throws GitAPIException,
+      IOException, PatchListNotAvailableException, OrmException,
+      RestApiException {
+    add(git, FILE_D, "4");
+    createCommit(git, admin.getIdent(), SUBJECT_1);
+    pushHead(git, "refs/heads/master", false);
+
+    // Change 1,1 (+FILE_A, +FILE_C, -FILE_D)
+    add(git, FILE_A, "1");
+    add(git, FILE_C, "3");
+    rm(git, FILE_D);
+    Commit c = createCommit(git, admin.getIdent(), SUBJECT_2);
+    pushHead(git, "refs/for/master", false);
+    ObjectId a = getCurrentRevisionId(c.getChangeId());
+
+    // Change 1,2 (+FILE_A, +FILE_B, -FILE_D)
+    add(git, FILE_B, "2");
+    rm(git, FILE_C);
+    c = amendCommit(git, admin.getIdent(), SUBJECT_2, c.getChangeId());
+    pushHead(git, "refs/for/master", false);
+    ObjectId b = getCurrentRevisionId(c.getChangeId());
+
+    // Compare Change 1,1 with Change 1,2 (+FILE_B)
+    List<PatchListEntry>  entries = getPatches(a, b);
+    assertEquals(2, entries.size());
+    assertModified(Patch.COMMIT_MSG, entries.get(0));
+    assertAdded(FILE_B, entries.get(1));
+  }
+
+  @Test
+  public void listPatchesAgainstOtherPatchSetWithRebase()
+      throws GitAPIException, IOException, PatchListNotAvailableException,
+      OrmException, RestApiException {
+    add(git, FILE_D, "4");
+    createCommit(git, admin.getIdent(), SUBJECT_1);
+    pushHead(git, "refs/heads/master", false);
+
+    // Change 1,1 (+FILE_A, -FILE_D)
+    add(git, FILE_A, "1");
+    rm(git, FILE_D);
+    Commit c = createCommit(git, admin.getIdent(), SUBJECT_2);
+    pushHead(git, "refs/for/master", false);
+    ObjectId a = getCurrentRevisionId(c.getChangeId());
+
+    // Change 2,1 (+FILE_B)
+    git.reset().setMode(ResetType.HARD).setRef("HEAD~1").call();
+    add(git, FILE_B, "2");
+    createCommit(git, admin.getIdent(), SUBJECT_3);
+    pushHead(git, "refs/for/master", false);
+
+    // Change 1,2 (+FILE_A, +FILE_C, -FILE_D)
+    git.cherryPick().include(c.getCommit()).call();
+    add(git, FILE_C, "2");
+    c = amendCommit(git, admin.getIdent(), SUBJECT_2, c.getChangeId());
+    pushHead(git, "refs/for/master", false);
+    ObjectId b = getCurrentRevisionId(c.getChangeId());
+
+    // Compare Change 1,1 with Change 1,2 (+FILE_C)
+    List<PatchListEntry>  entries = getPatches(a, b);
+    assertEquals(2, entries.size());
+    assertModified(Patch.COMMIT_MSG, entries.get(0));
+    assertAdded(FILE_C, entries.get(1));
+  }
+
+  private static void assertAdded(String expectedNewName, PatchListEntry e) {
+    assertName(expectedNewName, e);
+    assertEquals(ChangeType.ADDED, e.getChangeType());
+  }
+
+  private static void assertModified(String expectedNewName, PatchListEntry e) {
+    assertName(expectedNewName, e);
+    assertEquals(ChangeType.MODIFIED, e.getChangeType());
+  }
+
+  private static void assertDeleted(String expectedNewName, PatchListEntry e) {
+    assertName(expectedNewName, e);
+    assertEquals(ChangeType.DELETED, e.getChangeType());
+  }
+
+  private static void assertName(String expectedNewName, PatchListEntry e) {
+    assertEquals(expectedNewName, e.getNewName());
+    assertNull(e.getOldName());
+  }
+
+  private List<PatchListEntry> getCurrentPatches(String changeId)
+      throws PatchListNotAvailableException, OrmException, RestApiException {
+    return patchListCache.get(getKey(null, getCurrentRevisionId(changeId))).getPatches();
+  }
+
+  private List<PatchListEntry> getPatches(ObjectId revisionIdA, ObjectId revisionIdB)
+      throws PatchListNotAvailableException, OrmException {
+    return patchListCache.get(getKey(revisionIdA, revisionIdB)).getPatches();
+  }
+
+  private PatchListKey getKey(ObjectId revisionIdA, ObjectId revisionIdB) throws OrmException {
+    return new PatchListKey(project, revisionIdA, revisionIdB, Whitespace.IGNORE_NONE);
+  }
+
+  private ObjectId getCurrentRevisionId(String changeId) throws RestApiException {
+    return ObjectId.fromString(gApi.changes().id(changeId).get().currentRevision);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
index ecf0d9e..201a0bd 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -30,49 +30,43 @@
       throws RestApiException;
   ChangeApi create(ChangeInfo in) throws RestApiException;
 
-  /**
-   * Shorthand for {@link #query(QueryParameter)} without any conditions (i.e. lists all changes).
-   */
-  List<ChangeInfo> query() throws RestApiException;
-  List<ChangeInfo> query(QueryParameter queryParameter) throws RestApiException;
+  QueryRequest query();
+  QueryRequest query(String query);
 
-  public class QueryParameter {
+  public abstract class QueryRequest {
     private String query;
     private int limit;
     private int start;
     private EnumSet<ListChangesOption> options = EnumSet.noneOf(ListChangesOption.class);
 
-    public QueryParameter() {}
+    public abstract List<ChangeInfo> get() throws RestApiException;
 
-    public QueryParameter(String query) {
-      this.query = query;
-    }
-
-    public QueryParameter withQuery(String query) {
+    public QueryRequest withQuery(String query) {
       this.query = query;
       return this;
     }
 
-    public QueryParameter withLimit(int limit) {
+    public QueryRequest withLimit(int limit) {
       this.limit = limit;
       return this;
     }
 
-    public QueryParameter withStart(int start) {
+    public QueryRequest withStart(int start) {
       this.start = start;
       return this;
     }
 
-    public QueryParameter withOption(ListChangesOption options) {
+    public QueryRequest withOption(ListChangesOption options) {
       this.options.add(options);
       return this;
     }
-    public QueryParameter withOptions(ListChangesOption... options) {
+
+    public QueryRequest withOptions(ListChangesOption... options) {
       this.options.addAll(Arrays.asList(options));
       return this;
     }
 
-    public QueryParameter withOptions(EnumSet<ListChangesOption> options) {
+    public QueryRequest withOptions(EnumSet<ListChangesOption> options) {
       this.options = options;
       return this;
     }
@@ -120,12 +114,12 @@
     }
 
     @Override
-    public List<ChangeInfo> query() throws RestApiException {
+    public QueryRequest query() {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<ChangeInfo> query(QueryParameter queryParameter) throws RestApiException {
+    public QueryRequest query(String query) {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
index 02351cd..9c0cfd8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -23,37 +23,32 @@
 public interface Projects {
   ProjectApi name(String name) throws RestApiException;
 
-  List<ProjectInfo> list() throws RestApiException;
-  List<ProjectInfo> list(ListParameter listParameter) throws RestApiException;
+  ListRequest list();
 
-  public class ListParameter {
+  public abstract class ListRequest {
     private boolean description;
     private String prefix;
     private int limit;
     private int start;
 
-    public ListParameter() {}
+    public abstract List<ProjectInfo> get() throws RestApiException;
 
-    public ListParameter(String prefix) {
-      this.prefix = prefix;
-    }
-
-    public ListParameter withDescription(boolean description) {
+    public ListRequest withDescription(boolean description) {
       this.description = description;
       return this;
     }
 
-    public ListParameter withPrefix(String prefix) {
+    public ListRequest withPrefix(String prefix) {
       this.prefix = prefix;
       return this;
     }
 
-    public ListParameter withLimit(int limit) {
+    public ListRequest withLimit(int limit) {
       this.limit = limit;
       return this;
     }
 
-    public ListParameter withStart(int start) {
+    public ListRequest withStart(int start) {
       this.start = start;
       return this;
     }
@@ -86,12 +81,7 @@
     }
 
     @Override
-    public List<ProjectInfo> list() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
-    public List<ProjectInfo> list(ListParameter listParameter) throws RestApiException {
+    public ListRequest list() {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
index b8cc5c2d..053999a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -80,27 +80,33 @@
         setHeight(info.height() > 0 ? info.height() + "px" : "");
         setUrl(info.url());
         popup(account, addPopup);
+      } else if (account.email() != null) {
+        loadAvatar(account, size, addPopup);
       }
     } else if (account.email() != null) {
-      // TODO Kill /accounts/*/avatar URL.
-      String u = account.email();
-      if (Gerrit.isSignedIn()
-          && u.equals(Gerrit.getUserAccount().getPreferredEmail())) {
-        u = "self";
-      }
-      RestApi api = new RestApi("/accounts/").id(u).view("avatar");
-      if (size > 0) {
-        api.addParameter("s", size);
-        setSize("", size + "px");
-      }
-      setVisible(false);
-      setUrl(api.url());
-      popup(account, addPopup);
+      loadAvatar(account, size, addPopup);
     } else {
       setVisible(false);
     }
   }
 
+  private void loadAvatar(AccountInfo account, int size, boolean addPopup) {
+     // TODO Kill /accounts/*/avatar URL.
+    String u = account.email();
+    if (Gerrit.isSignedIn()
+        && u.equals(Gerrit.getUserAccount().getPreferredEmail())) {
+      u = "self";
+    }
+    RestApi api = new RestApi("/accounts/").id(u).view("avatar");
+    if (size > 0) {
+      api.addParameter("s", size);
+      setSize("", size + "px");
+    }
+    setVisible(false);
+    setUrl(api.url());
+    popup(account, addPopup);
+  }
+
   private void popup(AccountInfo account, boolean addPopup) {
     if (addPopup) {
       PopupHandler popupHandler = new PopupHandler(account, this);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
index 1fab60a..2b78fa4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
@@ -79,6 +79,6 @@
   @Source("listAdd.png")
   public ImageResource listAdd();
 
-  @Source("important.png")
-  public ImageResource important();
+  @Source("merge.png")
+  public ImageResource merge();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
index 915cbfb..162c19e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
@@ -46,7 +46,7 @@
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.ScrollPanel;
-import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
@@ -67,7 +67,7 @@
   @UiField TableCellElement webLinkCell;
   @UiField Element parents;
   @UiField FlowPanel parentCommits;
-  @UiField VerticalPanel parentWebLinks;
+  @UiField FlowPanel parentWebLinks;
   @UiField InlineHyperlink authorNameEmail;
   @UiField Element authorDate;
   @UiField InlineHyperlink committerNameEmail;
@@ -154,7 +154,7 @@
       GitwebLink gw = Gerrit.getGitwebLink();
       if (gw != null) {
         Anchor a =
-            new Anchor(gw.toRevision(project, c.commit()), gw.getLinkName());
+            new Anchor(gw.getLinkName(), gw.toRevision(project, c.commit()));
         a.setStyleName(style.parentWebLink());
         parentWebLinks.add(a);
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
index 456fa13..7fb43c4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
@@ -86,9 +86,10 @@
     }
     .parentWebLink {
       margin-left:16px;
+      display: block;
     }
 
-    .mergeCommit {
+    .commit {
       margin-right: 3px;
       float: left;
     }
@@ -126,12 +127,12 @@
       </tr>
       <tr>
         <th>
-          <div class='{style.mergeCommit}'>
+          <div class='{style.commit}'>
             <ui:msg>Commit</ui:msg>
           </div>
           <g:Image
               ui:field='mergeCommit'
-              resource='{ico.important}'
+              resource='{ico.merge}'
               visible='false'
               title='Merge Commit'>
             <ui:attribute name='title'/>
@@ -146,7 +147,7 @@
           <g:FlowPanel ui:field='parentCommits'/>
         </td>
         <td>
-          <g:VerticalPanel ui:field='parentWebLinks'/>
+          <g:FlowPanel ui:field='parentWebLinks'/>
         </td>
       </tr>
       <tr>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/important.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/important.png
deleted file mode 100644
index 81e9ed2..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/important.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/merge.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/merge.png
new file mode 100644
index 0000000..9c892db
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/merge.png
Binary files differ
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
index 11383ca..74dc56f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
@@ -3,25 +3,20 @@
 package com.google.gerrit.httpd;
 
 import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.api.changes.Changes;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeDataSource;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.HashSet;
+import java.util.List;
 
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
@@ -33,53 +28,30 @@
   private static final Logger log =
       LoggerFactory.getLogger(DirectChangeByCommit.class);
 
-  private final ChangeQueryBuilder.Factory queryBuilder;
-  private final Provider<ChangeQueryRewriter> queryRewriter;
-  private final Provider<CurrentUser> currentUser;
+  private final Changes changes;
 
   @Inject
-  DirectChangeByCommit(ChangeQueryBuilder.Factory queryBuilder,
-      Provider<ChangeQueryRewriter> queryRewriter,
-      Provider<CurrentUser> currentUser) {
-    this.queryBuilder = queryBuilder;
-    this.queryRewriter = queryRewriter;
-    this.currentUser = currentUser;
+  DirectChangeByCommit(Changes changes) {
+    this.changes = changes;
   }
 
   @Override
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
     String query = CharMatcher.is('/').trimTrailingFrom(req.getPathInfo());
-    HashSet<Change.Id> ids = new HashSet<>();
+    List<ChangeInfo> results;
     try {
-      ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
-      Predicate<ChangeData> visibleToMe = builder.is_visible();
-      Predicate<ChangeData> q = builder.parse(query);
-      q = Predicate.and(q, builder.sortkey_before("z"), builder.limit(2), visibleToMe);
-
-      ChangeQueryRewriter rewriter = queryRewriter.get();
-      Predicate<ChangeData> s = rewriter.rewrite(q, 0);
-      if (!(s instanceof ChangeDataSource)) {
-        s = rewriter.rewrite(Predicate.and(builder.status_open(), q), 0);
-      }
-
-      if (s instanceof ChangeDataSource) {
-        for (ChangeData d : ((ChangeDataSource) s).read()) {
-          ids.add(d.getId());
-        }
-      }
-    } catch (QueryParseException e) {
-      log.info("Received invalid query by URL: /r/" + query);
-    } catch (OrmException e) {
+      results = changes.query(query).withLimit(2).get();
+    } catch (RestApiException e) {
       log.warn("Cannot process query by URL: /r/" + query, e);
+      results = ImmutableList.of();
     }
-
     String token;
-    if (ids.size() == 1) {
+    if (results.size() == 1) {
       // If exactly one change matches, link to that change.
       // TODO Link to a specific patch set, if one matched.
-      token = PageLinks.toChange(ids.iterator().next());
-
+      token = PageLinks.toChange(
+          new Change.Id(results.iterator().next()._number));
     } else {
       // Otherwise, link to the query page.
       token = PageLinks.toChangeQuery(query);
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index 8c7e261..404f0d9 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -22,6 +22,8 @@
   <artifactId>gerrit-plugin-archetype</artifactId>
   <version>2.10-SNAPSHOT</version>
   <name>Gerrit Code Review - Plugin Archetype</name>
+  <description>Maven Archetype for Gerrit Plugins</description>
+  <url>http://code.google.com/p/gerrit/</url>
 
   <properties>
     <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion>
@@ -46,4 +48,52 @@
     </resources>
   </build>
 
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+
+  <scm>
+    <url>https://gerrit.googlesource.com/gerrit</url>
+    <connection>https://gerrit.googlesource.com/gerrit</connection>
+  </scm>
+
+  <developers>
+    <developer>
+      <name>Dave Borowitz</name>
+    </developer>
+    <developer>
+      <name>David Pursehouse</name>
+    </developer>
+    <developer>
+      <name>Edwin Kempin</name>
+    </developer>
+    <developer>
+      <name>Martin Fick</name>
+    </developer>
+    <developer>
+      <name>Saša Živkov</name>
+    </developer>
+    <developer>
+      <name>Shawn Pearce</name>
+    </developer>
+  </developers>
+
+  <mailingLists>
+    <mailingList>
+      <name>Repo and Gerrit Discussion</name>
+      <post>repo-discuss@googlegroups.com</post>
+      <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe>
+      <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe>
+      <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive>
+    </mailingList>
+  </mailingLists>
+
+  <issueManagement>
+    <url>http://code.google.com/p/gerrit/issues/list</url>
+    <system>Google Code Issue Tracker</system>
+  </issueManagement>
 </project>
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index 09f70e8..38b223a 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -21,7 +21,9 @@
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwt-archetype</artifactId>
   <version>2.10-SNAPSHOT</version>
-  <name>Gerrit Code Review - Web Ui GWT Plugin Archetype</name>
+  <name>Gerrit Code Review - Web UI GWT Plugin Archetype</name>
+  <description>Maven Archetype for Gerrit Web UI GWT Plugins</description>
+  <url>http://code.google.com/p/gerrit/</url>
 
   <properties>
     <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion>
@@ -46,4 +48,52 @@
     </resources>
   </build>
 
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+
+  <scm>
+    <url>https://gerrit.googlesource.com/gerrit</url>
+    <connection>https://gerrit.googlesource.com/gerrit</connection>
+  </scm>
+
+  <developers>
+    <developer>
+      <name>Dave Borowitz</name>
+    </developer>
+    <developer>
+      <name>David Pursehouse</name>
+    </developer>
+    <developer>
+      <name>Edwin Kempin</name>
+    </developer>
+    <developer>
+      <name>Martin Fick</name>
+    </developer>
+    <developer>
+      <name>Saša Živkov</name>
+    </developer>
+    <developer>
+      <name>Shawn Pearce</name>
+    </developer>
+  </developers>
+
+  <mailingLists>
+    <mailingList>
+      <name>Repo and Gerrit Discussion</name>
+      <post>repo-discuss@googlegroups.com</post>
+      <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe>
+      <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe>
+      <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive>
+    </mailingList>
+  </mailingLists>
+
+  <issueManagement>
+    <url>http://code.google.com/p/gerrit/issues/list</url>
+    <system>Google Code Issue Tracker</system>
+  </issueManagement>
 </project>
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index b99130e..9da4e89 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -22,6 +22,8 @@
   <artifactId>gerrit-plugin-js-archetype</artifactId>
   <version>2.10-SNAPSHOT</version>
   <name>Gerrit Code Review - Web UI JavaScript Plugin Archetype</name>
+  <description>Maven Archetype for Gerrit Web UI JavaScript Plugins</description>
+  <url>http://code.google.com/p/gerrit/</url>
 
   <properties>
     <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion>
@@ -46,4 +48,52 @@
     </resources>
   </build>
 
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+
+  <scm>
+    <url>https://gerrit.googlesource.com/gerrit</url>
+    <connection>https://gerrit.googlesource.com/gerrit</connection>
+  </scm>
+
+  <developers>
+    <developer>
+      <name>Dave Borowitz</name>
+    </developer>
+    <developer>
+      <name>David Pursehouse</name>
+    </developer>
+    <developer>
+      <name>Edwin Kempin</name>
+    </developer>
+    <developer>
+      <name>Martin Fick</name>
+    </developer>
+    <developer>
+      <name>Saša Živkov</name>
+    </developer>
+    <developer>
+      <name>Shawn Pearce</name>
+    </developer>
+  </developers>
+
+  <mailingLists>
+    <mailingList>
+      <name>Repo and Gerrit Discussion</name>
+      <post>repo-discuss@googlegroups.com</post>
+      <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe>
+      <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe>
+      <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive>
+    </mailingList>
+  </mailingLists>
+
+  <issueManagement>
+    <url>http://code.google.com/p/gerrit/issues/list</url>
+    <system>Google Code Issue Tracker</system>
+  </issueManagement>
 </project>
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 8d4a0c9..5c5c569 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
@@ -164,7 +164,7 @@
   public ChangeInfo get(EnumSet<ListChangesOption> s)
       throws RestApiException {
     try {
-      return new ChangeInfoMapper(s).map(
+      return ChangeInfoMapper.INSTANCE.apply(
           changeJson.addOptions(s).format(change));
     } catch (OrmException e) {
       throw new RestApiException("Cannot retrieve change", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java
index 6aab89c..8e6c20b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java
@@ -14,13 +14,7 @@
 
 package com.google.gerrit.server.api.changes;
 
-import static com.google.gerrit.extensions.common.ListChangesOption.ALL_REVISIONS;
-import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_ACTIONS;
-import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_REVISION;
-import static com.google.gerrit.extensions.common.ListChangesOption.DETAILED_LABELS;
-import static com.google.gerrit.extensions.common.ListChangesOption.LABELS;
-import static com.google.gerrit.extensions.common.ListChangesOption.MESSAGES;
-
+import com.google.common.base.Function;
 import com.google.common.collect.EnumBiMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -29,48 +23,46 @@
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.ChangeStatus;
 import com.google.gerrit.extensions.common.LabelInfo;
-import com.google.gerrit.extensions.common.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.server.api.accounts.AccountInfoMapper;
 import com.google.gerrit.server.change.ChangeJson;
 
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 
-public class ChangeInfoMapper {
-  private final static EnumBiMap<Change.Status, ChangeStatus> MAP =
+public class ChangeInfoMapper
+    implements Function<ChangeJson.ChangeInfo, ChangeInfo> {
+  public static final ChangeInfoMapper INSTANCE = new ChangeInfoMapper();
+
+  private final static EnumBiMap<Change.Status, ChangeStatus> STATUS_MAP =
       EnumBiMap.create(Change.Status.class, ChangeStatus.class);
   static {
-    MAP.put(Status.DRAFT, ChangeStatus.DRAFT);
-    MAP.put(Status.NEW, ChangeStatus.NEW);
-    MAP.put(Status.SUBMITTED, ChangeStatus.SUBMITTED);
-    MAP.put(Status.MERGED, ChangeStatus.MERGED);
-    MAP.put(Status.ABANDONED, ChangeStatus.ABANDONED);
+    STATUS_MAP.put(Status.DRAFT, ChangeStatus.DRAFT);
+    STATUS_MAP.put(Status.NEW, ChangeStatus.NEW);
+    STATUS_MAP.put(Status.SUBMITTED, ChangeStatus.SUBMITTED);
+    STATUS_MAP.put(Status.MERGED, ChangeStatus.MERGED);
+    STATUS_MAP.put(Status.ABANDONED, ChangeStatus.ABANDONED);
   }
 
-  private final EnumSet<ListChangesOption> s;
-
-  ChangeInfoMapper(EnumSet<ListChangesOption> s) {
-    this.s = s;
+  public static Status changeStatus2Status(ChangeStatus status) {
+    if (status != null) {
+      return STATUS_MAP.inverse().get(status);
+    }
+    return Change.Status.NEW;
   }
 
-  ChangeInfo map(ChangeJson.ChangeInfo i) {
+  private ChangeInfoMapper() {
+  }
+
+  @Override
+  public ChangeInfo apply(ChangeJson.ChangeInfo i) {
     ChangeInfo o = new ChangeInfo();
     mapCommon(i, o);
-    if (has(LABELS) || has(DETAILED_LABELS)) {
-      mapLabels(i, o);
-    }
-    if (has(MESSAGES)) {
-      mapMessages(i, o);
-    }
-    if (has(ALL_REVISIONS) || has(CURRENT_REVISION)) {
-      o.revisions = i.revisions;
-    }
-    if (has(CURRENT_ACTIONS)) {
-      o.actions = i.actions;
-    }
+    mapLabels(i, o);
+    mapMessages(i, o);
+    o.revisions = i.revisions;
+    o.actions = i.actions;
     return o;
   }
 
@@ -81,7 +73,7 @@
     o.topic = i.topic;
     o.changeId = i.changeId;
     o.subject = i.subject;
-    o.status = MAP.get(i.status);
+    o.status = STATUS_MAP.get(i.status);
     o.created = i.created;
     o.updated = i.updated;
     o.starred = i.starred;
@@ -95,6 +87,9 @@
   }
 
   private void mapMessages(ChangeJson.ChangeInfo i, ChangeInfo o) {
+    if (i.messages == null) {
+      return;
+    }
     List<ChangeMessageInfo> r =
         Lists.newArrayListWithCapacity(i.messages.size());
     for (ChangeJson.ChangeMessageInfo m : i.messages) {
@@ -110,6 +105,9 @@
   }
 
   private void mapLabels(ChangeJson.ChangeInfo i, ChangeInfo o) {
+    if (i.labels == null) {
+      return;
+    }
     Map<String, LabelInfo> r = Maps.newLinkedHashMap();
     for (Map.Entry<String, ChangeJson.LabelInfo> e : i.labels.entrySet()) {
       ChangeJson.LabelInfo li = e.getValue();
@@ -134,17 +132,6 @@
     o.labels = r;
   }
 
-  private boolean has(ListChangesOption o) {
-    return s.contains(o);
-  }
-
-  public static Status changeStatus2Status(ChangeStatus status) {
-    if (status != null) {
-      return MAP.inverse().get(status);
-    }
-    return Change.Status.NEW;
-  }
-
   private static ApprovalInfo fromApprovalInfo(ChangeJson.ApprovalInfo ai) {
     ApprovalInfo ao = new ApprovalInfo();
     ao.value = ai.value;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index 976228f..0718810 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -14,11 +14,18 @@
 
 package com.google.gerrit.server.api.changes;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -27,23 +34,29 @@
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.CreateChange;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import java.io.IOException;
+import java.util.List;
 
-class ChangesImpl extends Changes.NotImplemented implements Changes {
+class ChangesImpl implements Changes {
   private final ChangesCollection changes;
   private final ChangeApiImpl.Factory api;
   private final CreateChange.Factory createChangeFactory;
+  private final Provider<QueryChanges> queryProvider;
 
   @Inject
   ChangesImpl(ChangesCollection changes,
       ChangeApiImpl.Factory api,
-      CreateChange.Factory createChangeFactory) {
+      CreateChange.Factory createChangeFactory,
+      Provider<QueryChanges> queryProvider) {
     this.changes = changes;
     this.api = api;
     this.createChangeFactory = createChangeFactory;
+    this.queryProvider = queryProvider;
   }
 
   @Override
@@ -82,4 +95,50 @@
       throw new RestApiException("Cannot create change", e);
     }
   }
+
+  @Override
+  public QueryRequest query() {
+    return new QueryRequest() {
+      @Override
+      public List<ChangeInfo> get() throws RestApiException {
+        return ChangesImpl.this.get(this);
+      }
+    };
+  }
+
+  @Override
+  public QueryRequest query(String query) {
+    return query().withQuery(query);
+  }
+
+  private List<ChangeInfo> get(final QueryRequest q) throws RestApiException {
+    QueryChanges qc = queryProvider.get();
+    if (q.getQuery() != null) {
+      qc.addQuery(q.getQuery());
+    }
+    qc.setLimit(q.getLimit());
+    qc.setStart(q.getStart());
+    for (ListChangesOption option : q.getOptions()) {
+      qc.addOption(option);
+    }
+
+    try {
+      List<?> result = qc.apply(TopLevelResource.INSTANCE);
+      if (result.isEmpty()) {
+        return ImmutableList.of();
+      }
+
+      // Check type safety of result; the extension API should be safer than the
+      // REST API in this case, since it's intended to be used in Java.
+      Object first = checkNotNull(result.iterator().next());
+      checkState(first instanceof ChangeJson.ChangeInfo);
+      @SuppressWarnings("unchecked")
+      List<ChangeJson.ChangeInfo> infos = (List<ChangeJson.ChangeInfo>) result;
+
+      return ImmutableList.copyOf(
+          Lists.transform(infos, ChangeInfoMapper.INSTANCE));
+    } catch (BadRequestException | AuthException | OrmException e) {
+      throw new RestApiException("Cannot query changes", e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index 66ea1e0..ebf5ac7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -42,7 +42,7 @@
 
 import java.util.List;
 
-class EmailArguments {
+public class EmailArguments {
   final GitRepositoryManager server;
   final ProjectCache projectCache;
   final GroupBackend groupBackend;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index c36fbc0..ff03840 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -34,7 +34,7 @@
 import java.io.Serializable;
 
 public class PatchListKey implements Serializable {
-  static final long serialVersionUID = 16L;
+  static final long serialVersionUID = 17L;
 
   private transient ObjectId oldId;
   private transient ObjectId newId;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index f704ea2..d4131b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -15,7 +15,9 @@
 
 package com.google.gerrit.server.patch;
 
+import com.google.common.base.Function;
 import com.google.common.cache.CacheLoader;
+import com.google.common.collect.FluentIterable;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -51,8 +53,6 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
 import org.slf4j.Logger;
@@ -60,23 +60,28 @@
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 public class PatchListLoader extends CacheLoader<PatchListKey, PatchList> {
   static final Logger log = LoggerFactory.getLogger(PatchListLoader.class);
 
   private final GitRepositoryManager repoManager;
+  private final PatchListCache patchListCache;
 
   @Inject
-  PatchListLoader(GitRepositoryManager mgr) {
+  PatchListLoader(GitRepositoryManager mgr, PatchListCache plc) {
     repoManager = mgr;
+    patchListCache = plc;
   }
 
   @Override
-  public PatchList load(final PatchListKey key) throws Exception {
+  public PatchList load(final PatchListKey key) throws IOException,
+      PatchListNotAvailableException {
     final Repository repo = repoManager.openRepository(key.projectKey);
     try {
       return readPatchList(key, repo);
@@ -102,8 +107,8 @@
     }
   }
 
-  private PatchList readPatchList(final PatchListKey key,
-      final Repository repo) throws IOException {
+  private PatchList readPatchList(final PatchListKey key, final Repository repo)
+      throws IOException, PatchListNotAvailableException {
     final RawTextComparator cmp = comparatorFor(key.getWhitespace());
     final ObjectReader reader = repo.newObjectReader();
     try {
@@ -123,42 +128,42 @@
       final boolean againstParent =
           b.getParentCount() > 0 && b.getParent(0) == a;
 
-      RevCommit aCommit;
-      RevTree aTree;
-      if (a instanceof RevCommit) {
-        aCommit = (RevCommit) a;
-        aTree = aCommit.getTree();
-      } else if (a instanceof RevTree) {
-        aCommit = null;
-        aTree = (RevTree) a;
-      } else {
-        throw new IOException("Unexpected type: " + a.getClass());
-      }
-
+      RevCommit aCommit = a instanceof RevCommit ? (RevCommit) a : null;
+      RevTree aTree = rw.parseTree(a);
       RevTree bTree = b.getTree();
 
-      final TreeWalk walk = new TreeWalk(reader);
-      walk.reset();
-      walk.setRecursive(true);
-      walk.addTree(aTree);
-      walk.addTree(bTree);
-      walk.setFilter(TreeFilter.ANY_DIFF);
-
       DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
       df.setRepository(repo);
       df.setDiffComparator(cmp);
       df.setDetectRenames(true);
       List<DiffEntry> diffEntries = df.scan(aTree, bTree);
 
-      final int cnt = diffEntries.size();
-      final PatchListEntry[] entries = new PatchListEntry[1 + cnt];
-      entries[0] = newCommitMessage(cmp, repo, reader, //
-          againstParent ? null : aCommit, b);
+      Set<String> paths = key.getOldId() != null
+          ? FluentIterable.from(patchListCache.get(
+                  new PatchListKey(key.projectKey, null, key.getNewId(),
+                  key.getWhitespace())).getPatches())
+              .transform(new Function<PatchListEntry, String>() {
+                @Override
+                public String apply(PatchListEntry entry) {
+                  return entry.getNewName();
+                }
+              })
+          .toSet()
+          : null;
+      int cnt = diffEntries.size();
+      List<PatchListEntry> entries = new ArrayList<>();
+      entries.add(newCommitMessage(cmp, repo, reader, //
+          againstParent ? null : aCommit, b));
       for (int i = 0; i < cnt; i++) {
-        FileHeader fh = df.toFileHeader(diffEntries.get(i));
-        entries[1 + i] = newEntry(aTree, fh);
+        DiffEntry diffEntry = diffEntries.get(i);
+        if (paths == null || paths.contains(diffEntry.getNewPath())
+            || paths.contains(diffEntry.getOldPath())) {
+          FileHeader fh = df.toFileHeader(diffEntry);
+          entries.add(newEntry(aTree, fh));
+        }
       }
-      return new PatchList(a, b, againstParent, entries);
+      return new PatchList(a, b, againstParent,
+          entries.toArray(new PatchListEntry[entries.size()]));
     } finally {
       reader.release();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index 41c2e23..fb55b49 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -106,7 +106,7 @@
   }
 
   @Override
-  public Object apply(TopLevelResource rsrc)
+  public List<?> apply(TopLevelResource rsrc)
       throws BadRequestException, AuthException, OrmException {
     List<List<ChangeInfo>> out;
     try {
diff --git a/tools/maven/package.defs b/tools/maven/package.defs
index 9e5a144..a9dc60c 100644
--- a/tools/maven/package.defs
+++ b/tools/maven/package.defs
@@ -21,27 +21,51 @@
     doc = {},
     war = {}):
   cmd = ['$(exe //tools/maven:mvn)', '-v', version, '-o', '$OUT']
-  dep = []
-
-  for type,d in [('jar', jar), ('java-source', src), ('javadoc', doc), ('war', war)]:
+  api_cmd = []
+  api_deps = []
+  for type,d in [('jar', jar), ('java-source', src), ('javadoc', doc)]:
     for a,t in d.iteritems():
-      cmd.append('-s %s:%s:$(location %s)' % (a,type,t))
-      dep.append(t)
+      api_cmd.append('-s %s:%s:$(location %s)' % (a,type,t))
+      api_deps.append(t)
 
   genrule(
-    name = 'install',
-    cmd = ' '.join(cmd + ['-a', 'install']),
-    deps = dep + ['//tools/maven:mvn'],
-    out = 'install.info',
+    name = 'api_install',
+    cmd = ' '.join(cmd + api_cmd + ['-a', 'install']),
+    deps = api_deps + ['//tools/maven:mvn'],
+    out = 'api_install.info',
   )
 
   if repository and url:
     genrule(
-      name = 'deploy',
-      cmd = ' '.join(cmd + [
+      name = 'api_deploy',
+      cmd = ' '.join(cmd + api_cmd + [
         '-a', 'deploy',
         '--repository', repository,
         '--url', url]),
-      deps = dep + ['//tools/maven:mvn'],
-      out = 'deploy.info',
+      deps = api_deps + ['//tools/maven:mvn'],
+      out = 'api_deploy.info',
+    )
+
+  war_cmd = []
+  war_deps = []
+  for a,t in war.iteritems():
+    war_cmd.append('-s %s:war:$(location %s)' % (a,t))
+    war_deps.append(t)
+
+  genrule(
+    name = 'war_install',
+    cmd = ' '.join(cmd + war_cmd + ['-a', 'install']),
+    deps = war_deps + ['//tools/maven:mvn'],
+    out = 'war_install.info',
+  )
+
+  if repository and url:
+    genrule(
+      name = 'war_deploy',
+      cmd = ' '.join(cmd + war_cmd + [
+        '-a', 'deploy',
+        '--repository', repository,
+        '--url', url]),
+      deps = war_deps + ['//tools/maven:mvn'],
+      out = 'war_deploy.info',
     )