Merge "Move 'User Refs' docs to the intro-user doc."
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 14523ec..c648725 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2201,7 +2201,7 @@
 thread pool waiting for a worker thread to become available.
 0 sets the queue size to the Integer.MAX_VALUE.
 +
-By default 50.
+By default 200.
 
 [[httpd.maxWait]]httpd.maxWait::
 +
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 94f8ac9..4b57827 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3326,6 +3326,60 @@
 `application/json` the content is returned as JSON string and
 `X-FYI-Content-Encoding` is set to `json`.
 
+[[get-safe-content]]
+=== Download Content
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/download'
+--
+
+Downloads the content of a file from a certain revision, in a safe format
+that poses no risk for inadvertent execution of untrusted code.
+
+If the content type is defined as safe, the binary file content is returned
+verbatim. If the content type is not safe, the file is stored inside a ZIP
+file, containing a single entry with a random, unpredictable name having the
+same base and suffix as the true filename. The ZIP file is returned in
+verbatim binary form.
+
+See link:config-gerrit.html#mimetype.name.safe[Gerrit config documentation]
+for information about safe file type configuration.
+
+The HTTP resource Content-Type is dependent on the file type: the
+applicable type for safe files, or "application/zip" for unsafe files.
+
+The `suffix` parameter can be specified to decorate the names of the files.
+The suffix is inserted between the base filename and the random component or
+extension, or appended to the filename if neither such component is present.
+Only the lowercase Roman letters a-z are permitted; other characters are ignored.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/website%2Freleases%2Flogo.png/safe_content HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment; filename="logo.png"
+  Content-Type: image/png
+
+  `[binary data for logo.png]`
+----
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/safe_content?suffix=new HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: Content-Disposition:attachment; filename="RefControl_new-931cdb73ae9d97eb500a3533455b055d90b99944.java.zip"
+  Content-Type:application/zip
+
+  `[binary ZIP archive containing a single file, "RefControl_new-cb218df1337df48a0e7ab30a49a8067ac7321881.java"]`
+----
+
 [[get-diff]]
 === Get Diff
 --
diff --git a/Documentation/user-named-destinations.txt b/Documentation/user-named-destinations.txt
new file mode 100644
index 0000000..1b6f143
--- /dev/null
+++ b/Documentation/user-named-destinations.txt
@@ -0,0 +1,32 @@
+= Gerrit Code Review - Named Destinations
+
+[[user-named-destinations]]
+== User Named Destinations
+It is possible to define named destination sets on a user level.
+To do this, define the named destination sets in files named after
+each destination set in the `destinations` directory of the user's
+account ref in the `All-Users` project.  The user's account ref is
+based on the user's account id which is an integer.  The account
+refs are sharded by the last two digits (`+nn+`) in the refname,
+leading to refs of the format `+refs/users/nn/accountid+`.  The
+user's destination files are a 2 column tab delimited file.  Each
+row in a destination file represents a single destination in the
+named set.  The left column represents the ref of the destination,
+and the right column represents the project of the destination.
+
+Example destination file named `destinations/myreviews`:
+
+----
+# Ref            	Project
+#
+refs/heads/master	gerrit
+refs/heads/stable-2.11	gerrit
+refs/heads/master	plugins/cookbook-plugin
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index 1015e0f..80b14eb 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -53,18 +53,6 @@
 +
 The change has all necessary approvals and may be submitted.
 
-- [[submitted-merge-pending]]`Submitted, Merge Pending`:
-+
-The change was submitted and was added to the merge queue.
-+
-The change stays in the merge queue if it depends on a change that is
-still in review. In this case it will get automatically merged when all
-dependency changes have been merged.
-+
-This status can also mean that the change depends on an abandoned
-change or on an outdated patch set of another change. In this case you
-may want to rebase the change.
-
 - [[merged]]`Merged`:
 +
 The change was successfully merged into the destination branch.
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index bb60a7e..adf77cf 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -88,6 +88,12 @@
 as a legacy numerical 'ID' such as 15183, or a newer style Change-Id
 that was scraped out of the commit message.
 
+[[destination]]
+destination:'NAME'::
++
+Changes which match the current user's destination named 'NAME'.
+(see link:user-named-destinations.html[Named Destinations]).
+
 [[owner]]
 owner:'USER', o:'USER'::
 +
@@ -283,7 +289,7 @@
 
 is:open, is:pending::
 +
-True if the change is either open or submitted, merge pending.
+True if the change is open.
 
 is:draft::
 +
@@ -306,8 +312,7 @@
 [[status]]
 status:open, status:pending::
 +
-True if the change state is either 'review in progress' or 'submitted,
-merge pending'.
+True if the change state is 'review in progress'.
 
 status:reviewed::
 +
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index d978c70..43ad799 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.gerrit.testutil.DisabledReviewDb;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Key;
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index 69a09f0..52cddf2 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GPGKEY;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.PageLinks;
@@ -35,6 +36,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -78,8 +80,7 @@
       while (userIds.hasNext()) {
         String userId = userIds.next();
         if (isAllowed(userId, allowedUserIds)) {
-          @SuppressWarnings("unchecked")
-          Iterator<PGPSignature> sigs = key.getSignaturesForID(userId);
+          Iterator<PGPSignature> sigs = getSignaturesForId(key, userId);
           while (sigs.hasNext()) {
             if (isValidCertification(key, sigs.next(), userId)) {
               return;
@@ -96,6 +97,14 @@
     }
   }
 
+  @SuppressWarnings("unchecked")
+  private Iterator<PGPSignature> getSignaturesForId(PGPPublicKey key,
+      String userId) {
+    return MoreObjects.firstNonNull(
+        key.getSignaturesForID(userId),
+        Collections.emptyIterator());
+  }
+
   private Set<String> getAllowedUserIds() {
     IdentifiedUser user = userProvider.get();
     Set<String> result = new HashSet<>();
diff --git a/gerrit-gwtui-common/BUCK b/gerrit-gwtui-common/BUCK
index 3977487..2a79db4 100644
--- a/gerrit-gwtui-common/BUCK
+++ b/gerrit-gwtui-common/BUCK
@@ -22,12 +22,6 @@
 
 java_library(
   name = 'client-lib',
-  exported_deps = [':client-lib2'],
-  visibility = ['PUBLIC'],
-)
-
-java_library(
-  name = 'client-lib2',
   srcs = glob(['src/main/**/*.java']),
   resources = glob(['src/main/**/*']),
   exported_deps = EXPORTED_DEPS,
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
index 5db620c..0250939 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -201,6 +201,10 @@
       this.auth = auth;
     }
 
+    public long getExpiresAt() {
+      return expiresAt;
+    }
+
     Account.Id getAccountId() {
       return accountId;
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 5028e4d3..9f94dbf 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -353,7 +353,7 @@
   private ThreadPool threadPool(Config cfg) {
     int maxThreads = cfg.getInt("httpd", null, "maxthreads", 25);
     int minThreads = cfg.getInt("httpd", null, "minthreads", 5);
-    int maxQueued = cfg.getInt("httpd", null, "maxqueued", 50);
+    int maxQueued = cfg.getInt("httpd", null, "maxqueued", 200);
     int idleTimeout = (int)MILLISECONDS.convert(60, SECONDS);
     int maxCapacity = maxQueued == 0
         ? Integer.MAX_VALUE
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
index 1632c82..1c70468 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -215,12 +215,11 @@
 
     if (secureStoreInitData != null && currentSecureStoreClassName != null
         && !currentSecureStoreClassName.equals(secureStoreInitData.className)) {
-      String err =
-          String.format(
-              "Different secure store was previously configured: %s. "
-              + "Use SwitchSecureStore program to switch between implementations.",
-              currentSecureStoreClassName);
-      die(err, new RuntimeException("secure store mismatch"));
+      String err = String.format(
+          "Different secure store was previously configured: %s. "
+          + "Use SwitchSecureStore program to switch between implementations.",
+          currentSecureStoreClassName);
+      throw die(err);
     }
 
     m.add(new InitModule(standalone, initDb));
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
index 31c3be1..064cc19 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
@@ -262,14 +262,16 @@
     }
   }
 
-  private List<File> getAllFiles(File dir, String extension) {
+  private List<File> getAllFiles(File dir, String extension)
+      throws IOException {
     ArrayList<File> fileList = new ArrayList<>();
     getAllFiles(dir, extension, fileList);
     return fileList;
   }
 
-  private void getAllFiles(File dir, String extension, List<File> fileList) {
-    for (File f : dir.listFiles()) {
+  private void getAllFiles(File dir, String extension, List<File> fileList)
+      throws IOException {
+    for (File f : listFiles(dir)) {
       if (f.getName().endsWith(extension)) {
         fileList.add(f);
       }
@@ -279,14 +281,16 @@
     }
   }
 
-  private List<String> getRelativePaths(File dir, String extension) {
+  private List<String> getRelativePaths(File dir, String extension)
+      throws IOException {
     ArrayList<String> pathList = new ArrayList<>();
     getRelativePaths(dir, extension, "", pathList);
     return pathList;
   }
 
-  private void getRelativePaths(File dir, String extension, String path, List<String> pathList) {
-    for (File f : dir.listFiles()) {
+  private static void getRelativePaths(File dir, String extension, String path,
+      List<String> pathList) throws IOException {
+    for (File f : listFiles(dir)) {
       if (f.getName().endsWith(extension)) {
         pathList.add(path + f.getName());
       }
@@ -296,8 +300,8 @@
     }
   }
 
-  private void deleteAllFiles(File dir) {
-    for (File f : dir.listFiles()) {
+  private static void deleteAllFiles(File dir) throws IOException {
+    for (File f : listFiles(dir)) {
       if (f.isDirectory()) {
         deleteAllFiles(f);
       } else {
@@ -306,4 +310,12 @@
     }
     dir.delete();
   }
+
+  private static File[] listFiles(File dir) throws IOException {
+    File[] files = dir.listFiles();
+    if (files == null) {
+      throw new IOException("Failed to list directory: " + dir);
+    }
+    return files;
+  }
 }
diff --git a/gerrit-plugin-gwtui/BUCK b/gerrit-plugin-gwtui/BUCK
index 132ade5..ec5903a 100644
--- a/gerrit-plugin-gwtui/BUCK
+++ b/gerrit-plugin-gwtui/BUCK
@@ -16,15 +16,9 @@
 
 java_library(
   name = 'gwtui-api-lib',
-  exported_deps = [':gwtui-api-lib2'],
-  visibility = ['PUBLIC'],
-)
-
-java_library(
-  name = 'gwtui-api-lib2',
   srcs = SRCS,
   resources = glob(['src/main/**/*']),
-  exported_deps = ['//gerrit-gwtui-common:client-lib2'],
+  exported_deps = ['//gerrit-gwtui-common:client-lib'],
   provided_deps = DEPS + ['//lib/gwt:dev'],
   visibility = ['PUBLIC'],
 )
@@ -62,7 +56,7 @@
     '//lib:gwtjsonrpc',
     '//lib:gwtorm_client',
     '//lib/gwt:dev__jar',
-    '//gerrit-gwtui-common:client-lib2',
+    '//gerrit-gwtui-common:client-lib',
     '//gerrit-common:client',
     '//gerrit-reviewdb:client',
   ],
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountDestinations.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
new file mode 100644
index 0000000..d928bec
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
@@ -0,0 +1,80 @@
+// 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.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.DestinationList;
+import com.google.gerrit.server.git.ValidationError;
+import com.google.gerrit.server.git.VersionedMetaData;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.FileMode;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/** Preferences for user accounts. */
+public class VersionedAccountDestinations extends VersionedMetaData {
+  private static final Logger log = LoggerFactory.getLogger(VersionedAccountDestinations.class);
+
+  public static VersionedAccountDestinations forUser(Account.Id id) {
+    return new VersionedAccountDestinations(RefNames.refsUsers(id));
+  }
+
+  private final String ref;
+  private final DestinationList destinations = new DestinationList();
+
+  private VersionedAccountDestinations(String ref) {
+    this.ref = ref;
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  public DestinationList getDestinationList() {
+    return destinations;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    String prefix = DestinationList.DIR_NAME + "/";
+    for (PathInfo p : getPathInfos(true)) {
+      if (p.fileMode == FileMode.REGULAR_FILE) {
+        String path = p.path;
+        if (path.startsWith(prefix)) {
+          String label = path.substring(prefix.length());
+          ValidationError.Sink errors = destinations.createLoggerSink(path, log);
+          destinations.parseLabel(label, readUTF8(path), errors);
+        }
+      }
+    }
+  }
+
+  public ValidationError.Sink createSink(String file) {
+    return ValidationError.createLoggerSink(file, log);
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException {
+    throw new UnsupportedOperationException("Cannot yet save destinations");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java
new file mode 100644
index 0000000..733ff0b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java
@@ -0,0 +1,52 @@
+// 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.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+
+public class DownloadContent implements RestReadView<FileResource> {
+  private final FileContentUtil fileContentUtil;
+
+  @Option(name = "--suffix")
+  private String suffix;
+
+  @Inject
+  DownloadContent(FileContentUtil fileContentUtil) {
+    this.fileContentUtil = fileContentUtil;
+  }
+
+  @Override
+  public BinaryResult apply(FileResource rsrc)
+      throws ResourceNotFoundException, IOException, NoSuchChangeException,
+      OrmException {
+    String path = rsrc.getPatchKey().get();
+    ProjectState projectState =
+        rsrc.getRevision().getControl().getProjectControl().getProjectState();
+    ObjectId revstr = ObjectId.fromString(
+        rsrc.getRevision().getPatchSet().getRevision().get());
+    return fileContentUtil.downloadContent(projectState, revstr, path, suffix);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
index 4336058..3027fd0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -16,6 +16,12 @@
 
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.PatchScript.FileMode;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -26,8 +32,11 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import eu.medsea.mimeutil.MimeType;
+
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -35,9 +44,13 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.NB;
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.Random;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 
 @Singleton
 public class FileContentUtil {
@@ -45,6 +58,8 @@
   private static final String X_GIT_SYMLINK = "x-git/symlink";
   private static final String X_GIT_GITLINK = "x-git/gitlink";
   private static final int MAX_SIZE = 5 << 20;
+  private static final String ZIP_TYPE = "application/zip";
+  private static final Random rng = new Random();
 
   private final GitRepositoryManager repoManager;
   private final FileTypeRegistry registry;
@@ -75,7 +90,7 @@
             .base64();
       }
 
-      final ObjectLoader obj = repo.open(id, OBJ_BLOB);
+      ObjectLoader obj = repo.open(id, OBJ_BLOB);
       byte[] raw;
       try {
         raw = obj.getCachedBytes(MAX_SIZE);
@@ -110,6 +125,133 @@
     return result;
   }
 
+  public BinaryResult downloadContent(ProjectState project, ObjectId revstr,
+      String path, @Nullable String suffix)
+          throws ResourceNotFoundException, IOException {
+    suffix = Strings.emptyToNull(CharMatcher.inRange('a', 'z')
+        .retainFrom(Strings.nullToEmpty(suffix)));
+
+    try (Repository repo = openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      RevCommit commit = rw.parseCommit(revstr);
+      ObjectReader reader = rw.getObjectReader();
+      TreeWalk tw = TreeWalk.forPath(reader, path, commit.getTree());
+      if (tw == null) {
+        throw new ResourceNotFoundException();
+      }
+
+      int mode = tw.getFileMode(0).getObjectType();
+      if (mode != Constants.OBJ_BLOB) {
+        throw new ResourceNotFoundException();
+      }
+
+      ObjectId id = tw.getObjectId(0);
+      ObjectLoader obj = repo.open(id, OBJ_BLOB);
+      byte[] raw;
+      try {
+        raw = obj.getCachedBytes(MAX_SIZE);
+      } catch (LargeObjectException e) {
+        raw = null;
+      }
+
+      MimeType contentType = registry.getMimeType(path, raw);
+      return registry.isSafeInline(contentType)
+          ? wrapBlob(path, obj, raw, contentType, suffix)
+          : zipBlob(path, obj, commit, suffix);
+    }
+  }
+
+  private BinaryResult wrapBlob(String path, final ObjectLoader obj, byte[] raw,
+      MimeType contentType, @Nullable String suffix) {
+    return asBinaryResult(raw, obj)
+        .setContentType(contentType.toString())
+        .setAttachmentName(safeFileName(path, suffix));
+  }
+
+  @SuppressWarnings("resource")
+  private BinaryResult zipBlob(final String path, final ObjectLoader obj,
+      RevCommit commit, final @Nullable String suffix) {
+    final String commitName = commit.getName();
+    final long when = commit.getCommitTime() * 1000L;
+    return new BinaryResult() {
+      @Override
+      public void writeTo(OutputStream os) throws IOException {
+        try (ZipOutputStream zipOut = new ZipOutputStream(os)) {
+          String decoration = randSuffix();
+          if (!Strings.isNullOrEmpty(suffix)) {
+            decoration = suffix + '-' + decoration;
+          }
+          ZipEntry e = new ZipEntry(safeFileName(path, decoration));
+          e.setComment(commitName + ":" + path);
+          e.setSize(obj.getSize());
+          e.setTime(when);
+          zipOut.putNextEntry(e);
+          obj.copyTo(zipOut);
+          zipOut.closeEntry();
+        }
+      }
+    }.setContentType(ZIP_TYPE)
+        .setAttachmentName(safeFileName(path, suffix) + ".zip")
+        .disableGzip();
+  }
+
+  private static String safeFileName(String fileName, @Nullable String suffix) {
+    // Convert a file path (e.g. "src/Init.c") to a safe file name with
+    // no meta-characters that might be unsafe on any given platform.
+    //
+    int slash = fileName.lastIndexOf('/');
+    if (slash >= 0) {
+      fileName = fileName.substring(slash + 1);
+    }
+
+    StringBuilder r = new StringBuilder(fileName.length());
+    for (int i = 0; i < fileName.length(); i++) {
+      final char c = fileName.charAt(i);
+      if (c == '_' || c == '-' || c == '.' || c == '@') {
+        r.append(c);
+      } else if ('0' <= c && c <= '9') {
+        r.append(c);
+      } else if ('A' <= c && c <= 'Z') {
+        r.append(c);
+      } else if ('a' <= c && c <= 'z') {
+        r.append(c);
+      } else if (c == ' ' || c == '\n' || c == '\r' || c == '\t') {
+        r.append('-');
+      } else {
+        r.append('_');
+      }
+    }
+    fileName = r.toString();
+
+    int ext = fileName.lastIndexOf('.');
+    if (suffix == null) {
+      return fileName;
+    } else if (ext <= 0) {
+      return fileName + "_" + suffix;
+    } else {
+      return fileName.substring(0, ext) + "_" + suffix
+          + fileName.substring(ext);
+    }
+  }
+
+  private static String randSuffix() {
+    // Produce a random suffix that is difficult (or nearly impossible)
+    // for an attacker to guess in advance. This reduces the risk that
+    // an attacker could upload a *.class file and have us send a ZIP
+    // that can be invoked through an applet tag in the victim's browser.
+    //
+    Hasher h = Hashing.md5().newHasher();
+    byte[] buf = new byte[8];
+
+    NB.encodeInt64(buf, 0, TimeUtil.nowMs());
+    h.putBytes(buf);
+
+    rng.nextBytes(buf);
+    h.putBytes(buf);
+
+    return h.hash().toString();
+  }
+
   public static String resolveContentType(ProjectState project, String path,
       FileMode fileMode, String mimeType) {
     switch (fileMode) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
index aeed0a6..a502a7d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
@@ -19,13 +19,12 @@
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.restapi.ETagView;
 import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.ChangeSet;
 import com.google.gerrit.server.git.MergeSuperSet;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Inject;
@@ -69,11 +68,9 @@
       rsrc.getChangeResource().prepareETag(h, user);
       h.putBoolean(Submit.wholeTopicEnabled(config));
       ReviewDb db = dbProvider.get();
-      ChangeSet cs = mergeSuperSet.completeChangeSet(db,
-          ChangeSet.create(rsrc.getChange()));
-      ProjectControl ctl = rsrc.getControl().getProjectControl();
-      for (Change c : cs.changes()) {
-        new ChangeResource(ctl.controlFor(c)).prepareETag(h, user);
+      ChangeSet cs = mergeSuperSet.completeChangeSet(db, rsrc.getChange());
+      for (ChangeData cd : cs.changes()) {
+        new ChangeResource(cd.changeControl()).prepareETag(h, user);
       }
     } catch (IOException | OrmException e) {
       throw new OrmRuntimeException(e);
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 ed2b835..99f8ba6 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
@@ -105,6 +105,7 @@
     put(FILE_KIND, "reviewed").to(PutReviewed.class);
     delete(FILE_KIND, "reviewed").to(DeleteReviewed.class);
     get(FILE_KIND, "content").to(GetContent.class);
+    get(FILE_KIND, "download").to(DownloadContent.class);
     get(FILE_KIND, "diff").to(GetDiff.class);
 
     child(CHANGE_KIND, "edit").to(ChangeEdits.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
index 093506c..29fa0cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.common.base.Splitter;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
@@ -41,6 +43,7 @@
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriterConfig;
 import org.apache.lucene.index.IndexWriterConfig.OpenMode;
+import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanClause.Occur;
 import org.apache.lucene.search.BooleanQuery;
@@ -124,8 +127,8 @@
     for (ScoreDoc h : hits) {
       Document doc = searcher.doc(h.doc);
 
-      AccountInfo info = new AccountInfo(
-          doc.getField(ID).numericValue().intValue());
+      IndexableField idField = checkNotNull(doc.getField(ID));
+      AccountInfo info = new AccountInfo(idField.numericValue().intValue());
       info.name = doc.get(NAME);
       info.email = doc.get(EMAIL);
       info.username = doc.get(USERNAME);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index c6048bb..ec8d70d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -182,11 +182,9 @@
           rsrc.getPatchSet().getRevision().get()));
     }
 
-    ChangeSet submittedChanges = ChangeSet.create(change);
-
     try {
       ReviewDb db = dbProvider.get();
-      mergeOpProvider.get().merge(db, submittedChanges, caller, true);
+      mergeOpProvider.get().merge(db, change, caller, true);
       change = db.changes().get(change.getId());
     } catch (NoSuchChangeException e) {
       throw new OrmException("Submission failed", e);
@@ -306,8 +304,7 @@
 
     ChangeSet cs;
     try {
-      cs = mergeSuperSet.completeChangeSet(db,
-          ChangeSet.create(cd.change()));
+      cs = mergeSuperSet.completeChangeSet(db, cd.change());
     } catch (OrmException | IOException e) {
       throw new OrmRuntimeException("Could not determine complete set of " +
           "changes to be submitted", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
index 1f6dfe4..a4f9fef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
@@ -60,8 +60,8 @@
       ResourceConflictException, Exception {
     try {
       ChangeSet cs = mergeSuperSet.completeChangeSet(dbProvider.get(),
-          ChangeSet.create(resource.getChange()));
-      if (cs.ids().size() > 1) {
+          resource.getChange());
+      if (cs.size() > 1) {
         return json.create(EnumSet.of(
             ListChangesOption.CURRENT_REVISION,
             ListChangesOption.CURRENT_COMMIT))
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index 5aef5bd..38d3188 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -235,6 +235,12 @@
         for (PatchSet p :
             db.patchSets().byRevision(a.getAncestorRevision())) {
           Change c = db.changes().get(p.getId().getParentKey());
+          if (c == null) {
+            log.error("Error while generating the ancestor change for"
+                + " revision " + a.getAncestorRevision() + ": Cannot find"
+                + " Change entry in database for " + p.getId().getParentKey());
+            continue;
+          }
           ca.dependsOn.add(newDependsOn(c, p));
         }
       }
@@ -255,6 +261,12 @@
             continue;
           }
           final Change c = db.changes().get(p.getId().getParentKey());
+          if (c == null) {
+            log.error("Error while generating the list of descendants for"
+                + " revision " + revId.get() + ": Cannot find Change entry in"
+                + " database for " + p.getId().getParentKey());
+            continue;
+          }
           ca.neededBy.add(newNeededBy(c, p));
         }
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
index 531db79..d4b0c4b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
@@ -14,70 +14,103 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.auto.value.AutoValue;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.SetMultimap;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
 
-/** A set of changes grouped together to be submitted atomically.*/
-@AutoValue
-public abstract class ChangeSet {
-  public static ChangeSet create(Iterable<Change> changes) {
-    ImmutableSet.Builder<Project.NameKey> pb = ImmutableSet.builder();
-    ImmutableSet.Builder<Branch.NameKey> bb = ImmutableSet.builder();
-    ImmutableSet.Builder<Change.Id> ib = ImmutableSet.builder();
-    ImmutableSet.Builder<PatchSet.Id> psb = ImmutableSet.builder();
-    ImmutableSetMultimap.Builder<Project.NameKey, Branch.NameKey> pbb =
-        ImmutableSetMultimap.builder();
-    ImmutableSetMultimap.Builder<Project.NameKey, Change.Id> pcb =
-        ImmutableSetMultimap.builder();
-    ImmutableSetMultimap.Builder<Branch.NameKey, Change.Id> cbb =
-        ImmutableSetMultimap.builder();
-    ImmutableSet.Builder<Change> cb = ImmutableSet.builder();
+import java.util.HashSet;
+import java.util.Set;
 
-    for (Change c : changes) {
-      Branch.NameKey branch = c.getDest();
-      Project.NameKey project = branch.getParentKey();
-      pb.add(project);
-      bb.add(branch);
-      ib.add(c.getId());
-      psb.add(c.currentPatchSetId());
-      pbb.put(project, branch);
-      pcb.put(project, c.getId());
-      cbb.put(branch, c.getId());
-      cb.add(c);
+/**
+ * A set of changes grouped together to be submitted atomically.
+ * <p>
+ * This class is not thread safe.
+ */
+public class ChangeSet {
+  private final ImmutableCollection<ChangeData> changeData;
+
+  public ChangeSet(Iterable<ChangeData> changes) {
+    Set<Change.Id> ids = new HashSet<>();
+    ImmutableSet.Builder<ChangeData> cdb = ImmutableSet.builder();
+    for (ChangeData cd : changes) {
+      if (ids.add(cd.getId())) {
+        cdb.add(cd);
+      }
     }
-
-    return new AutoValue_ChangeSet(pb.build(), bb.build(), ib.build(),
-        psb.build(), pbb.build(), pcb.build(), cbb.build(), cb.build());
+    changeData = cdb.build();
   }
 
-  public static ChangeSet create(Change change) {
-    return create(ImmutableList.of(change));
+  public ChangeSet(ChangeData change) {
+    this(ImmutableList.of(change));
   }
 
-  public abstract ImmutableSet<Project.NameKey> projects();
-  public abstract ImmutableSet<Branch.NameKey> branches();
-  public abstract ImmutableSet<Change.Id> ids();
-  public abstract ImmutableSet<PatchSet.Id> patchIds();
-  public abstract ImmutableSetMultimap<Project.NameKey, Branch.NameKey>
-      branchesByProject();
-  public abstract ImmutableSetMultimap<Project.NameKey, Change.Id>
-      changesByProject();
-  public abstract ImmutableSetMultimap<Branch.NameKey, Change.Id>
-      changesByBranch();
-  public abstract ImmutableSet<Change> changes();
+  public ImmutableSet<Change.Id> ids() {
+    ImmutableSet.Builder<Change.Id> ret = ImmutableSet.builder();
+    for (ChangeData cd : changeData) {
+      ret.add(cd.getId());
+    }
+    return ret.build();
+  }
 
-  @Override
-  public int hashCode() {
-    return ids().hashCode();
+  public Set<PatchSet.Id> patchIds() throws OrmException {
+    Set<PatchSet.Id> ret = new HashSet<>();
+    for (ChangeData cd : changeData) {
+      ret.add(cd.change().currentPatchSetId());
+    }
+    return ret;
+  }
+
+  public SetMultimap<Project.NameKey, Branch.NameKey> branchesByProject()
+      throws OrmException {
+    SetMultimap<Project.NameKey, Branch.NameKey> ret =
+        HashMultimap.create();
+    for (ChangeData cd : changeData) {
+      ret.put(cd.change().getProject(), cd.change().getDest());
+    }
+    return ret;
+  }
+
+  public Multimap<Project.NameKey, Change.Id> changesByProject()
+      throws OrmException {
+    ListMultimap<Project.NameKey, Change.Id> ret =
+        ArrayListMultimap.create();
+    for (ChangeData cd : changeData) {
+      ret.put(cd.change().getProject(), cd.getId());
+    }
+    return ret;
+  }
+
+  public Multimap<Branch.NameKey, Change.Id> changesByBranch()
+      throws OrmException {
+    ListMultimap<Branch.NameKey, Change.Id> ret =
+        ArrayListMultimap.create();
+    for (ChangeData cd : changeData) {
+      ret.put(cd.change().getDest(), cd.getId());
+    }
+    return ret;
+  }
+
+  public ImmutableCollection<ChangeData> changes() {
+    return changeData;
   }
 
   public int size() {
-    return ids().size();
+    return changeData.size();
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + ids();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java
new file mode 100644
index 0000000..ca1f705c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java
@@ -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.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+
+public class DestinationList extends TabFile {
+  public static final String DIR_NAME = "destinations";
+  private SetMultimap<String, Branch.NameKey> destinations = HashMultimap.create();
+
+  public Set<Branch.NameKey> getDestinations(String label) {
+    return destinations.get(label);
+  }
+
+  public void parseLabel(String label, String text,
+      ValidationError.Sink errors) throws IOException {
+    destinations.replaceValues(label,
+        toSet(parse(text, DIR_NAME + label, TRIM, null, errors)));
+  }
+
+  public String asText(String label) {
+    Set<Branch.NameKey> dests = destinations.get(label);
+    if (dests == null) {
+      return null;
+    }
+    List<Row> rows = Lists.newArrayListWithCapacity(dests.size());
+    for (Branch.NameKey dest : sort(dests)) {
+      rows.add(new Row(dest.get(), dest.getParentKey().get()));
+    }
+    return asText("Ref", "Project", rows);
+  }
+
+  protected static Set<Branch.NameKey> toSet(List<Row> destRows) {
+    Set<Branch.NameKey> dests = Sets.newHashSetWithExpectedSize(destRows.size());
+    for(Row row : destRows) {
+      dests.add(new Branch.NameKey(new Project.NameKey(row.right), row.left));
+    }
+    return dests;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
index d07572b..1477f6a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
@@ -36,7 +36,7 @@
 
   public static GroupList parse(String text, ValidationError.Sink errors)
       throws IOException {
-    List<Row> rows = parse(text, FILE_NAME, errors);
+    List<Row> rows = parse(text, FILE_NAME, TRIM, TRIM, errors);
     Map<AccountGroup.UUID, GroupReference> groupsByUUID =
         new HashMap<>(rows.size());
     for(Row row : rows) {
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 d945f77..3bfa47e 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
@@ -25,6 +25,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import com.google.common.collect.Table;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.TimeUtil;
@@ -331,14 +332,14 @@
     }
   }
 
-  public void merge(ReviewDb db, ChangeSet changes, IdentifiedUser caller,
+  public void merge(ReviewDb db, Change change, IdentifiedUser caller,
       boolean checkSubmitRules) throws NoSuchChangeException,
       OrmException, ResourceConflictException {
-    logPrefix = String.format("[%s]: ", String.valueOf(changes.hashCode()));
+    logPrefix = String.format("[%s]: ", String.valueOf(change.hashCode()));
     this.db = db;
-    logDebug("Beginning merge of {}", changes);
+    logDebug("Beginning integration of {}", change);
     try {
-      ChangeSet cs = mergeSuperSet.completeChangeSet(db, changes);
+      ChangeSet cs = mergeSuperSet.completeChangeSet(db, change);
       logDebug("Calculated to merge {}", cs);
       if (checkSubmitRules) {
         logDebug("Checking submit rules and state");
@@ -361,15 +362,17 @@
     logDebug("Beginning merge attempt on {}", cs);
     Map<Branch.NameKey, ListMultimap<SubmitType, ChangeData>> toSubmit =
         new HashMap<>();
+    logDebug("Perform the merges");
     try {
-      logDebug("Perform the merges");
-      for (Project.NameKey project : cs.projects()) {
+      Multimap<Project.NameKey, Branch.NameKey> br = cs.branchesByProject();
+      Multimap<Branch.NameKey, Change.Id> cbb = cs.changesByBranch();
+      for (Project.NameKey project : br.keySet()) {
         openRepository(project);
-        for (Branch.NameKey branch : cs.branchesByProject().get(project)) {
+        for (Branch.NameKey branch : br.get(project)) {
           setDestProject(branch);
 
           List<ChangeData> cds = new ArrayList<>();
-          for (Change.Id id : cs.changesByBranch().get(branch)) {
+          for (Change.Id id : cbb.get(branch)) {
             cds.add(changeDataFactory.create(db, id));
           }
           ListMultimap<SubmitType, ChangeData> submitting =
@@ -393,9 +396,9 @@
       }
       logDebug("Write out the new branch tips");
       SubmoduleOp subOp = subOpProvider.get();
-      for (Project.NameKey project : cs.projects()) {
+      for (Project.NameKey project : br.keySet()) {
         openRepository(project);
-        for (Branch.NameKey branch : cs.branchesByProject().get(project)) {
+        for (Branch.NameKey branch : br.get(project)) {
 
           RefUpdate update = updateBranch(branch);
           pendingRefUpdates.remove(branch);
@@ -413,7 +416,8 @@
         }
         closeRepository();
       }
-      updateSuperProjects(subOp, cs.branches());
+
+      updateSuperProjects(subOp, br.values());
       checkState(pendingRefUpdates.isEmpty(), "programmer error: "
           + "pending ref update list not emptied");
     } catch (NoSuchProjectException noProject) {
@@ -501,7 +505,7 @@
       if (branchUpdate.getOldObjectId() != null) {
         branchTip =
             (CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId());
-      } else if (repo.getFullBranch().equals(destBranch.get())) {
+      } else if (Objects.equals(repo.getFullBranch(), destBranch.get())) {
         branchTip = null;
         branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
       } else {
@@ -698,8 +702,12 @@
     CodeReviewCommit currentTip =
         mergeTip != null ? mergeTip.getCurrentTip() : null;
     if (Objects.equals(branchTip, currentTip)) {
-      logDebug("Branch already at merge tip {}, no update to perform",
-          currentTip.name());
+      if (currentTip != null) {
+        logDebug("Branch already at merge tip {}, no update to perform",
+            currentTip.name());
+      } else {
+        logDebug("Both branch and merge tip are nonexistent, no update");
+      }
       return null;
     } else if (currentTip == null) {
       logDebug("No merge tip, no update to perform");
@@ -906,7 +914,7 @@
   }
 
   private void updateSuperProjects(SubmoduleOp subOp,
-      Set<Branch.NameKey> branches) {
+      Collection<Branch.NameKey> branches) {
     logDebug("Updating superprojects");
     try {
       subOp.updateSuperProjects(db, branches);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
index 804417f..3add138 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -80,25 +81,27 @@
     this.repoManager = repoManager;
   }
 
-  public ChangeSet completeChangeSet(ReviewDb db, ChangeSet changes)
+  public ChangeSet completeChangeSet(ReviewDb db, Change change)
       throws MissingObjectException, IncorrectObjectTypeException, IOException,
       OrmException {
+    ChangeData cd = changeDataFactory.create(db, change.getId());
     if (Submit.wholeTopicEnabled(cfg)) {
-      return completeChangeSetIncludingTopics(db, changes);
+      return completeChangeSetIncludingTopics(db, new ChangeSet(cd));
     } else {
-      return completeChangeSetWithoutTopic(db, changes);
+      return completeChangeSetWithoutTopic(db, new ChangeSet(cd));
     }
   }
 
   private ChangeSet completeChangeSetWithoutTopic(ReviewDb db, ChangeSet changes)
       throws MissingObjectException, IncorrectObjectTypeException, IOException,
       OrmException {
-    List<Change> ret = new ArrayList<>();
+    List<ChangeData> ret = new ArrayList<>();
 
-    for (Project.NameKey project : changes.projects()) {
+    Multimap<Project.NameKey, Change.Id> pc = changes.changesByProject();
+    for (Project.NameKey project : pc.keySet()) {
       try (Repository repo = repoManager.openRepository(project);
            RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
-        for (Change.Id cId : changes.changesByProject().get(project)) {
+        for (Change.Id cId : pc.get(project)) {
           ChangeData cd = changeDataFactory.create(db, cId);
 
           SubmitTypeRecord r = new SubmitRuleEvaluator(cd).getSubmitType();
@@ -106,7 +109,7 @@
             logErrorAndThrow("Failed to get submit type for " + cd.getId());
           }
           if (r.type == SubmitType.CHERRY_PICK) {
-            ret.add(cd.change());
+            ret.add(cd);
             continue;
           }
 
@@ -137,17 +140,15 @@
             // Merged changes are ok to exclude
             Iterable<ChangeData> destChanges = queryProvider.get()
                 .byCommitsOnBranchNotMerged(cd.change().getDest(), hashes);
-
             for (ChangeData chd : destChanges) {
-              Change chg = chd.change();
-              ret.add(chg);
+              ret.add(chd);
             }
           }
         }
       }
     }
 
-    return ChangeSet.create(ret);
+    return new ChangeSet(ret);
   }
 
   private ChangeSet completeChangeSetIncludingTopics(
@@ -157,24 +158,18 @@
     boolean done = false;
     ChangeSet newCs = completeChangeSetWithoutTopic(db, changes);
     while (!done) {
-      List<Change> chgs = new ArrayList<>();
+      List<ChangeData> chgs = new ArrayList<>();
       done = true;
-      for (Change.Id cId : newCs.ids()) {
-        // TODO(sbeller): Cache the change data here and in completeChangeSet
-        // There is no need to reread it a few times.
-        ChangeData cd = changeDataFactory.create(db, cId);
-        chgs.add(cd.change());
-
+      for (ChangeData cd : newCs.changes()) {
+        chgs.add(cd);
         String topic = cd.change().getTopic();
         if (!Strings.isNullOrEmpty(topic) && !topicsTraversed.contains(topic)) {
-          for (ChangeData addCd : queryProvider.get().byTopicOpen(topic)) {
-            chgs.add(addCd.change());
-          }
+          chgs.addAll(queryProvider.get().byTopicOpen(topic));
           done = false;
           topicsTraversed.add(topic);
         }
       }
-      changes = ChangeSet.create(chgs);
+      changes = new ChangeSet(chgs);
       newCs = completeChangeSetWithoutTopic(db, changes);
     }
     return newCs;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/QueryList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueryList.java
index 0df866d..dffb18a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/QueryList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueryList.java
@@ -28,7 +28,7 @@
 
   public static QueryList parse(String text, ValidationError.Sink errors)
       throws IOException {
-    return new QueryList(parse(text, FILE_NAME, errors));
+    return new QueryList(parse(text, FILE_NAME, TRIM, TRIM, errors));
   }
 
   public String getQuery(String name) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 0ae77b0..9e5353a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -1787,31 +1787,28 @@
       throws OrmException, ResourceConflictException {
     Submit submit = submitProvider.get();
     RevisionResource rsrc = new RevisionResource(changes.parse(changeCtl), ps);
-    List<Change> changes = Lists.newArrayList(rsrc.getChange());
     try {
-      mergeOpProvider.get().merge(db, ChangeSet.create(changes),
+      mergeOpProvider.get().merge(db, rsrc.getChange(),
           (IdentifiedUser) changeCtl.getCurrentUser(), false);
     } catch (NoSuchChangeException e) {
       throw new OrmException(e);
     }
     addMessage("");
-    for (Change c : changes) {
-      c = db.changes().get(c.getId());
-      switch (c.getStatus()) {
-        case MERGED:
-          addMessage("Change " + c.getChangeId() + " merged.");
+    Change c = db.changes().get(rsrc.getChange().getId());
+    switch (c.getStatus()) {
+      case MERGED:
+        addMessage("Change " + c.getChangeId() + " merged.");
+        break;
+      case NEW:
+        ChangeMessage msg = submit.getConflictMessage(rsrc);
+        if (msg != null) {
+          addMessage("Change " + c.getChangeId() + ": " + msg.getMessage());
           break;
-        case NEW:
-          ChangeMessage msg = submit.getConflictMessage(rsrc);
-          if (msg != null) {
-            addMessage("Change " + c.getChangeId() + ": " + msg.getMessage());
-            break;
-          }
-          //$FALL-THROUGH$
-        default:
-          addMessage("change " + c.getChangeId() + " is "
-              + c.getStatus().name().toLowerCase());
-      }
+        }
+        //$FALL-THROUGH$
+      default:
+        addMessage("change " + c.getChangeId() + " is "
+            + c.getStatus().name().toLowerCase());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 8fe8379..7951fd3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -121,6 +121,10 @@
         RevWalk rw = new RevWalk(repo)) {
 
       ObjectId id = repo.resolve(destBranch.get());
+      if (id == null) {
+        logAndThrowSubmoduleException(
+            "Cannot resolve submodule destination branch " + destBranch);
+      }
       RevCommit commit = rw.parseCommit(id);
 
       Set<SubmoduleSubscription> oldSubscriptions =
@@ -180,7 +184,7 @@
   }
 
   protected void updateSuperProjects(ReviewDb db,
-      Set<Branch.NameKey> updatedBranches) throws SubmoduleException {
+      Collection<Branch.NameKey> updatedBranches) throws SubmoduleException {
     try {
       // These (repo/branch) will be updated later with all the given
       // individual submodule subscriptions
@@ -253,7 +257,7 @@
           }
 
           DirCacheEntry dce = dc.getEntry(s.getPath());
-          ObjectId oldId = null;
+          ObjectId oldId;
           if (dce != null) {
             if (!dce.getFileMode().equals(FileMode.GITLINK)) {
               log.error("Requested to update gitlink " + s.getPath() + " in "
@@ -283,10 +287,7 @@
 
             try {
               rw.markStart(newCommit);
-
-              if (oldId != null) {
-                rw.markUninteresting(rw.parseCommit(oldId));
-              }
+              rw.markUninteresting(rw.parseCommit(oldId));
               for (RevCommit c : rw) {
                 msgbuf.append(c.getFullMessage() + "\n\n");
               }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java
index 13d2b1e..87f9a23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java
@@ -27,6 +27,17 @@
 import java.util.Map;
 
 public class TabFile {
+  public interface Parser {
+    public String parse(String str);
+  }
+
+  public static Parser TRIM = new Parser() {
+        public String parse(String str) {
+           return str.trim();
+        }
+      };
+
+
   protected static class Row {
     public String left;
     public String right;
@@ -37,9 +48,9 @@
     }
   }
 
-  protected static List<Row> parse(String text, String filename,
-      ValidationError.Sink errors) throws IOException {
-    List<Row> rows = new ArrayList<>();
+  protected static List<Row> parse(String text, String filename, Parser left,
+      Parser right, ValidationError.Sink errors) throws IOException {
+    List<Row> rows = new ArrayList<Row>();
     BufferedReader br = new BufferedReader(new StringReader(text));
     String s;
     for (int lineNumber = 1; (s = br.readLine()) != null; lineNumber++) {
@@ -54,8 +65,15 @@
         continue;
       }
 
-      rows.add(new Row(s.substring(0, tab).trim(),
-          s.substring(tab + 1).trim()));
+      Row row = new Row(s.substring(0, tab), s.substring(tab + 1));
+      rows.add(row);
+
+      if (left != null) {
+        row.left = left.parse(row.left);
+      }
+      if (right != null) {
+        row.right = right.parse(row.right);
+      }
     }
     return rows;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index 17f51ef..dfde5d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.Lists;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
@@ -50,6 +51,7 @@
 import java.io.IOException;
 import java.io.StringReader;
 import java.util.Objects;
+import java.util.List;
 
 /**
  * Support for metadata stored within a version controlled branch.
@@ -59,6 +61,23 @@
  * later be written back to the repository.
  */
 public abstract class VersionedMetaData {
+  /**
+   * Path information that does not hold references to any repository
+   * data structures, allowing the application to retain this object
+   * for long periods of time.
+   */
+  public static class PathInfo {
+    public final FileMode fileMode;
+    public final String path;
+    public final ObjectId objectId;
+
+    protected PathInfo(TreeWalk tw) {
+      fileMode = tw.getFileMode(0);
+      path = tw.getPathString();
+      objectId = tw.getObjectId(0);
+    }
+  }
+
   private RevCommit revision;
   protected ObjectReader reader;
   protected ObjectInserter inserter;
@@ -439,6 +458,17 @@
     return null;
   }
 
+  public List<PathInfo> getPathInfos(boolean recursive) throws IOException {
+    TreeWalk tw = new TreeWalk(reader);
+    tw.addTree(revision.getTree());
+    tw.setRecursive(recursive);
+    List<PathInfo> paths = Lists.newArrayList();
+    while (tw.next()) {
+      paths.add(new PathInfo(tw));
+    }
+    return paths;
+  }
+
   protected static void set(Config rc, String section, String subsection,
       String name, String value) {
     if (value != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 88bb942..c6ffc27 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -34,6 +35,7 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.VersionedAccountQueries;
+import com.google.gerrit.server.account.VersionedAccountDestinations;
 import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
@@ -98,6 +100,7 @@
   public static final String FIELD_CONFLICTS = "conflicts";
   public static final String FIELD_DELETED = "deleted";
   public static final String FIELD_DELTA = "delta";
+  public static final String FIELD_DESTINATION = "destination";
   public static final String FIELD_DRAFTBY = "draftby";
   public static final String FIELD_EDITBY = "editby";
   public static final String FIELD_FILE = "file";
@@ -818,6 +821,28 @@
     return IsReviewedPredicate.create(args.getSchema(), parseAccount(who));
   }
 
+  @Operator
+  public Predicate<ChangeData> destination(String name)
+      throws QueryParseException {
+    AllUsersName allUsers = args.allUsersName.get();
+    try (Repository git = args.repoManager.openRepository(allUsers)) {
+      VersionedAccountDestinations d =
+          VersionedAccountDestinations.forUser(self());
+      d.load(git);
+      Set<Branch.NameKey> destinations =
+          d.getDestinationList().getDestinations(name);
+      if (destinations != null) {
+        return new DestinationPredicate(destinations, name);
+      }
+    } catch (RepositoryNotFoundException e) {
+      throw new QueryParseException("Unknown named destination (no " +
+          allUsers.get() +" repo): " + name, e);
+    } catch (IOException | ConfigInvalidException e) {
+      throw new QueryParseException("Error parsing named destination: " + name, e);
+    }
+    throw new QueryParseException("Unknown named destination: " + name);
+  }
+
   @Override
   protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
     if (query.startsWith("refs/")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
index 983e8ef..5184c53 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -28,7 +28,7 @@
   static FieldDef<ChangeData, ?> commitField(Schema<ChangeData> schema,
       String id) {
     if (id.length() == OBJECT_ID_STRING_LENGTH
-        && schema.hasField(EXACT_COMMIT)) {
+        && schema != null && schema.hasField(EXACT_COMMIT)) {
       return EXACT_COMMIT;
     }
     return COMMIT;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java
new file mode 100644
index 0000000..25fa09f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java
@@ -0,0 +1,45 @@
+// 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.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.server.OrmException;
+
+import java.util.Set;
+
+class DestinationPredicate extends OperatorPredicate<ChangeData> {
+  Set<Branch.NameKey> destinations;
+
+  DestinationPredicate(Set<Branch.NameKey> destinations, String value) {
+    super(ChangeQueryBuilder.FIELD_DESTINATION, value);
+    this.destinations = destinations;
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change change = object.change();
+    if (change == null) {
+      return false;
+    }
+    return destinations.contains(change.getDest());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 56d26e5..10c3db3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -128,7 +128,7 @@
   public Iterable<ChangeData> byCommitsOnBranchNotMerged(Branch.NameKey branch,
       List<String> hashes) throws OrmException {
     Schema<ChangeData> schema = schema(indexes);
-    if (schema.hasField(ChangeField.EXACT_COMMIT)) {
+    if (schema != null && schema.hasField(ChangeField.EXACT_COMMIT)) {
       return query(commitsOnBranchNotMerged(branch, commits(schema, hashes)));
     } else {
       return byCommitsOnBranchNotMerged(
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/DestinationListTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/DestinationListTest.java
new file mode 100644
index 0000000..2304ece
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/DestinationListTest.java
@@ -0,0 +1,164 @@
+// 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.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.replay;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Set;
+
+public class DestinationListTest extends TestCase {
+  public static final String R_FOO = "refs/heads/foo";
+  public static final String R_BAR = "refs/heads/bar";
+
+  public static final String P_MY = "myproject";
+  public static final String P_SLASH = "my/project/with/slashes";
+  public static final String P_COMPLEX = " a/project/with spaces and \ttabs ";
+
+  public static final String L_FOO = R_FOO + "\t" + P_MY + "\n";
+  public static final String L_BAR = R_BAR + "\t" + P_SLASH + "\n";
+  public static final String L_FOO_PAD_F = " " + R_FOO + "\t" + P_MY + "\n";
+  public static final String L_FOO_PAD_E = R_FOO + " \t" + P_MY + "\n";
+  public static final String L_COMPLEX = R_FOO + "\t" + P_COMPLEX + "\n";
+  public static final String L_BAD = R_FOO + "\n";
+
+  public static final String HEADER = "# Ref\tProject\n";
+  public static final String HEADER_PROPER = "# Ref         \tProject\n";
+  public static final String C1 = "# A Simple Comment\n";
+  public static final String C2 = "# Comment with a tab\t and multi # # #\n";
+
+  public static final String F_SIMPLE = L_FOO + L_BAR;
+  public static final String F_PROPER = L_BAR + L_FOO; // alpha order
+  public static final String F_PAD_F = L_FOO_PAD_F + L_BAR;
+  public static final String F_PAD_E = L_FOO_PAD_E + L_BAR;
+
+  public static final String LABEL = "label";
+  public static final String LABEL2 = "another";
+
+  public static final Branch.NameKey B_FOO = dest(P_MY, R_FOO);
+  public static final Branch.NameKey B_BAR = dest(P_SLASH, R_BAR);
+  public static final Branch.NameKey B_COMPLEX = dest(P_COMPLEX, R_FOO);
+
+  public static final Set<Branch.NameKey> D_SIMPLE = Sets.newHashSet();
+  static {
+    D_SIMPLE.clear();
+    D_SIMPLE.add(B_FOO);
+    D_SIMPLE.add(B_BAR);
+  }
+
+  private static Branch.NameKey dest(String project, String ref) {
+    return new Branch.NameKey(new Project.NameKey(project), ref);
+  }
+
+  @Test
+  public void testParseSimple() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, F_SIMPLE, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+  }
+
+  @Test
+  public void testParseWHeader() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, HEADER + F_SIMPLE, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+  }
+
+  @Test
+  public void testParseWComments() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, C1 + F_SIMPLE + C2, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+  }
+
+  @Test
+  public void testParseFooComment() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, "#" + L_FOO + L_BAR, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).doesNotContain(B_FOO);
+    assertThat(branches).contains(B_BAR);
+  }
+
+  @Test
+  public void testParsePaddedFronts() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, F_PAD_F, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+  }
+
+  @Test
+  public void testParsePaddedEnds() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, F_PAD_E, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+  }
+
+  @Test
+  public void testParseComplex() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, L_COMPLEX, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).contains(B_COMPLEX);
+  }
+
+  @Test(expected = IOException.class)
+  public void testParseBad() throws IOException {
+    ValidationError.Sink sink = createNiceMock(ValidationError.Sink.class);
+    replay(sink);
+    new DestinationList().parseLabel(LABEL, L_BAD, sink);
+  }
+
+  @Test
+  public void testParse2Labels() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, F_SIMPLE, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+
+    dl.parseLabel(LABEL2, L_COMPLEX, null);
+    branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+    branches = dl.getDestinations(LABEL2);
+    assertThat(branches).contains(B_COMPLEX);
+  }
+
+  @Test
+  public void testAsText() throws Exception {
+    String text = HEADER_PROPER + "#\n" + F_PROPER;
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, F_SIMPLE, null);
+    String asText = dl.asText(LABEL);
+    assertThat(text).isEqualTo(asText);
+
+    dl.parseLabel(LABEL2, asText, null);
+    assertThat(text).isEqualTo(dl.asText(LABEL2));
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 1372479..27c3443 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -62,6 +62,7 @@
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.DisabledReviewDb;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
 import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
@@ -123,6 +124,8 @@
   @Inject protected InternalChangeQuery internalChangeQuery;
   @Inject protected NotesMigration notesMigration;
   @Inject protected ProjectControl.GenericFactory projectControlFactory;
+  @Inject protected ChangeQueryBuilder queryBuilder;
+  @Inject protected QueryProcessor queryProcessor;
   @Inject protected SchemaCreator schemaCreator;
   @Inject protected ThreadLocalRequestContext requestContext;
 
@@ -1195,6 +1198,41 @@
     }
   }
 
+  @Test
+  public void prepopulatedFields() throws Exception {
+    assume().that(notesMigration.enabled()).isFalse();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change = newChange(repo, null, null, null, null).insert();
+
+    db = new DisabledReviewDb();
+    requestContext.setContext(newRequestContext(userId));
+    // Use QueryProcessor directly instead of API so we get ChangeDatas back.
+    List<ChangeData> cds = queryProcessor
+        .queryChanges(queryBuilder.parse(change.getId().toString()))
+        .changes();
+    assertThat(cds).hasSize(1);
+
+    ChangeData cd = cds.get(0);
+    cd.change();
+    cd.patchSets();
+    cd.currentApprovals();
+    cd.changedLines();
+    cd.reviewedBy();
+
+    // TODO(dborowitz): Swap out GitRepositoryManager somehow? Will probably be
+    // necessary for notedb anyway.
+    cd.isMergeable();
+
+    // Don't use ExpectedException since that wouldn't distinguish between
+    // failures here and on the previous calls.
+    try {
+      cd.messages();
+    } catch (AssertionError e) {
+      assertThat(e.getMessage()).isEqualTo(DisabledReviewDb.MESSAGE);
+    }
+  }
+
+
   protected ChangeInserter newChange(
       TestRepository<Repo> repo,
       @Nullable RevCommit commit, @Nullable String key, @Nullable Integer owner,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
index 79e326f..9bdd795 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
@@ -71,6 +71,13 @@
     // Ignore.
   }
 
+  @Override
+  @Ignore
+  @Test
+  public void prepopulatedFields() throws Exception {
+    // Ignore.
+  }
+
   @Test
   public void isReviewed() throws Exception {
     clockStepMs = MILLISECONDS.convert(2, MINUTES);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/DisabledReviewDb.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
similarity index 96%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/DisabledReviewDb.java
rename to gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
index 44d3d7f..d5444e3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/DisabledReviewDb.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance;
+package com.google.gerrit.testutil;
 
 import com.google.gerrit.reviewdb.server.AccountAccess;
 import com.google.gerrit.reviewdb.server.AccountDiffPreferenceAccess;
@@ -41,8 +41,8 @@
 import com.google.gwtorm.server.StatementExecutor;
 
 /** ReviewDb that is disabled for testing. */
-class DisabledReviewDb implements ReviewDb {
-  private static final String MESSAGE = "ReviewDb is disabled for this test";
+public class DisabledReviewDb implements ReviewDb {
+  public static final String MESSAGE = "ReviewDb is disabled for this test";
 
   @Override
   public void close() {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 379ee4b..c3dae3a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -162,7 +162,7 @@
 
       // Find out the object to get from the specified reference and paths
       ObjectId treeId = repo.resolve(options.treeIsh);
-      if (treeId.equals(ObjectId.zeroId())) {
+      if (treeId == null) {
         throw new Failure(4, "fatal: reference not found");
       }