Merge changes from topic 'group-audit-log'

* changes:
  Add new group screen that shows the audit log of the group
  Add integration test for listing group audit events
  Allow to get audit log of a group through GroupApi
  Add new REST endpoint that provides the audit log of a group
diff --git a/.buckconfig b/.buckconfig
index 20954d7..7b75225 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -27,4 +27,4 @@
 
 [cache]
   mode = dir
-  dir = ~/.gerritcodereview/buck-cache/cache
+  dir = ~/.gerritcodereview/buck-cache/locally-built-artifacts
diff --git a/Documentation/cmd-index-activate.txt b/Documentation/cmd-index-activate.txt
new file mode 100644
index 0000000..0135cfe
--- /dev/null
+++ b/Documentation/cmd-index-activate.txt
@@ -0,0 +1,32 @@
+= gerrit index activate
+
+== NAME
+gerrit index activate - Activate the latest index version available
+
+== SYNOPSIS
+--
+'ssh' -p @SSH_PORT@ @SSH_HOST@ 'gerrit index activate'
+--
+
+== DESCRIPTION
+Gerrit supports online index schema upgrades. When starting Gerrit for the first
+time after an upgrade that requires an index schema upgrade, the online indexer
+will be started. If the schema upgrade is a success, the new index will be
+activated and if it fails, a statement in the logs will be printed with the
+number of successfully/failed indexed changes.
+
+This command allows to activate the latest index even if there were some
+failures.
+
+== ACCESS
+Caller must be a member of the privileged 'Administrators' group.
+
+== SCRIPTING
+This command is intended to be used in scripts.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/cmd-index-start.txt b/Documentation/cmd-index-start.txt
new file mode 100644
index 0000000..4148b24
--- /dev/null
+++ b/Documentation/cmd-index-start.txt
@@ -0,0 +1,33 @@
+= gerrit index start
+
+== NAME
+gerrit index start - Start the online indexer
+
+== SYNOPSIS
+--
+'ssh' -p @SSH_PORT@ @SSH_HOST@ 'gerrit index start'
+--
+
+== DESCRIPTION
+Gerrit supports online index schema upgrades. When starting Gerrit for the first
+time after an upgrade that requires an index schema upgrade, the online indexer
+will be started. If the schema upgrade is a success, the new index will be
+activated and if it fails, a statement in the logs will be printed with the
+number of successfully/failed indexed changes.
+
+This command allows restarting the online indexer without having to restart
+Gerrit. This command will not start the indexer if it is already running or if
+the active index is the latest.
+
+== ACCESS
+Caller must be a member of the privileged 'Administrators' group.
+
+== SCRIPTING
+This command is intended to be used in scripts.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index f3bff7d..ef653cc 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -120,6 +120,12 @@
 link:cmd-gsql.html[gerrit gsql]::
 	Administrative interface to active database.
 
+link:cmd-index-index.html[gerrit index activate]::
+	Activate the latest index version available.
+
+link:cmd-index-start.html[gerrit index start]::
+	Start the online indexer.
+
 link:cmd-logging-ls-level.html[gerrit logging ls-level]::
 	List loggers and their logging level.
 
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index ac33778..f95d9d1 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1525,6 +1525,31 @@
 If `download.scheme` is not specified, SSH, HTTP and Anonymous HTTP
 downloads are allowed.
 
+[[download.checkForHiddenChangeRefs]]download.checkForHiddenChangeRefs::
++
+Whether the download commands should be adapted when the change refs
+are hidden.
++
+Git has a configuration option to hide refs from the initial
+advertisement (`uploadpack.hideRefs`). This option can be used to hide
+the change refs from the client. As consequence fetching changes by
+change ref does not work anymore. However by setting
+`uploadpack.allowTipSha1InWant` to `true` fetching changes by commit ID
+is possible. If `download.checkForHiddenChangeRefs` is set to `true`
+the git download commands use the commit ID instead of the change ref
+when a project is configured like this.
++
+Example git configuration on a project:
++
+----
+[uploadpack]
+  hideRefs = refs/changes/
+  hideRefs = refs/cache-automerge/
+  allowTipSha1InWant = true
+----
++
+By default `false`.
+
 [[download.archive]]download.archive::
 +
 Specifies which archive formats, if any, should be offered on the change
@@ -1670,6 +1695,18 @@
 by the system administrator, and might not even be running on the
 same host as Gerrit.
 
+[[gerrit.docUrl]]gerrit.docUrl::
++
+Optional base URL for documentation, under which one can find
+"index.html", "rest-api.html", etc. Used as the base for the fixed set
+of links in the "Documentation" tab. A slash is implicitly appended.
+(For finer control over the top menu, consider writing a
+link:dev-plugins.html#top-menu-extensions[plugin].)
++
+If unset or empty, the documentation tab will only be shown if
+`/Documentation/index.html` can be reached by the browser at app load
+time.
+
 [[gerrit.installCommitMsgHookCommand]]gerrit.installCommitMsgHookCommand::
 +
 Optional command to install the `commit-msg` hook. Typically of the
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index 220b579..aedc0b4 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -118,9 +118,12 @@
 these plugins.
 
 The Gerrit Project doesn't provide binaries for these plugins, but
-there are some public services, like the
-link:https://ci.gerritforge.com/[CI Server from GerritForge], that
-offer the download of ready plugin jars.
+there are some public services that offer the download of pre-built
+plugin jars:
+
+* link:https://ci.gerritforge.com/[CI Server from GerritForge]
+* link:http://builds.quelltextlich.at/gerrit/nightly/index.html[
+  CI Server from Quelltextlich]
 
 The following list gives an overview about available plugins, but the
 list may not be complete. You may discover more plugins on
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index e307391..de3e6de 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -510,7 +510,7 @@
 === Cleaning The Buck Cache
 
 The cache for the Gerrit Code Review project is located in
-`~/.gerritcodereview/buck-cache/cache`.
+`~/.gerritcodereview/buck-cache/locally-built-artifacts`.
 
 The Buck cache should never need to be manually deleted. If you find yourself
 deleting the Buck cache regularly, then it is likely that there is something
@@ -519,11 +519,12 @@
 If you really do need to clean the cache manually, then:
 
 ----
- rm -rf ~/.gerritcodereview/buck-cache/cache
+ rm -rf ~/.gerritcodereview/buck-cache/locally-built-artifacts
 ----
 
-Note that the root `buck-cache` folder should not be deleted as this is where
-downloaded artifacts are stored.
+Note that the root `buck-cache` folder should not be deleted as it also contains
+the `downloaded-artifacts` directory, which holds the artifacts that got
+downloaded (not built locally).
 
 [[buck-daemon]]
 === Using Buck daemon
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 6abfb9e..3157214 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -222,10 +222,6 @@
 link:https://oss.sonatype.org/[Sonatype Nexus Server].
 
 * Verify the staging repository
-+
-How to do this is described in the
-link:https://docs.sonatype.org/display/Repository/Sonatype+OSS+Maven+Repository+Usage+Guide#SonatypeOSSMavenRepositoryUsageGuide-8.a.1.ClosingaStagingRepository[
-Sonatype OSS Maven Repository Usage Guide].
 
 ** Go to the link:https://oss.sonatype.org/[Sonatype Nexus Server] and
 sign in with your Sonatype credentials.
@@ -354,13 +350,10 @@
 gerrit-documentation] storage bucket.
 
 [[update-links]]
-==== Update Google Code project links
+==== Update homepage links
 
-* Go to http://code.google.com/p/gerrit/admin
-* Update the documentation link in the `Resources` section of the
-Description text, and in the `Links` section.
-* Add a link to the new release notes in the `News` section of the
-Description text
+Upload a change on the link:https://gerrit-review.googlesource.com/#/admin/projects/homepage[
+homepage project] to change the version numbers to the new version.
 
 [[update-issues]]
 ==== Update the Issues
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 8ccd03b..32fa472 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -43,9 +43,6 @@
 
   DRAFT;; Change is a draft change that only consists of draft patchsets.
 
-  SUBMITTED;; Change has been submitted and is in the merge queue.
-  It may be waiting for one or more dependencies.
-
   MERGED;; Change has been merged to its branch.
 
   ABANDONED;; Change was abandoned by its owner or administrator.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 0384993..4224c76 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3756,8 +3756,7 @@
 |`subject`            ||
 The subject of the change (header line of the commit message).
 |`status`             ||
-The status of the change (`NEW`, `SUBMITTED`, `MERGED`, `ABANDONED`,
-`DRAFT`).
+The status of the change (`NEW`, `MERGED`, `ABANDONED`, `DRAFT`).
 |`created`            ||
 The link:rest-api.html#timestamp[timestamp] of when the change was
 created.
@@ -4322,7 +4321,7 @@
 |`_revision_number`        |optional|The revision number.
 |`_current_revision_number`|optional|The current revision number.
 |`status`                  |optional|The status of the change. The status of
-the change is one of (`NEW`, `SUBMITTED`, `MERGED`, `ABANDONED`, `DRAFT`).
+the change is one of (`NEW`, `MERGED`, `ABANDONED`, `DRAFT`).
 |===========================
 
 [[related-changes-info]]
@@ -4530,8 +4529,8 @@
 |==========================
 |Field Name    ||Description
 |`status`      ||
-The status of the change after submitting, can be `MERGED` or
-`SUBMITTED`.+
+The status of the change after submitting is `MERGED`.
++
 As `wait_for_merge` in the link:#submit-input[SubmitInput] is deprecated and
 the request always waits for the merge to be completed, you can expect
 `MERGED` to be returned here.
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 5a6c032..6762411 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1163,6 +1163,10 @@
 |`all_users_name`    ||
 Name of the link:config-gerrit.html#gerrit.allUsers[project in which
 meta data of all users is stored].
+|`doc_url`           |optional|
+Custom base URL where Gerrit server documentation is located.
+(Documentation may still be available at /Documentation relative to the
+Gerrit base path even if this value is unset.)
 |`report_bug_url`    |optional|
 link:config-gerrit.html#gerrit.reportBugUrl[URL to report bugs].
 |`report_bug_text`   |optional, not set if default|
diff --git a/README.md b/README.md
index b693e01..573042d 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,6 @@
 
 ## Events
 
-Next developer conference is September 2015, Berlin.
-Next user conference is November 2015, MV, CA. Registration form is
-[here](http://goo.gl/forms/fifi2YQTc7).
+- November 7-8 2015: Gerrit User Conference, Mountain View. ([Register](http://goo.gl/forms/fifi2YQTc7)).
+- November 9-13 2015: Gerrit Hackathon, Mountain View. (Invitation Only).
+- March 2016: Gerrit Hackathon, Berlin. (Details to be confirmed).
diff --git a/ReleaseNotes/ReleaseNotes-2.11.2.txt b/ReleaseNotes/ReleaseNotes-2.11.2.txt
new file mode 100644
index 0000000..1e411e4
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.11.2.txt
@@ -0,0 +1,97 @@
+Release notes for Gerrit 2.11.2
+===============================
+
+Gerrit 2.11.2 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.11.2.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.11.2.war]
+
+Gerrit 2.11.2 includes the bug fixes done with
+link:ReleaseNotes-2.10.6.html[Gerrit 2.10.6]. These bug fixes are *not* listed
+in these release notes.
+
+There are no schema changes from link:ReleaseNotes-2.11.1.html[2.11.1].
+
+New Features
+------------
+
+New SSH commands:
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11.2/cmd-index-start.html[
+`index start`]
++
+Allows to restart the online indexer without restarting the Gerrit server.
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11.2/cmd-index-activate.html[
+`index activate`]
++
+Allows to activate the latest index version even if the indexing encountered
+problems.
+
+
+Bug Fixes
+---------
+
+* link:link:https://code.google.com/p/gerrit/issues/detail?id=3460[Issue 3460]:
+Fix regression in the search box auto-suggestions.
++
+A change introduced in version 2.11 caused the auto-suggestions to not work
+any more.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3355[Issue 3355]:
+Fix corruption of database when deleting draft change ref fails.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3426[Issue 3426]:
+Fix regression in the `%base` option.
++
+A change introduced in version 2.11 caused the `%base` option to not work
+any more, meaning it was not possible to push a commit, which is already merged
+into a branch, for review to another branch of the same project.
+
+* link:https://bugs.eclipse.org/bugs/show_bug.cgi?id=468024[JGit bug 468024]:
+Fix data loss if a pack is pushed to a JGit based server and gc runs
+concurrently on the same repository.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3371[Issue 3371]:
+Fix wrong date/time for commits in `refs/meta/config` branch.
++
+When the `refs/meta/config` branch was modified using the PutConfig REST endpoint
+(e.g. when changing the project configuration in the web UI) the commit date/time
+was wrong. Instead of the actual date/time the date/time of the last Gerrit server
+start was used.
+
+* Fix NullPointerException in the 'related changes' REST API endpoint.
+
+* Make sure `/a` is not in the project name for git-over-http requests.
++
+The `/a` prefix is used to trigger authentication but was not removed from the
+request. Therefore, it was included in the project name and hence the project
+wasn't found when performing, for example `git fetch http://server/a/project`.
+
+* Fix disabling of git ssh commands.
++
+The ssh commands were available even when ssh commands were disabled.
+
+* Fix native string handling in Plugin API.
++
+The results of REST API calls were incorrectly being converted from NativeString
+to String when called from Javascript.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3440[Issue 3440]:
+Include prettify source files in the documentation.
++
+The prettify source files were being loaded from `cdnjs.cloudflare.com`, which
+may cause trouble if the Gerrit instance is behind a firewall on a machine not
+allowed to access the Internet at large.
++
+Now those files are bundled with the documentation.
+
+* Print proper name for project indexer tasks in `show-queue` command.
+
+* Print proper name for reindex after update tasks in `show-queue` command.
+
+Updates
+-------
+
+* Update JGit to 4.0.1.201506240215-r.
+
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 773fdd2..735bc0d 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -4,6 +4,7 @@
 [[2_11]]
 Version 2.11.x
 --------------
+* link:ReleaseNotes-2.11.2.html[2.11.2]
 * link:ReleaseNotes-2.11.1.html[2.11.1]
 * link:ReleaseNotes-2.11.html[2.11]
 
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py
index f118346..32edf84 100755
--- a/contrib/abandon_stale.py
+++ b/contrib/abandon_stale.py
@@ -101,7 +101,7 @@
         logging.error("Gerrit URL is required")
         return 1
 
-    pattern = re.compile(r"^([\d]+)(months|years)")
+    pattern = re.compile(r"^([\d]+)(month[s]?|year[s]?|week[s]?)")
     match = pattern.match(options.age)
     if not match:
         logging.error("Invalid age: %s", options.age)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 8ac8a88..a4cbe72 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.account.GroupCache;
@@ -67,6 +68,7 @@
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.Transport;
 import org.junit.AfterClass;
@@ -148,6 +150,10 @@
   @Inject
   private Provider<AnonymousUser> anonymousUser;
 
+  @Inject
+  @GerritPersonIdent
+  protected Provider<PersonIdent> serverIdent;
+
   protected TestRepository<InMemoryRepository> testRepo;
   protected GerritServer server;
   protected TestAccount admin;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
index e7b5834..4a6d22d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
@@ -79,7 +79,7 @@
   }
 
   public PersonIdent getIdent() {
-    return new PersonIdent(username, email);
+    return new PersonIdent(fullName, email);
   }
 
   public String getHttpUrl(GerritServer server) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 201d940..23e3685 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -123,7 +123,6 @@
         .review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
     in.onBehalfOf = admin2.email;
-    in.waitForMerge = true;
     gApi.changes()
         .id(project.get() + "~master~" + r.getChangeId())
         .current()
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index 8bf4511..2dbbb16 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -29,6 +29,8 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.RefSpec;
 
+import java.util.concurrent.atomic.AtomicInteger;
+
 public abstract class AbstractSubmoduleSubscription extends AbstractDaemonTest {
   protected TestRepository<?> createProjectWithPush(String name)
       throws Exception {
@@ -38,25 +40,54 @@
     return cloneProject(project);
   }
 
-  protected void createSubscription(
-      TestRepository<?> repo, String branch, String subscribeToRepo,
-      String subscribeToBranch) throws Exception {
-    subscribeToRepo = name(subscribeToRepo);
+  private static AtomicInteger contentCounter = new AtomicInteger(0);
 
+  protected ObjectId pushChangeTo(TestRepository<?> repo, String ref,
+      String message, String topic) throws Exception {
+    ObjectId ret = repo.branch("HEAD").commit().insertChangeId()
+      .message(message)
+      .add("a.txt", "a contents: " + contentCounter.incrementAndGet())
+      .create();
+    String refspec = "HEAD:" + ref;
+    if (!topic.isEmpty()) {
+      refspec += "/" + topic;
+    }
+    repo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec(refspec)).call();
+    return ret;
+  }
+
+  protected ObjectId pushChangeTo(TestRepository<?> repo, String branch)
+      throws Exception {
+    return pushChangeTo(repo, "refs/heads/" + branch, "some change", "");
+  }
+
+  protected void createSubscription(TestRepository<?> repo, String branch,
+      String subscribeToRepo, String subscribeToBranch) throws Exception {
+    Config config = new Config();
+    prepareSubscriptionConfigEntry(config, subscribeToRepo, subscribeToBranch);
+    pushSubscriptionConfig(repo, branch, config);
+  }
+
+  protected void prepareSubscriptionConfigEntry(Config config,
+      String subscribeToRepo, String subscribeToBranch) {
+    subscribeToRepo = name(subscribeToRepo);
     // The submodule subscription module checks for gerrit.canonicalWebUrl to
     // detect if it's configured for automatic updates. It doesn't matter if
     // it serves from that URL.
     String url = cfg.getString("gerrit", null, "canonicalWebUrl") + "/"
         + subscribeToRepo;
+    config.setString("submodule", subscribeToRepo, "path", subscribeToRepo);
+    config.setString("submodule", subscribeToRepo, "url", url);
+    config.setString("submodule", subscribeToRepo, "branch", subscribeToBranch);
+  }
 
-    Config cfg = new Config();
-    cfg.setString("submodule", subscribeToRepo, "path", subscribeToRepo);
-    cfg.setString("submodule", subscribeToRepo, "url", url);
-    cfg.setString("submodule", subscribeToRepo, "branch", subscribeToBranch);
+  protected void pushSubscriptionConfig(TestRepository<?> repo,
+      String branch, Config config) throws Exception {
 
     repo.branch("HEAD").commit().insertChangeId()
       .message("subject: adding new subscription")
-      .add(".gitmodules", cfg.toText().toString())
+      .add(".gitmodules", config.toText().toString())
       .create();
 
     repo.git().push().setRemote("origin").setRefSpecs(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index 1f0c601..9a8bb51 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -26,14 +26,12 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.git.CommitMergeStatus;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -53,10 +51,6 @@
   @Inject
   private ChangeNotes.Factory changeNotesFactory;
 
-  @Inject
-  @GerritPersonIdent
-  private PersonIdent serverIdent;
-
   @Test
   public void submitOnPush() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
@@ -124,7 +118,7 @@
     PushOneCommit.Result r =
         push("refs/for/master%submit", "other change", "a.txt", "other content");
     r.assertErrorStatus();
-    r.assertChange(Change.Status.NEW, null, admin);
+    r.assertChange(Change.Status.NEW, null);
     r.assertMessage(CommitMergeStatus.PATH_CONFLICT.getMessage());
   }
 
@@ -257,7 +251,7 @@
       assertThat(c.getShortMessage()).isEqualTo("Merge \"" + subject + "\"");
       assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email);
       assertThat(c.getCommitterIdent().getEmailAddress()).isEqualTo(
-          serverIdent.getEmailAddress());
+          serverIdent.get().getEmailAddress());
     }
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index 3ffe07c..ad63e3c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -24,8 +24,6 @@
 import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Test;
 
-import java.util.concurrent.atomic.AtomicInteger;
-
 public class SubmoduleSubscriptionsIT extends AbstractSubmoduleSubscription {
 
   @Test
@@ -52,6 +50,34 @@
   }
 
   @Test
+  public void testSubmoduleCommitMessage() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    pushChangeTo(subRepo, "master");
+    createSubscription(superRepo, "master", "subscribed-to-project", "master");
+    ObjectId subHEAD = pushChangeTo(subRepo, "master");
+
+    // The first update doesn't include the rev log
+    RevWalk rw = subRepo.getRevWalk();
+    RevCommit subCommitMsg = rw.parseCommit(subHEAD);
+    expectToHaveCommitMessage(superRepo, "master",
+        "Updated git submodules\n\n" +
+        "Project: " + name("subscribed-to-project")
+            + " master " + subHEAD.name() + "\n\n");
+
+    // The next commit should generate only its commit message,
+    // omitting previous commit logs
+    subHEAD = pushChangeTo(subRepo, "master");
+    subCommitMsg = rw.parseCommit(subHEAD);
+    expectToHaveCommitMessage(superRepo, "master",
+        "Updated git submodules\n\n" +
+        "Project: " + name("subscribed-to-project")
+            + " master " + subHEAD.name() + "\n\n" +
+        subCommitMsg.getFullMessage() + "\n\n");
+  }
+
+  @Test
   public void testSubscriptionUnsubscribe() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
@@ -66,8 +92,10 @@
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEADbeforeUnsubscribing);
 
-    pushChangeTo(superRepo, "master", "commit after unsubscribe");
-    pushChangeTo(subRepo, "master", "commit after unsubscribe");
+    pushChangeTo(superRepo, "refs/heads/master",
+        "commit after unsubscribe", "");
+    pushChangeTo(subRepo, "refs/heads/master",
+        "commit after unsubscribe", "");
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEADbeforeUnsubscribing);
   }
@@ -87,8 +115,10 @@
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEADbeforeUnsubscribing);
 
-    pushChangeTo(superRepo, "master", "commit after unsubscribe");
-    pushChangeTo(subRepo, "master", "commit after unsubscribe");
+    pushChangeTo(superRepo, "refs/heads/master",
+        "commit after unsubscribe", "");
+    pushChangeTo(subRepo, "refs/heads/master",
+        "commit after unsubscribe", "");
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEADbeforeUnsubscribing);
   }
@@ -124,27 +154,6 @@
     assertThat(hasSubmodule(subRepo, "master", "super-project")).isFalse();
   }
 
-  private static AtomicInteger contentCounter = new AtomicInteger(0);
-
-  private ObjectId pushChangeTo(TestRepository<?> repo, String branch, String message)
-      throws Exception {
-
-    ObjectId ret = repo.branch("HEAD").commit().insertChangeId()
-      .message(message)
-      .add("a.txt", "a contents: " + contentCounter.addAndGet(1))
-      .create();
-
-    repo.git().push().setRemote("origin").setRefSpecs(
-        new RefSpec("HEAD:refs/heads/" + branch)).call();
-
-    return ret;
-  }
-
-  private ObjectId pushChangeTo(TestRepository<?> repo, String branch)
-      throws Exception {
-    return pushChangeTo(repo, branch, "some change");
-  }
-
   private void deleteAllSubscriptions(TestRepository<?> repo, String branch)
       throws Exception {
     repo.git().fetch().setRemote("origin").call();
@@ -198,4 +207,14 @@
     }
   }
 
+  private void expectToHaveCommitMessage(TestRepository<?> repo,
+      String branch, String expectedMessage) throws Exception {
+
+    ObjectId commitId = repo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/" + branch).getObjectId();
+
+    RevWalk rw = repo.getRevWalk();
+    RevCommit c = rw.parseCommit(commitId);
+    assertThat(c.getFullMessage()).isEqualTo(expectedMessage);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 7823e7d..086c205 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.acceptance.git;
 
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.GitUtil.getChangeId;
 
 import com.google.gerrit.acceptance.NoHttpd;
@@ -87,4 +88,45 @@
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subRepoId);
   }
+
+  @Test
+  public void testUpdateManySubmodules() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> sub1 = createProjectWithPush("sub1");
+    TestRepository<?> sub2 = createProjectWithPush("sub2");
+    TestRepository<?> sub3 = createProjectWithPush("sub3");
+
+    Config config = new Config();
+    prepareSubscriptionConfigEntry(config, "sub1", "master");
+    prepareSubscriptionConfigEntry(config, "sub2", "master");
+    prepareSubscriptionConfigEntry(config, "sub3", "master");
+    pushSubscriptionConfig(superRepo, "master", config);
+
+    ObjectId superPreviousId = pushChangeTo(superRepo, "master");
+
+    ObjectId sub1Id = pushChangeTo(sub1, "refs/for/master",
+        "some message", "same-topic");
+    ObjectId sub2Id = pushChangeTo(sub2, "refs/for/master",
+        "some message", "same-topic");
+    ObjectId sub3Id = pushChangeTo(sub3, "refs/for/master",
+        "some message", "same-topic");
+
+    approve(getChangeId(sub1, sub1Id).get());
+    approve(getChangeId(sub2, sub2Id).get());
+    approve(getChangeId(sub3, sub3Id).get());
+
+    gApi.changes().id(getChangeId(sub1, sub1Id).get()).current().submit();
+
+    expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1Id);
+    expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2Id);
+    expectToHaveSubmoduleState(superRepo, "master", "sub3", sub3Id);
+
+    superRepo.git().fetch().setRemote("origin").call()
+      .getAdvertisedRef("refs/heads/master").getObjectId();
+
+    assertWithMessage("submodule subscription update "
+        + "should have made one commit")
+        .that(superRepo.getRepository().resolve("origin/master^"))
+        .isEqualTo(superPreviousId);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 666be73..6fcdbce 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -41,7 +41,6 @@
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
@@ -61,6 +60,7 @@
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -71,8 +71,6 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -145,9 +143,11 @@
     change1.assertChange(Change.Status.MERGED, "test-topic", admin);
     change2.assertChange(Change.Status.MERGED, "test-topic", admin);
     change3.assertChange(Change.Status.MERGED, "test-topic", admin);
+    // Check for the exact change to have the correct submitter.
+    assertSubmitter(change3);
+    // Also check submitters for changes submitted via the topic relationship.
     assertSubmitter(change1);
     assertSubmitter(change2);
-    assertSubmitter(change3);
   }
 
   private void assertSubmitter(PushOneCommit.Result change) throws Exception {
@@ -202,26 +202,9 @@
     submit(changeId, HttpStatus.SC_CONFLICT);
   }
 
-  protected void submitStatusOnly(String changeId) throws Exception {
-    approve(changeId);
-    Change c = queryProvider.get().byKeyPrefix(changeId).get(0).change();
-    c.setStatus(Change.Status.SUBMITTED);
-    db.changes().update(Collections.singleton(c));
-    db.patchSetApprovals().insert(Collections.singleton(
-        new PatchSetApproval(
-            new PatchSetApproval.Key(
-                c.currentPatchSetId(),
-                admin.id,
-                LabelId.SUBMIT),
-            (short) 1,
-            new Timestamp(System.currentTimeMillis()))));
-    indexer.index(db, c);
-  }
-
   private void submit(String changeId, int expectedStatus) throws Exception {
     approve(changeId);
     SubmitInput subm = new SubmitInput();
-    subm.waitForMerge = false;
     RestResponse r =
         adminSession.post("/changes/" + changeId + "/submit", subm);
     assertThat(r.getStatusCode()).isEqualTo(expectedStatus);
@@ -246,8 +229,10 @@
       BranchInfo branch =
           newGson().fromJson(b.getReader(),
               new TypeToken<BranchInfo>() {}.getType());
-      assertThat(branch.revision).isEqualTo(
-          mergeResults.get(Integer.toString(change._number)));
+      assertThat(mergeResults).isNotEmpty();
+      String newRev = mergeResults.get(Integer.toString(change._number));
+      assertThat(newRev).isNotNull();
+      assertThat(branch.revision).isEqualTo(newRev);
     }
     b.consume();
   }
@@ -266,6 +251,10 @@
     }
   }
 
+  protected void assertNew(String changeId) throws Exception {
+    assertThat(get(changeId).status).isEqualTo(ChangeStatus.NEW);
+  }
+
   protected void assertApproved(String changeId) throws Exception {
     ChangeInfo c = get(changeId, DETAILED_LABELS);
     LabelInfo cr = c.labels.get("Code-Review");
@@ -274,6 +263,16 @@
     assertThat(new Account.Id(cr.all.get(0)._accountId)).isEqualTo(admin.getId());
   }
 
+  protected void assertPersonEquals(PersonIdent expected,
+      PersonIdent actual) {
+    assertThat(actual.getEmailAddress())
+        .isEqualTo(expected.getEmailAddress());
+    assertThat(actual.getName())
+        .isEqualTo(expected.getName());
+    assertThat(actual.getTimeZone())
+        .isEqualTo(expected.getTimeZone());
+  }
+
   protected void assertSubmitter(String changeId, int psId)
       throws OrmException {
     ChangeNotes cn = notesFactory.create(
@@ -284,6 +283,15 @@
     assertThat(submitter.getAccountId()).isEqualTo(admin.getId());
   }
 
+  protected void assertNoSubmitter(String changeId, int psId)
+      throws OrmException {
+    ChangeNotes cn = notesFactory.create(
+        getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change());
+    PatchSetApproval submitter = approvalsUtil.getSubmitter(
+        db, cn, new PatchSet.Id(cn.getChangeId(), psId));
+    assertThat(submitter).isNull();
+  }
+
   protected void assertCherryPick(TestRepository<?> testRepo,
       boolean contentMerge) throws IOException {
     assertRebase(testRepo, contentMerge);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index ee17797..d0bcfd9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -56,7 +56,7 @@
 
   @Test
   public void createEmptyChange_InvalidStatus() throws Exception {
-    ChangeInfo ci = newChangeInfo(ChangeStatus.SUBMITTED);
+    ChangeInfo ci = newChangeInfo(ChangeStatus.MERGED);
     assertCreateFails(ci, BadRequestException.class,
         "unsupported change status");
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index b54c5d6..b99b30a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -65,6 +65,8 @@
     assertCurrentRevision(change2.getChangeId(), 2, newHead);
     assertSubmitter(change2.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 2);
+    assertPersonEquals(admin.getIdent(), newHead.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), newHead.getCommitterIdent());
   }
 
   @Test
@@ -106,7 +108,7 @@
     submitWithConflict(change2.getChangeId());
     assertThat(getRemoteHead()).isEqualTo(oldHead);
     assertCurrentRevision(change2.getChangeId(), 1, change2.getCommitId());
-    assertSubmitter(change2.getChangeId(), 1);
+    assertNoSubmitter(change2.getChangeId(), 1);
   }
 
   @Test
@@ -146,7 +148,7 @@
     submitWithConflict(change3.getChangeId());
     assertThat(getRemoteHead()).isEqualTo(oldHead);
     assertCurrentRevision(change3.getChangeId(), 1, change3.getCommitId());
-    assertSubmitter(change3.getChangeId(), 1);
+    assertNoSubmitter(change3.getChangeId(), 1);
   }
 
   @Test
@@ -162,24 +164,17 @@
     testRepo.reset(initialHead);
     PushOneCommit.Result change4 = createChange("Change 4", "d", "d");
 
-    submitStatusOnly(change2.getChangeId());
-    submitStatusOnly(change3.getChangeId());
+    approve(change2.getChangeId());
+    approve(change3.getChangeId());
     submit(change4.getChangeId());
 
     List<RevCommit> log = getRemoteLog();
     assertThat(log.get(0).getShortMessage()).isEqualTo(
         change4.getCommit().getShortMessage());
-    assertThat(log.get(0).getParent(0)).isEqualTo(log.get(1));
+    assertThat(log.get(1).getId()).isEqualTo(initialHead.getId());
 
-    assertThat(log.get(1).getShortMessage()).isEqualTo(
-        change3.getCommit().getShortMessage());
-    assertThat(log.get(1).getParent(0)).isEqualTo(log.get(2));
-
-    assertThat(log.get(2).getShortMessage()).isEqualTo(
-        change2.getCommit().getShortMessage());
-    assertThat(log.get(2).getParent(0)).isEqualTo(log.get(3));
-
-    assertThat(log.get(3).getId()).isEqualTo(initialHead.getId());
+    assertNew(change2.getChangeId());
+    assertNew(change3.getChangeId());
   }
 
   @Test
@@ -231,6 +226,7 @@
     // Tip has not changed.
     List<RevCommit> log = getRemoteLog();
     assertThat(log.get(0)).isEqualTo(initialHead.getId());
+    assertNoSubmitter(change3.getChangeId(), 1);
   }
 
   @Test
@@ -238,57 +234,16 @@
     RevCommit initialHead = getRemoteHead();
 
     testRepo.reset(initialHead);
-    createChange("Change 2", "b", "b");
+    PushOneCommit.Result change2 = createChange("Change 2", "b", "b");
     PushOneCommit.Result change3 = createChange("Change 3", "c", "c");
-    createChange("Change 4", "d", "d");
-    PushOneCommit.Result change5 = createChange("Change 5", "e", "e");
+    PushOneCommit.Result change4 = createChange("Change 5", "e", "e");
 
-    // Out of the above, only submit 3 and 5.
-    submitStatusOnly(change3.getChangeId());
-    submit(change5.getChangeId());
+    // Out of the above, only submit 4. 2,3 are not related to 4
+    // by topic or ancestor (due to cherrypicking!)
+    approve(change3.getChangeId());
+    submit(change4.getChangeId());
 
-    ChangeInfo info3 = get(change3.getChangeId());
-    assertThat(info3.status).isEqualTo(ChangeStatus.MERGED);
-
-    List<RevCommit> log = getRemoteLog();
-    assertThat(log.get(0).getShortMessage())
-        .isEqualTo(change5.getCommit().getShortMessage());
-    assertThat(log.get(1).getShortMessage())
-        .isEqualTo(change3.getCommit().getShortMessage());
-    assertThat(log.get(2).getShortMessage())
-        .isEqualTo(initialHead.getShortMessage());
-  }
-
-  @Test
-  public void submitChangeAfterParentFailsDueToConflict() throws Exception {
-    RevCommit initialHead = getRemoteHead();
-
-    testRepo.reset(initialHead);
-    PushOneCommit.Result change2 = createChange("Change 2", "b", "b1");
-    submit(change2.getChangeId());
-
-    testRepo.reset(initialHead);
-    PushOneCommit.Result change3 = createChange("Change 3", "b", "b2");
-    assertThat(change3.getCommit().getParent(0)).isEqualTo(initialHead);
-    PushOneCommit.Result change4 = createChange("Change 3", "c", "c3");
-
-    submitStatusOnly(change3.getChangeId());
-    submitStatusOnly(change4.getChangeId());
-
-    // Merge fails; change3 contains the delta "b1" -> "b2", which cannot be
-    // applied against tip.
-    // As change4 sits on top of change 3 we need to trigger submission there
-    // to include it into the mergeing
-    submitWithConflict(change4.getChangeId());
-
-    // change4 is a clean merge, so should succeed in the same run where change3
-    // failed.
-    ChangeInfo info4 = get(change4.getChangeId());
-    assertThat(info4.status).isEqualTo(ChangeStatus.MERGED);
-    List<RevCommit> log = getRemoteLog();
-    assertThat(log.get(0).getShortMessage())
-        .isEqualTo(change4.getCommit().getShortMessage());
-    assertThat(log.get(1).getShortMessage())
-        .isEqualTo(change2.getCommit().getShortMessage());
+    assertNew(change2.getChangeId());
+    assertNew(change3.getChangeId());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index 62a92b8..2655789 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -56,12 +56,14 @@
     assertThat(head.getParent(0).getId()).isEqualTo(change.getCommitId());
     assertSubmitter(change.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 1);
+    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
   }
 
   @Test
   public void submitTwoChangesWithFastForward_missingDependency() throws Exception {
     RevCommit oldHead = getRemoteHead();
-    PushOneCommit.Result change = createChange();
+    createChange();
     PushOneCommit.Result change2 = createChange();
 
     submitWithConflict(change2.getChangeId());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
index ebd3d3c..eb1d16b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
@@ -41,6 +41,8 @@
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertThat(head.getParent(1)).isEqualTo(change.getCommitId());
     assertSubmitter(change.getChangeId(), 1);
+    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
+    assertPersonEquals(serverIdent.get(), head.getCommitterIdent());
   }
 
   @Test
@@ -56,23 +58,22 @@
     testRepo.reset(initialHead);
     PushOneCommit.Result change4 = createChange("Change 4", "d", "d");
 
-    submitStatusOnly(change2.getChangeId());
-    submitStatusOnly(change3.getChangeId());
+    approve(change2.getChangeId());
+    approve(change3.getChangeId());
     submit(change4.getChangeId());
 
     List<RevCommit> log = getRemoteLog();
     RevCommit tip = log.get(0);
     assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
         change4.getCommit().getShortMessage());
-
-    tip = tip.getParent(0);
-    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
-        change3.getCommit().getShortMessage());
-
-    tip = tip.getParent(0);
-    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
-        change2.getCommit().getShortMessage());
-
+    assertThat(tip.getParent(0).getShortMessage()).isEqualTo(
+        initialHead.getShortMessage());
     assertThat(tip.getParent(0).getId()).isEqualTo(initialHead.getId());
+
+    assertPersonEquals(admin.getIdent(), tip.getAuthorIdent());
+    assertPersonEquals(serverIdent.get(), tip.getCommitterIdent());
+
+    assertNew(change2.getChangeId());
+    assertNew(change3.getChangeId());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index a9beefe..032cb7d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -29,6 +29,8 @@
     assertThat(head.getId()).isEqualTo(change.getCommitId());
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertSubmitter(change.getChangeId(), 1);
+    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
   }
 
   @Test
@@ -44,23 +46,32 @@
     testRepo.reset(initialHead);
     PushOneCommit.Result change4 = createChange("Change 4", "d", "d");
 
-    submitStatusOnly(change2.getChangeId());
-    submitStatusOnly(change3.getChangeId());
-    submit(change4.getChangeId());
+    // Change 2 stays untouched.
+    approve(change2.getChangeId());
+    // Change 3 is a fast-forward, no need to merge.
+    submit(change3.getChangeId());
 
     RevCommit tip = getRemoteLog().get(0);
+    assertThat(tip.getShortMessage()).isEqualTo(
+        change3.getCommit().getShortMessage());
+    assertThat(tip.getParent(0).getId()).isEqualTo(
+        initialHead.getId());
+    assertPersonEquals(admin.getIdent(), tip.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), tip.getCommitterIdent());
+
+    // We need to merge change 4.
+    submit(change4.getChangeId());
+
+    tip = getRemoteLog().get(0);
     assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
         change4.getCommit().getShortMessage());
-
-    tip = tip.getParent(0);
-    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
+    assertThat(tip.getParent(0).getShortMessage()).isEqualTo(
         change3.getCommit().getShortMessage());
 
-    tip = tip.getParent(0);
-    assertThat(tip.getShortMessage()).isEqualTo(
-        change2.getCommit().getShortMessage());
+    assertPersonEquals(admin.getIdent(), tip.getAuthorIdent());
+    assertPersonEquals(serverIdent.get(), tip.getCommitterIdent());
 
-    assertThat(tip.getParent(0).getId()).isEqualTo(initialHead.getId());
+    assertNew(change2.getChangeId());
   }
 
   @Test
@@ -184,6 +195,10 @@
           initialHead2.getShortMessage());
       assertThat(tip3.getShortMessage()).isEqualTo(
           change3Conflict.getCommit().getShortMessage());
+      assertNoSubmitter(change1a.getChangeId(), 1);
+      assertNoSubmitter(change2a.getChangeId(), 1);
+      assertNoSubmitter(change2b.getChangeId(), 1);
+      assertNoSubmitter(change3.getChangeId(), 1);
     } else {
       assertThat(tip1.getShortMessage()).isEqualTo(
           change1b.getCommit().getShortMessage());
@@ -191,6 +206,9 @@
           initialHead2.getShortMessage());
       assertThat(tip3.getShortMessage()).isEqualTo(
           change3Conflict.getCommit().getShortMessage());
+      assertNoSubmitter(change2a.getChangeId(), 1);
+      assertNoSubmitter(change2b.getChangeId(), 1);
+      assertNoSubmitter(change3.getChangeId(), 1);
     }
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
index bb26a30..d1d3bdf 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
@@ -43,6 +43,8 @@
     assertApproved(change.getChangeId());
     assertCurrentRevision(change.getChangeId(), 1, head);
     assertSubmitter(change.getChangeId(), 1);
+    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
   }
 
   @Test
@@ -65,6 +67,8 @@
     assertCurrentRevision(change2.getChangeId(), 2, head);
     assertSubmitter(change2.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 2);
+    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
+    assertPersonEquals(serverIdent.get(), head.getCommitterIdent());
   }
 
   @Test
@@ -107,6 +111,6 @@
     RevCommit head = getRemoteHead();
     assertThat(head).isEqualTo(oldHead);
     assertCurrentRevision(change2.getChangeId(), 1, change2.getCommitId());
-    assertSubmitter(change2.getChangeId(), 1);
+    assertNoSubmitter(change2.getChangeId(), 1);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
index d3677ab..7044ad0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -102,9 +102,9 @@
     assertThat(info.subject).isEqualTo("test commit");
     assertThat(info.message).isEqualTo(
         "test commit\n\nChange-Id: " + r.getChangeId() + "\n");
-    assertThat(info.author.name).isEqualTo("admin");
+    assertThat(info.author.name).isEqualTo("Administrator");
     assertThat(info.author.email).isEqualTo("admin@example.com");
-    assertThat(info.committer.name).isEqualTo("admin");
+    assertThat(info.committer.name).isEqualTo("Administrator");
     assertThat(info.committer.email).isEqualTo("admin@example.com");
 
     CommitInfo parent = Iterables.getOnlyElement(info.parents);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index c15c1085..342fe4c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.git.ProjectConfig;
 
 import org.eclipse.jgit.lib.Repository;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index b804607..80ca561 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -135,7 +135,6 @@
       case MERGED:
         return "status:merged";
       case NEW:
-      case SUBMITTED:
       default:
         return "status:open";
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 3d1e3bd..cb0d5f3 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -29,7 +29,6 @@
   void delete() throws RestApiException;
   void review(ReviewInput in) throws RestApiException;
 
-  /** {@code submit} with {@link SubmitInput#waitForMerge} set to true. */
   void submit() throws RestApiException;
   void submit(SubmitInput in) throws RestApiException;
   void publish() throws RestApiException;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
index f3fc887..56ebf9d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
@@ -28,40 +28,13 @@
    * <p>
    * Changes in the NEW state can be moved to:
    * <ul>
-   * <li>{@link #SUBMITTED} - when the Submit Patch Set action is used;
+   * <li>{@link #MERGED} - when the Submit Patch Set action is used;
    * <li>{@link #ABANDONED} - when the Abandon action is used.
    * </ul>
    */
   NEW,
 
   /**
-   * Change is open, but has been submitted to the merge queue.
-   *
-   * <p>
-   * A change enters the SUBMITTED state when an authorized user presses the
-   * "submit" action through the web UI, requesting that Gerrit merge the
-   * change's current patch set into the destination branch.
-   *
-   * <p>
-   * Typically a change resides in the SUBMITTED for only a brief sub-second
-   * period while the merge queue fires and the destination branch is updated.
-   * However, if a dependency commit (directly or transitively) is not yet
-   * merged into the branch, the change will hang in the SUBMITTED state
-   * indefinitely.
-   *
-   * <p>
-   * Changes in the SUBMITTED state can be moved to:
-   * <ul>
-   * <li>{@link #NEW} - when a replacement patch set is supplied, OR when a
-   * merge conflict is detected;
-   * <li>{@link #MERGED} - when the change has been successfully merged into
-   * the destination branch;
-   * <li>{@link #ABANDONED} - when the Abandon action is used.
-   * </ul>
-   */
-  SUBMITTED,
-
-  /**
    * Change is a draft change that only consists of draft patchsets.
    *
    * <p>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index 8a2d645..070a868 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -113,6 +113,7 @@
   private static String myHost;
   private static ServerInfo myServerInfo;
   private static boolean hasDocumentation;
+  private static String docUrl;
   private static HostPageData.Theme myTheme;
   private static Account myAccount;
   private static String defaultScreenToken;
@@ -434,12 +435,18 @@
       @Override
       public void onSuccess(DocInfo indexInfo) {
         hasDocumentation = indexInfo != null;
+        docUrl = selfRedirect("/Documentation/");
       }
     }));
     ConfigServerApi.serverInfo(cbg.add(new GerritCallback<ServerInfo>() {
       @Override
       public void onSuccess(ServerInfo info) {
         myServerInfo = info;
+        String du = info.gerrit().docUrl();
+        if (du != null && !du.isEmpty()) {
+          hasDocumentation = true;
+          docUrl = du;
+        }
       }
     }));
     HostPageDataService hpd = GWT.create(HostPageDataService.class);
@@ -822,10 +829,15 @@
     req.setCallback(new RequestCallback() {
       @Override
       public void onResponseReceived(Request req, Response resp) {
-        if (resp.getStatusCode() == Response.SC_OK) {
-          cb.onSuccess(DocInfo.create());
-        } else {
-          cb.onSuccess(null);
+        switch (resp.getStatusCode()) {
+          case Response.SC_OK:
+          case Response.SC_MOVED_PERMANENTLY:
+          case Response.SC_MOVED_TEMPORARILY:
+            cb.onSuccess(DocInfo.create());
+            break;
+          default:
+            cb.onSuccess(null);
+            break;
         }
       }
 
@@ -996,7 +1008,7 @@
 
   private static void addDocLink(final LinkMenuBar m, final String text,
       final String href) {
-    final Anchor atag = anchor(text, selfRedirect("/Documentation/" + href));
+    final Anchor atag = anchor(text, docUrl + href);
     atag.setTarget("_blank");
     m.add(atag);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
index 9092508..de83354 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
@@ -38,6 +38,7 @@
       @Override
       public void onSend() {
         ChangeApi.createChange(project, getDestinationBranch(),
+          getDestinationTopic(),
           message.getText(), null,
           new GerritCallback<ChangeInfo>() {
             @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
index 0fac957..b5f87cf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
@@ -27,7 +27,7 @@
   static void call(final Button b, final String project) {
     b.setEnabled(false);
 
-    ChangeApi.createChange(project, RefNames.REFS_CONFIG,
+    ChangeApi.createChange(project, RefNames.REFS_CONFIG, null,
         Util.C.editConfigMessage(), null, new GerritCallback<ChangeInfo>() {
           @Override
           public void onSuccess(ChangeInfo result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index e6f262c..4edcfa65 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -690,7 +690,7 @@
     return pluginConfigValues;
   }
 
-  public class ProjectDownloadPanel extends DownloadPanel {
+  public static class ProjectDownloadPanel extends DownloadPanel {
     public ProjectDownloadPanel(String project, boolean isAllowsAnonymous) {
       super(project, isAllowsAnonymous);
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
index b7307be..ec5c4a6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
@@ -166,6 +166,14 @@
     api.get(wrap(cb));
   }
 
+  /**
+   * The same as {@link #get(RestApi, JavaScriptObject)} but without converting
+   * a {@link NativeString} result to String.
+   */
+  static final void getRaw(RestApi api, final JavaScriptObject cb) {
+    api.get(wrapRaw(cb));
+  }
+
   static final void post(RestApi api, JavaScriptObject in, JavaScriptObject cb) {
     if (NativeString.is(in)) {
       post(api, ((NativeString) in).asString(), cb);
@@ -174,14 +182,42 @@
     }
   }
 
+  /**
+   * The same as {@link #post(RestApi, JavaScriptObject, JavaScriptObject)} but
+   * without converting a {@link NativeString} result to String.
+   */
+  static final void postRaw(RestApi api, JavaScriptObject in, JavaScriptObject cb) {
+    if (NativeString.is(in)) {
+      postRaw(api, ((NativeString) in).asString(), cb);
+    } else {
+      api.post(in, wrapRaw(cb));
+    }
+  }
+
   static final void post(RestApi api, String in, JavaScriptObject cb) {
     api.post(in, wrap(cb));
   }
 
+  /**
+   * The same as {@link #post(RestApi, String, JavaScriptObject)} but without
+   * converting a {@link NativeString} result to String.
+   */
+  static final void postRaw(RestApi api, String in, JavaScriptObject cb) {
+    api.post(in, wrapRaw(cb));
+  }
+
   static final void put(RestApi api, JavaScriptObject cb) {
     api.put(wrap(cb));
   }
 
+  /**
+   * The same as {@link #put(RestApi, JavaScriptObject)} but without converting
+   * a {@link NativeString} result to String.
+   */
+  static final void putRaw(RestApi api, JavaScriptObject cb) {
+    api.put(wrapRaw(cb));
+  }
+
   static final void put(RestApi api, JavaScriptObject in, JavaScriptObject cb) {
     if (NativeString.is(in)) {
       put(api, ((NativeString) in).asString(), cb);
@@ -190,14 +226,42 @@
     }
   }
 
+  /**
+   * The same as {@link #put(RestApi, JavaScriptObject, JavaScriptObject)} but
+   * without converting a {@link NativeString} result to String.
+   */
+  static final void putRaw(RestApi api, JavaScriptObject in, JavaScriptObject cb) {
+    if (NativeString.is(in)) {
+      putRaw(api, ((NativeString) in).asString(), cb);
+    } else {
+      api.put(in, wrapRaw(cb));
+    }
+  }
+
   static final void put(RestApi api, String in, JavaScriptObject cb) {
     api.put(in, wrap(cb));
   }
 
+  /**
+   * The same as {@link #put(RestApi, String, JavaScriptObject)} but without
+   * converting a {@link NativeString} result to String.
+   */
+  static final void putRaw(RestApi api, String in, JavaScriptObject cb) {
+    api.put(in, wrapRaw(cb));
+  }
+
   static final void delete(RestApi api, JavaScriptObject cb) {
     api.delete(wrap(cb));
   }
 
+  /**
+   * The same as {@link #delete(RestApi, JavaScriptObject)} but without
+   * converting a {@link NativeString} result to String.
+   */
+  static final void deleteRaw(RestApi api, JavaScriptObject cb) {
+    api.delete(wrapRaw(cb));
+  }
+
   private static GerritCallback<JavaScriptObject> wrap(final JavaScriptObject cb) {
     return new GerritCallback<JavaScriptObject>() {
       @Override
@@ -211,4 +275,13 @@
       }
     };
   }
+
+  private static GerritCallback<JavaScriptObject> wrapRaw(final JavaScriptObject cb) {
+    return new GerritCallback<JavaScriptObject>() {
+      @Override
+      public void onSuccess(JavaScriptObject result) {
+        ApiGlue.invoke(cb, result);
+      }
+    };
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
index b52efe8..e4a5446 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
@@ -102,6 +102,12 @@
             Lcom/google/gwt/core/client/JavaScriptObject;)
           (this._api(u), b);
       },
+      get_raw: function(u,b) {
+        @com.google.gerrit.client.api.ActionContext::getRaw(
+            Lcom/google/gerrit/client/rpc/RestApi;
+            Lcom/google/gwt/core/client/JavaScriptObject;)
+          (this._api(u), b);
+      },
       post: function(u,i,b) {
         if (typeof i == 'string') {
           @com.google.gerrit.client.api.ActionContext::post(
@@ -117,6 +123,21 @@
             (this._api(u), i, b);
         }
       },
+      post_raw: function(u,i,b) {
+        if (typeof i == 'string') {
+          @com.google.gerrit.client.api.ActionContext::postRaw(
+              Lcom/google/gerrit/client/rpc/RestApi;
+              Ljava/lang/String;
+              Lcom/google/gwt/core/client/JavaScriptObject;)
+            (this._api(u), i, b);
+        } else {
+          @com.google.gerrit.client.api.ActionContext::postRaw(
+              Lcom/google/gerrit/client/rpc/RestApi;
+              Lcom/google/gwt/core/client/JavaScriptObject;
+              Lcom/google/gwt/core/client/JavaScriptObject;)
+            (this._api(u), i, b);
+        }
+      },
       put: function(u,i,b) {
         if (b) {
           if (typeof i == 'string') {
@@ -139,6 +160,28 @@
             (this._api(u), i);
         }
       },
+      put_raw: function(u,i,b) {
+        if (b) {
+          if (typeof i == 'string') {
+            @com.google.gerrit.client.api.ActionContext::putRaw(
+                Lcom/google/gerrit/client/rpc/RestApi;
+                Ljava/lang/String;
+                Lcom/google/gwt/core/client/JavaScriptObject;)
+              (this._api(u), i, b);
+          } else {
+            @com.google.gerrit.client.api.ActionContext::putRaw(
+                Lcom/google/gerrit/client/rpc/RestApi;
+                Lcom/google/gwt/core/client/JavaScriptObject;
+                Lcom/google/gwt/core/client/JavaScriptObject;)
+              (this._api(u), i, b);
+          }
+        } else {
+          @com.google.gerrit.client.api.ActionContext::putRaw(
+              Lcom/google/gerrit/client/rpc/RestApi;
+              Lcom/google/gwt/core/client/JavaScriptObject;)
+            (this._api(u), i);
+        }
+      },
       'delete': function(u,b) {
         @com.google.gerrit.client.api.ActionContext::delete(
             Lcom/google/gerrit/client/rpc/RestApi;
@@ -151,6 +194,12 @@
             Lcom/google/gwt/core/client/JavaScriptObject;)
           (this._api(u), b);
       },
+      del_raw: function(u,b) {
+        @com.google.gerrit.client.api.ActionContext::deleteRaw(
+            Lcom/google/gerrit/client/rpc/RestApi;
+            Lcom/google/gwt/core/client/JavaScriptObject;)
+          (this._api(u), b);
+      },
     };
   }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
index 10d77cb..3df0b2d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -60,6 +60,7 @@
   private ChangeInfo changeInfo;
   private String revision;
   private String project;
+  private String topic;
   private String subject;
   private String message;
   private String branch;
@@ -80,6 +81,7 @@
     CommitInfo commit = revInfo.commit();
     changeId = info.legacyId();
     project = info.project();
+    topic = info.topic();
     subject = commit.subject();
     message = commit.message();
     branch = info.branch();
@@ -160,7 +162,7 @@
   void onFollowUp(@SuppressWarnings("unused") ClickEvent e) {
     if (followUpAction == null) {
       followUpAction = new FollowUpAction(followUp, project,
-          branch, key);
+          branch, topic, key);
     }
     followUpAction.show();
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
index 30394d6..8f31010 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
@@ -24,18 +24,20 @@
 class FollowUpAction extends ActionMessageBox {
   private final String project;
   private final String branch;
+  private final String topic;
   private final String base;
 
-  FollowUpAction(Button b, String project, String branch, String key) {
+  FollowUpAction(Button b, String project, String branch, String topic, String key) {
     super(b);
     this.project = project;
     this.branch = branch;
+    this.topic = topic;
     this.base = project + "~" + branch + "~" + key;
   }
 
   @Override
   void send(String message) {
-    ChangeApi.createChange(project, branch, message, base,
+    ChangeApi.createChange(project, branch, topic, message, base,
         new GerritCallback<ChangeInfo>() {
           @Override
           public void onSuccess(ChangeInfo result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
index 7b899ba..9f7e515 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ChangeList;
 import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index 0dd85b1..46d5067 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -45,11 +45,12 @@
    * new change is created as NEW.
    *
    */
-  public static void createChange(String project, String branch,
+  public static void createChange(String project, String branch, String topic,
       String subject, String base, AsyncCallback<ChangeInfo> cb) {
     CreateChangeInput input = CreateChangeInput.create();
     input.project(emptyToNull(project));
     input.branch(emptyToNull(branch));
+    input.topic(emptyToNull(topic));
     input.subject(emptyToNull(subject));
     input.baseChange(emptyToNull(base));
 
@@ -249,6 +250,7 @@
     }
 
     public final native void branch(String b) /*-{ if(b)this.branch=b; }-*/;
+    public final native void topic(String t) /*-{ if(t)this.topic=t; }-*/;
     public final native void project(String p) /*-{ if(p)this.project=p; }-*/;
     public final native void subject(String s) /*-{ if(s)this.subject=s; }-*/;
     public final native void baseChange(String b) /*-{ if(b)this.base_change=b; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 2727ae2..759e722 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -18,7 +18,6 @@
 
 public interface ChangeConstants extends Constants {
   String statusLongNew();
-  String statusLongSubmitted();
   String statusLongMerged();
   String statusLongAbandoned();
   String statusLongDraft();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 8db9319..ca29773 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -1,5 +1,4 @@
 statusLongNew = Review in Progress
-statusLongSubmitted = Submitted, Merge Pending
 statusLongMerged = Merged
 statusLongAbandoned = Abandoned
 statusLongDraft = Draft
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
index d34492c..e6d3dbe 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
@@ -34,8 +34,6 @@
         return C.statusLongDraft();
       case NEW:
         return C.statusLongNew();
-      case SUBMITTED:
-        return C.statusLongSubmitted();
       case MERGED:
         return C.statusLongMerged();
       case ABANDONED:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/GerritInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/GerritInfo.java
index 66e4154..83c1ba9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/GerritInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/GerritInfo.java
@@ -36,6 +36,7 @@
 
   public final native String allProjects() /*-{ return this.all_projects; }-*/;
   public final native String allUsers() /*-{ return this.all_users; }-*/;
+  public final native String docUrl() /*-{ return this.doc_url; }-*/;
   public final native String reportBugUrl() /*-{ return this.report_bug_url; }-*/;
   public final native String reportBugText() /*-{ return this.report_bug_text; }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
index a2b4aa8..e398e78 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
@@ -24,6 +24,7 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwt.user.client.ui.TextBox;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
@@ -33,6 +34,7 @@
 public abstract class CreateChangeDialog extends TextAreaActionDialog {
   private SuggestBox newChange;
   private List<BranchInfo> branches;
+  private TextBox topic;
 
   public CreateChangeDialog(Project.NameKey project) {
     super(Util.C.dialogCreateChangeTitle(),
@@ -45,6 +47,15 @@
           }
         });
 
+    topic = new TextBox();
+    topic.setWidth("100%");
+    topic.getElement().getStyle().setProperty("boxSizing", "border-box");
+    FlowPanel newTopicPanel = new FlowPanel();
+    newTopicPanel.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
+    newTopicPanel.add(topic);
+    panel.insert(newTopicPanel, 0);
+    panel.insert(new SmallHeading(Util.C.newChangeTopicSuggestion()), 0);
+
     newChange = new SuggestBox(new HighlightSuggestOracle() {
       @Override
       protected void onRequestSuggestions(Request request, Callback done) {
@@ -60,14 +71,13 @@
 
     newChange.setWidth("100%");
     newChange.getElement().getStyle().setProperty("boxSizing", "border-box");
-    message.setCharacterWidth(70);
-
-    FlowPanel mwrap = new FlowPanel();
-    mwrap.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
-    mwrap.add(newChange);
-
-    panel.insert(mwrap, 0);
+    FlowPanel newChangePanel = new FlowPanel();
+    newChangePanel.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
+    newChangePanel.add(newChange);
+    panel.insert(newChangePanel, 0);
     panel.insert(new SmallHeading(Util.C.newChangeBranchSuggestion()), 0);
+
+    message.setCharacterWidth(70);
   }
 
   @Override
@@ -81,6 +91,10 @@
     return newChange.getText();
   }
 
+  public String getDestinationTopic() {
+    return topic.getText();
+  }
+
   static class BranchSuggestion implements Suggestion {
     private BranchInfo branch;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
index 0f5e12a..a220ea0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
@@ -29,4 +29,5 @@
   String dialogCreateChangeTitle();
   String dialogCreateChangeHeading();
   String newChangeBranchSuggestion();
+  String newChangeTopicSuggestion();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
index a0845d9..736e210 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
@@ -10,3 +10,4 @@
 dialogCreateChangeTitle = Create Change
 dialogCreateChangeHeading = Description
 newChangeBranchSuggestion = Select branch for new change
+newChangeTopicSuggestion = Enter topic for new change
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
index bb20875..65f42e5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -249,7 +249,6 @@
     CacheHeaders.setNotCacheable(rsp);
 
     OutputStream out;
-    @SuppressWarnings("resource")
     ZipOutputStream zo;
 
     final MimeType contentType = registry.getMimeType(path, raw);
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index 35d6636..5af7cc5 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -48,6 +48,7 @@
   @Override
   protected void configure() {
     factory(LuceneChangeIndex.Factory.class);
+    factory(OnlineReindexer.Factory.class);
     install(new IndexModule(threads));
     if (singleVersion == null && base == null) {
       install(new MultiVersionModule());
@@ -65,7 +66,6 @@
   private static class MultiVersionModule extends LifecycleModule {
     @Override
     public void configure() {
-      factory(OnlineReindexer.Factory.class);
       listener().to(LuceneVersionManager.class);
     }
   }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index 44b1ea0..407f5a8 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -46,7 +46,7 @@
 import java.util.TreeMap;
 
 @Singleton
-class LuceneVersionManager implements LifecycleListener {
+public class LuceneVersionManager implements LifecycleListener {
   private static final Logger log = LoggerFactory
       .getLogger(LuceneVersionManager.class);
 
@@ -95,6 +95,7 @@
   private final IndexCollection indexes;
   private final OnlineReindexer.Factory reindexerFactory;
   private final boolean onlineUpgrade;
+  private OnlineReindexer reindexer;
 
   @Inject
   LuceneVersionManager(
@@ -165,7 +166,53 @@
 
     int latest = write.get(0).version;
     if (onlineUpgrade && latest != search.version) {
-      reindexerFactory.create(latest).start();
+      reindexer = reindexerFactory.create(latest);
+      reindexer.start();
+    }
+  }
+
+  /**
+   * Start the online reindexer if the current index is not already the latest.
+   *
+   * @return true if started, otherwise false.
+   * @throws ReindexerAlreadyRunningException
+   */
+  public synchronized boolean startReindexer()
+      throws ReindexerAlreadyRunningException {
+    validateReindexerNotRunning();
+    if (!isCurrentIndexVersionLatest()) {
+      reindexer.start();
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Activate the latest index if the current index is not already the latest.
+   *
+   * @return true if index was activate, otherwise false.
+   * @throws ReindexerAlreadyRunningException
+   */
+  public synchronized boolean activateLatestIndex()
+      throws ReindexerAlreadyRunningException {
+    validateReindexerNotRunning();
+    if (!isCurrentIndexVersionLatest()) {
+      reindexer.activateIndex();
+      return true;
+    }
+    return false;
+  }
+
+  private boolean isCurrentIndexVersionLatest() {
+    return reindexer == null
+        || reindexer.getVersion() == indexes.getSearchIndex().getSchema()
+            .getVersion();
+  }
+
+  private void validateReindexerNotRunning()
+      throws ReindexerAlreadyRunningException {
+    if (reindexer != null && reindexer.isRunning()) {
+      throw new ReindexerAlreadyRunningException();
     }
   }
 
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
index edded44..1dbc427 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
@@ -29,6 +29,7 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public class OnlineReindexer {
   private static final Logger log = LoggerFactory
@@ -42,6 +43,8 @@
   private final SiteIndexer batchIndexer;
   private final ProjectCache projectCache;
   private final int version;
+  private ChangeIndex index;
+  private final AtomicBoolean running = new AtomicBoolean();
 
   @Inject
   OnlineReindexer(
@@ -56,15 +59,29 @@
   }
 
   public void start() {
-    Thread t = new Thread() {
-      @Override
-      public void run() {
-        reindex();
-      }
-    };
-    t.setName(String.format("Reindex v%d-v%d",
-        version(indexes.getSearchIndex()), version));
-    t.start();
+    if (running.compareAndSet(false, true)) {
+      Thread t = new Thread() {
+        @Override
+        public void run() {
+          try {
+            reindex();
+          } finally {
+            running.set(false);
+          }
+        }
+      };
+      t.setName(String.format("Reindex v%d-v%d",
+          version(indexes.getSearchIndex()), version));
+      t.start();
+    }
+  }
+
+  public boolean isRunning() {
+    return running.get();
+  }
+
+  public int getVersion() {
+    return version;
   }
 
   private static int version(ChangeIndex i) {
@@ -72,7 +89,7 @@
   }
 
   private void reindex() {
-    ChangeIndex index = checkNotNull(indexes.getWriteIndex(version),
+    index = checkNotNull(indexes.getWriteIndex(version),
         "not an active write schema version: %s", version);
     log.info("Starting online reindex from schema version {} to {}",
         version(indexes.getSearchIndex()), version(index));
@@ -84,9 +101,13 @@
           version(index), result.doneCount(), result.failedCount());
       return;
     }
+    log.info("Reindex to version {} complete", version(index));
+    activateIndex();
+  }
 
+  void activateIndex() {
     indexes.setSearchIndex(index);
-    log.info("Reindex complete, using schema version {}", version(index));
+    log.info("Using schema version {}", version(index));
     try {
       index.markReady(true);
     } catch (IOException e) {
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ReindexerAlreadyRunningException.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ReindexerAlreadyRunningException.java
new file mode 100644
index 0000000..0ca632b
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ReindexerAlreadyRunningException.java
@@ -0,0 +1,24 @@
+// 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.lucene;
+
+public class ReindexerAlreadyRunningException extends Exception {
+
+  private static final long serialVersionUID = 1L;
+
+  public ReindexerAlreadyRunningException() {
+    super("Reindexer is already running.");
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index dcb6c85..139af9e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -49,6 +49,7 @@
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
+import com.google.gerrit.server.config.DownloadConfig;
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.RestCacheAdminModule;
@@ -402,8 +403,8 @@
     if (!test) {
       modules.add(new SshHostKeyModule());
     }
-    modules.add(new DefaultCommandModule(slave));
-
+    modules.add(new DefaultCommandModule(slave,
+        sysInjector.getInstance(DownloadConfig.class)));
     return sysInjector.createChildInjector(modules);
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
index 4397661..948182e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
@@ -136,7 +136,7 @@
   }
 
   @AutoValue
-  static abstract class Relation {
+  abstract static class Relation {
     private static Relation create(RelationModel model, ReviewDb db)
         throws IllegalAccessException, InvocationTargetException,
         NoSuchMethodException, ClassNotFoundException {
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
index 8b2a6b8..8d408fb 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
@@ -110,7 +110,7 @@
   }
 
   private static native void get(String p, JavaScriptObject r)
-  /*-{ $wnd.Gerrit.get(p, r) }-*/;
+  /*-{ $wnd.Gerrit.get_raw(p, r) }-*/;
 
   public <T extends JavaScriptObject>
   void put(AsyncCallback<T> cb) {
@@ -118,7 +118,7 @@
   }
 
   private static native void put(String p, JavaScriptObject r)
-  /*-{ $wnd.Gerrit.put(p, r) }-*/;
+  /*-{ $wnd.Gerrit.put_raw(p, r) }-*/;
 
   public <T extends JavaScriptObject>
   void put(String content, AsyncCallback<T> cb) {
@@ -127,7 +127,7 @@
 
   private static native
   void put(String p, String c, JavaScriptObject r)
-  /*-{ $wnd.Gerrit.put(p, c, r) }-*/;
+  /*-{ $wnd.Gerrit.put_raw(p, c, r) }-*/;
 
   public <T extends JavaScriptObject>
   void put(JavaScriptObject content, AsyncCallback<T> cb) {
@@ -136,7 +136,7 @@
 
   private static native
   void put(String p, JavaScriptObject c, JavaScriptObject r)
-  /*-{ $wnd.Gerrit.put(p, c, r) }-*/;
+  /*-{ $wnd.Gerrit.put_raw(p, c, r) }-*/;
 
   public <T extends JavaScriptObject>
   void post(String content, AsyncCallback<T> cb) {
@@ -145,7 +145,7 @@
 
   private static native
   void post(String p, String c, JavaScriptObject r)
-  /*-{ $wnd.Gerrit.post(p, c, r) }-*/;
+  /*-{ $wnd.Gerrit.post_raw(p, c, r) }-*/;
 
   public <T extends JavaScriptObject>
   void post(JavaScriptObject content, AsyncCallback<T> cb) {
@@ -154,14 +154,14 @@
 
   private static native
   void post(String p, JavaScriptObject c, JavaScriptObject r)
-  /*-{ $wnd.Gerrit.post(p, c, r) }-*/;
+  /*-{ $wnd.Gerrit.post_raw(p, c, r) }-*/;
 
   public void delete(AsyncCallback<NoContent> cb) {
     delete(path(), wrap(cb));
   }
 
   private static native void delete(String p, JavaScriptObject r)
-  /*-{ $wnd.Gerrit.del(p, r) }-*/;
+  /*-{ $wnd.Gerrit.del_raw(p, r) }-*/;
 
   private static native <T extends JavaScriptObject>
   JavaScriptObject wrap(AsyncCallback<T> b) /*-{
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 97fde9d..0701771 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -251,8 +251,6 @@
   private static final char MIN_OPEN = 'a';
   /** Database constant for {@link Status#NEW}. */
   public static final char STATUS_NEW = 'n';
-  /** Database constant for {@link Status#SUBMITTED}. */
-  public static final char STATUS_SUBMITTED = 's';
   /** Database constant for {@link Status#DRAFT}. */
   public static final char STATUS_DRAFT = 'd';
   /** Maximum database status constant for an open change. */
@@ -285,40 +283,13 @@
      * <p>
      * Changes in the NEW state can be moved to:
      * <ul>
-     * <li>{@link #SUBMITTED} - when the Submit Patch Set action is used;
+     * <li>{@link #MERGED} - when the Submit Patch Set action is used;
      * <li>{@link #ABANDONED} - when the Abandon action is used.
      * </ul>
      */
     NEW(STATUS_NEW, ChangeStatus.NEW),
 
     /**
-     * Change is open, but has been submitted to the merge queue.
-     *
-     * <p>
-     * A change enters the SUBMITTED state when an authorized user presses the
-     * "submit" action through the web UI, requesting that Gerrit merge the
-     * change's current patch set into the destination branch.
-     *
-     * <p>
-     * Typically a change resides in the SUBMITTED for only a brief sub-second
-     * period while the merge queue fires and the destination branch is updated.
-     * However, if a dependency commit (a {@link PatchSetAncestor}, directly or
-     * transitively) is not yet merged into the branch, the change will hang in
-     * the SUBMITTED state indefinitely.
-     *
-     * <p>
-     * Changes in the SUBMITTED state can be moved to:
-     * <ul>
-     * <li>{@link #NEW} - when a replacement patch set is supplied, OR when a
-     * merge conflict is detected;
-     * <li>{@link #MERGED} - when the change has been successfully merged into
-     * the destination branch;
-     * <li>{@link #ABANDONED} - when the Abandon action is used.
-     * </ul>
-     */
-    SUBMITTED(STATUS_SUBMITTED, ChangeStatus.SUBMITTED),
-
-    /**
      * Change is a draft change that only consists of draft patchsets.
      *
      * <p>
@@ -475,7 +446,7 @@
 
   /**
    * First line of first patch set's commit message.
-   *
+   * <p>
    * Unlike {@link #subject}, this string does not change if future patch sets
    * change the first line.
    */
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index a36d716..707664f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -59,9 +59,9 @@
 
   /**
    * Special ref for GPG public keys used by {@link
-   * com.google.gerrit.server.git.SignedPushPreReceiveHook}.
+   * com.google.gerrit.server.git.gpg.SignedPushPreReceiveHook}.
    */
-  public static final String REFS_GPG_KEYS = REFS + "gpg-keys";
+  public static final String REFS_GPG_KEYS = "refs/meta/gpg-keys";
 
   public static String fullName(String ref) {
     return ref.startsWith(REFS) ? ref : REFS_HEADS + ref;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
index 465c9ba..93a3814 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
@@ -252,7 +252,7 @@
     }
   }
 
-  public class AccessSectionInfo {
+  public static class AccessSectionInfo {
     public Map<String, PermissionInfo> permissions;
 
     public AccessSectionInfo(AccessSection section) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 1b420f7..1f8aa0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -152,7 +152,6 @@
   @Override
   public void submit() throws RestApiException {
     SubmitInput in = new SubmitInput();
-    in.waitForMerge = true;
     submit(in);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
index ab1cecb1..39c0712 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -210,7 +210,11 @@
       validateCommit(git, refControl, c, me, ins);
       updateRef(git, rw, c, change, ins.getPatchSet());
 
-      change.setTopic(input.topic);
+      String topic = input.topic;
+      if (topic != null) {
+        topic = Strings.emptyToNull(topic.trim());
+      }
+      change.setTopic(topic);
       ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
       ins.setGroups(groups);
       ins.insert();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index a840651..14aa5b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -244,7 +244,8 @@
               null /*inserter*/,
               canMerge,
               accepted,
-              key.load.dest).dryRun(tip, rev);
+              key.load.dest,
+              null).dryRun(tip, rev);
         }
       } finally {
         key.load = null;
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 92bf3bf..b9e9b3e 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
@@ -18,17 +18,12 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Optional;
-import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
-import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Table;
-import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
@@ -38,34 +33,24 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.ChangeSet;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LabelNormalizer;
 import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Inject;
@@ -73,15 +58,12 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -106,31 +88,19 @@
   private static final String CLICK_FAILURE_TOOLTIP =
       "Clicking the button would fail.";
 
-  public enum Status {
-    SUBMITTED, MERGED
-  }
-
   public static class Output {
-    public Status status;
     transient Change change;
 
-    private Output(Status s, Change c) {
-      status = s;
+    private Output(Change c) {
       change = c;
     }
   }
 
-  private final PersonIdent serverIdent;
   private final Provider<ReviewDb> dbProvider;
   private final GitRepositoryManager repoManager;
-  private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeData.Factory changeDataFactory;
-  private final ChangeUpdate.Factory updateFactory;
-  private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
-  private final MergeOp.Factory mergeOpFactory;
-  private final ChangeIndexer indexer;
-  private final LabelNormalizer labelNormalizer;
+  private final Provider<MergeOp> mergeOpProvider;
   private final AccountsCollection accounts;
   private final ChangesCollection changes;
   private final String label;
@@ -141,34 +111,22 @@
   private final Provider<InternalChangeQuery> queryProvider;
 
   @Inject
-  Submit(@GerritPersonIdent PersonIdent serverIdent,
-      Provider<ReviewDb> dbProvider,
+  Submit(Provider<ReviewDb> dbProvider,
       GitRepositoryManager repoManager,
-      IdentifiedUser.GenericFactory userFactory,
       ChangeData.Factory changeDataFactory,
-      ChangeUpdate.Factory updateFactory,
-      ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
-      MergeOp.Factory mergeOpFactory,
+      Provider<MergeOp> mergeOpProvider,
       AccountsCollection accounts,
       ChangesCollection changes,
-      ChangeIndexer indexer,
-      LabelNormalizer labelNormalizer,
       @GerritServerConfig Config cfg,
       Provider<InternalChangeQuery> queryProvider) {
-    this.serverIdent = serverIdent;
     this.dbProvider = dbProvider;
     this.repoManager = repoManager;
-    this.userFactory = userFactory;
     this.changeDataFactory = changeDataFactory;
-    this.updateFactory = updateFactory;
-    this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
-    this.mergeOpFactory = mergeOpFactory;
+    this.mergeOpProvider = mergeOpProvider;
     this.accounts = accounts;
     this.changes = changes;
-    this.indexer = indexer;
-    this.labelNormalizer = labelNormalizer;
     this.label = MoreObjects.firstNonNull(
         Strings.emptyToNull(cfg.getString("change", null, "submitLabel")),
         "Submit");
@@ -212,10 +170,19 @@
           rsrc.getPatchSet().getRevision().get()));
     }
 
-    ChangeSet submittedChanges = ChangeSet.create(submit(rsrc, caller, false));
+    List<Change> changes;
+    if (submitWholeTopic && !Strings.isNullOrEmpty(change.getTopic())) {
+      changes = new ArrayList<>();
+      for (ChangeData cd : getChangesByTopic(change.getTopic())) {
+        changes.add(cd.change());
+      }
+    } else {
+      changes = Arrays.asList(change);
+    }
+    ChangeSet submittedChanges = ChangeSet.create(changes);
 
     try {
-      mergeOpFactory.create(submittedChanges, caller).merge(true);
+      mergeOpProvider.get().merge(submittedChanges, caller, true);
       change = dbProvider.get().changes().get(change.getId());
     } catch (NoSuchChangeException e) {
       throw new OrmException("Submission failed", e);
@@ -225,10 +192,8 @@
       throw new ResourceConflictException("change is deleted");
     }
     switch (change.getStatus()) {
-      case SUBMITTED:
-        return new Output(Status.SUBMITTED, change);
       case MERGED:
-        return new Output(Status.MERGED, change);
+        return new Output(change);
       case NEW:
         ChangeMessage msg = getConflictMessage(rsrc);
         if (msg != null) {
@@ -374,203 +339,6 @@
         .orNull();
   }
 
-  private Change submitToDatabase(final ReviewDb db, final Change.Id changeId,
-      final Timestamp timestamp) throws OrmException,
-      ResourceConflictException {
-    Change ret = db.changes().atomicUpdate(changeId,
-      new AtomicUpdate<Change>() {
-        @Override
-        public Change update(Change change) {
-          if (change.getStatus().isOpen()) {
-            change.setStatus(Change.Status.SUBMITTED);
-            change.setLastUpdatedOn(timestamp);
-            return change;
-          }
-          return null;
-        }
-      });
-    if (ret != null) {
-      return ret;
-    } else {
-      throw new ResourceConflictException("change " + changeId + " is "
-          + status(db.changes().get(changeId)));
-    }
-  }
-
-  private Change submitThisChange(RevisionResource rsrc, IdentifiedUser caller,
-      boolean force) throws ResourceConflictException, OrmException,
-      IOException {
-    ReviewDb db = dbProvider.get();
-    ChangeData cd = changeDataFactory.create(db, rsrc.getControl());
-    List<SubmitRecord> submitRecords = checkSubmitRule(cd,
-        rsrc.getPatchSet(), force);
-
-    final Timestamp timestamp = TimeUtil.nowTs();
-    Change change = rsrc.getChange();
-    ChangeUpdate update = updateFactory.create(rsrc.getControl(), timestamp);
-    update.submit(submitRecords);
-
-    db.changes().beginTransaction(change.getId());
-    try {
-      BatchMetaDataUpdate batch = approve(rsrc.getPatchSet().getId(),
-          cd.changeControl(), update, caller, timestamp);
-      // Write update commit after all normalized label commits.
-      batch.write(update, new CommitBuilder());
-      change = submitToDatabase(db, change.getId(), timestamp);
-      db.commit();
-    } finally {
-      db.rollback();
-    }
-    indexer.index(db, change);
-    return change;
-  }
-
-  private List<Change> submitWholeTopic(RevisionResource rsrc, IdentifiedUser caller,
-      boolean force, String topic) throws ResourceConflictException, OrmException,
-      IOException {
-    Preconditions.checkNotNull(topic);
-    final Timestamp timestamp = TimeUtil.nowTs();
-
-    ReviewDb db = dbProvider.get();
-    ChangeData cd = changeDataFactory.create(db, rsrc.getControl());
-
-    List<ChangeData> changesByTopic = getChangesByTopic(topic);
-    String problems = problemsForSubmittingChanges(changesByTopic, caller);
-    if (problems != null) {
-      throw new ResourceConflictException(problems);
-    }
-
-    Change change = rsrc.getChange();
-    ChangeUpdate update = updateFactory.create(rsrc.getControl(), timestamp);
-
-    List<SubmitRecord> submitRecords = checkSubmitRule(cd,
-        rsrc.getPatchSet(), force);
-    update.submit(submitRecords);
-
-    db.changes().beginTransaction(change.getId());
-    try {
-      for (ChangeData c : changesByTopic) {
-        BatchMetaDataUpdate batch = approve(c.currentPatchSet().getId(),
-            c.changeControl(), update, caller, timestamp);
-        // Write update commit after all normalized label commits.
-        batch.write(update, new CommitBuilder());
-        submitToDatabase(db, c.getId(), timestamp);
-      }
-      db.commit();
-    } finally {
-      db.rollback();
-    }
-    List<Change.Id> ids = new ArrayList<>(changesByTopic.size());
-    List<Change> ret = new ArrayList<>(changesByTopic.size());
-    for (ChangeData c : changesByTopic) {
-      ids.add(c.getId());
-      ret.add(c.change());
-    }
-    indexer.indexAsync(ids).checkedGet();
-
-    return ret;
-  }
-
-  public List<Change> submit(RevisionResource rsrc, IdentifiedUser caller,
-      boolean force) throws ResourceConflictException, OrmException,
-      IOException {
-    String topic = rsrc.getChange().getTopic();
-    if (submitWholeTopic && !Strings.isNullOrEmpty(topic)) {
-      return submitWholeTopic(rsrc, caller, force, topic);
-    } else {
-      return Arrays.asList(submitThisChange(rsrc, caller, force));
-    }
-  }
-
-  private BatchMetaDataUpdate approve(PatchSet.Id psId, ChangeControl control,
-      ChangeUpdate update, IdentifiedUser caller, Timestamp timestamp)
-      throws OrmException {
-    Map<PatchSetApproval.Key, PatchSetApproval> byKey = Maps.newHashMap();
-    for (PatchSetApproval psa :
-        approvalsUtil.byPatchSet(dbProvider.get(), control, psId)) {
-      if (!byKey.containsKey(psa.getKey())) {
-        byKey.put(psa.getKey(), psa);
-      }
-    }
-
-    PatchSetApproval submit = ApprovalsUtil.getSubmitter(psId, byKey.values());
-    if (submit == null
-        || !submit.getAccountId().equals(caller.getAccountId())) {
-      submit = new PatchSetApproval(
-          new PatchSetApproval.Key(
-              psId,
-              caller.getAccountId(),
-              LabelId.SUBMIT),
-          (short) 1, TimeUtil.nowTs());
-      byKey.put(submit.getKey(), submit);
-    }
-    submit.setValue((short) 1);
-    submit.setGranted(timestamp);
-
-    // Flatten out existing approvals for this patch set based upon the current
-    // permissions. Once the change is closed the approvals are not updated at
-    // presentation view time, except for zero votes used to indicate a reviewer
-    // was added. So we need to make sure votes are accurate now. This way if
-    // permissions get modified in the future, historical records stay accurate.
-    LabelNormalizer.Result normalized =
-        labelNormalizer.normalize(control, byKey.values());
-
-    // TODO(dborowitz): Don't use a label in notedb; just check when status
-    // change happened.
-    update.putApproval(submit.getLabel(), submit.getValue());
-
-    dbProvider.get().patchSetApprovals().upsert(normalized.getNormalized());
-    dbProvider.get().patchSetApprovals().delete(normalized.deleted());
-
-    try {
-      return saveToBatch(control, update, normalized, timestamp);
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-  }
-
-  private BatchMetaDataUpdate saveToBatch(ChangeControl ctl,
-      ChangeUpdate callerUpdate, LabelNormalizer.Result normalized,
-      Timestamp timestamp) throws IOException {
-    Table<Account.Id, String, Optional<Short>> byUser = HashBasedTable.create();
-    for (PatchSetApproval psa : normalized.updated()) {
-      byUser.put(psa.getAccountId(), psa.getLabel(),
-          Optional.of(psa.getValue()));
-    }
-    for (PatchSetApproval psa : normalized.deleted()) {
-      byUser.put(psa.getAccountId(), psa.getLabel(), Optional.<Short> absent());
-    }
-
-    BatchMetaDataUpdate batch = callerUpdate.openUpdate();
-    for (Account.Id accountId : byUser.rowKeySet()) {
-      if (!accountId.equals(callerUpdate.getUser().getAccountId())) {
-        ChangeUpdate update = updateFactory.create(
-            ctl.forUser(userFactory.create(dbProvider, accountId)), timestamp);
-        update.setSubject("Finalize approvals at submit");
-        putApprovals(update, byUser.row(accountId));
-
-        CommitBuilder commit = new CommitBuilder();
-        commit.setCommitter(new PersonIdent(serverIdent, timestamp));
-        batch.write(update, commit);
-      }
-    }
-
-    putApprovals(callerUpdate,
-        byUser.row(callerUpdate.getUser().getAccountId()));
-    return batch;
-  }
-
-  private static void putApprovals(ChangeUpdate update,
-      Map<String, Optional<Short>> approvals) {
-    for (Map.Entry<String, Optional<Short>> e : approvals.entrySet()) {
-      if (e.getValue().isPresent()) {
-        update.putApproval(e.getKey(), e.getValue().get());
-      } else {
-        update.removeApproval(e.getKey());
-      }
-    }
-  }
-
   private List<SubmitRecord> checkSubmitRule(ChangeData cd,
       PatchSet patchSet, boolean force)
           throws ResourceConflictException, OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java
index 2dfd2de..d31805d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java
@@ -266,7 +266,7 @@
   }
 
   @AutoValue
-  static abstract class PatchSetData {
+  abstract static class PatchSetData {
     @VisibleForTesting
     static PatchSetData create(ChangeData cd, PatchSet ps, RevCommit commit) {
       return new AutoValue_WalkSorter_PatchSetData(cd, ps, commit);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 82372cc..c8360a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -74,14 +74,12 @@
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitModule;
-import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.ReceivePackInitializer;
-import com.google.gerrit.server.git.SignedPushModule;
-import com.google.gerrit.server.git.SubmoduleOp;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.gpg.SignedPushModule;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.git.validators.MergeValidationListener;
@@ -233,8 +231,6 @@
     bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class)
         .toProvider(DisableReverseDnsLookupProvider.class).in(SINGLETON);
 
-    factory(MergeOp.Factory.class);
-    factory(SubmoduleOp.Factory.class);
     bind(PatchSetInfoFactory.class);
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
     bind(ChangeControl.GenericFactory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
index e4a8c34..5934759 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.base.CharMatcher;
 import com.google.common.base.Function;
 import com.google.common.base.Optional;
+import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GitwebType;
@@ -30,7 +32,7 @@
 import com.google.gerrit.server.change.ArchiveFormat;
 import com.google.gerrit.server.change.GetArchive;
 import com.google.gerrit.server.change.Submit;
-import com.google.gerrit.server.git.SignedPushModule;
+import com.google.gerrit.server.git.gpg.SignedPushModule;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -230,9 +232,18 @@
     info.allUsers = allUsersName.get();
     info.reportBugUrl = cfg.getString("gerrit", null, "reportBugUrl");
     info.reportBugText = cfg.getString("gerrit", null, "reportBugText");
+    info.docUrl = getDocUrl(cfg);
     return info;
   }
 
+  private String getDocUrl(Config cfg) {
+    String docUrl = cfg.getString("gerrit", null, "docUrl");
+    if (Strings.isNullOrEmpty(docUrl)) {
+      return null;
+    }
+    return CharMatcher.is('/').trimTrailingFrom(docUrl) + '/';
+  }
+
   private GitwebInfo getGitwebInfo(GitwebConfig cfg) {
     if (cfg.getUrl() == null || cfg.getGitwebType() == null) {
       return null;
@@ -336,6 +347,7 @@
   public static class GerritInfo {
     public String allProjects;
     public String allUsers;
+    public String docUrl;
     public String reportBugUrl;
     public String reportBugText;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index 3deab11..b907866 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -215,6 +215,34 @@
   }
 
   /**
+   * Called after reading the project config value. To modify the value before
+   * returning it to the client, override this method and return the modified
+   * value. Default implementation returns the same value.
+   *
+   * @param project the project.
+   * @param value the actual value of the config entry (computed out of the
+   *        configured value, the inherited value and the default value).
+   * @return the modified value.
+   */
+  public String onRead(ProjectState project, String value) {
+    return value;
+  }
+
+  /**
+   * Called after reading the project config value of type ARRAY. To modify the
+   * values before returning it to the client, override this method and return
+   * the modified values. Default implementation returns the same values.
+   *
+   * @param project the project.
+   * @param values the actual values of the config entry (computed out of the
+   *        configured value, the inherited value and the default value).
+   * @return the modified values.
+   */
+  public List<String> onRead(ProjectState project, List<String> values) {
+    return values;
+  }
+
+  /**
    * Called after a project config is updated.
    *
    * @param project project name.
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 b906ba7..6c3b499 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
@@ -33,18 +33,22 @@
         ImmutableSetMultimap.builder();
     ImmutableSetMultimap.Builder<Project.NameKey, Change.Id> pcb =
         ImmutableSetMultimap.builder();
+    ImmutableSetMultimap.Builder<Branch.NameKey, Change.Id> cbb =
+        ImmutableSetMultimap.builder();
 
     for (Change c : changes) {
-      Project.NameKey project = c.getDest().getParentKey();
+      Branch.NameKey branch = c.getDest();
+      Project.NameKey project = branch.getParentKey();
       pb.add(project);
-      bb.add(c.getDest());
+      bb.add(branch);
       ib.add(c.getId());
-      pbb.put(project, c.getDest());
+      pbb.put(project, branch);
       pcb.put(project, c.getId());
+      cbb.put(branch, c.getId());
     }
 
     return new AutoValue_ChangeSet(pb.build(), bb.build(),
-        ib.build(), pbb.build(), pcb.build());
+        ib.build(), pbb.build(), pcb.build(), cbb.build());
   }
 
   public static ChangeSet create(Change change) {
@@ -58,6 +62,8 @@
       branchesByProject();
   public abstract ImmutableSetMultimap<Project.NameKey, Change.Id>
       changesByProject();
+  public abstract ImmutableSetMultimap<Branch.NameKey, Change.Id>
+      changesByBranch();
 
   @Override
   public int hashCode() {
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 ef2cef3..cc2ded6 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
@@ -20,10 +20,12 @@
 import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Table;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.SubmitRecord;
@@ -34,6 +36,7 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
@@ -43,15 +46,14 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
 import com.google.gerrit.server.git.validators.MergeValidationException;
@@ -82,13 +84,13 @@
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
 import com.google.inject.Provides;
-import com.google.inject.assistedinject.Assisted;
 import com.google.inject.servlet.RequestScoped;
 
 import com.jcraft.jsch.HostKey;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -105,6 +107,7 @@
 
 import java.io.IOException;
 import java.net.SocketAddress;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -131,10 +134,6 @@
  * be merged cleanly.
  */
 public class MergeOp {
-  public interface Factory {
-    MergeOp create(ChangeSet changes, IdentifiedUser caller);
-  }
-
   private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
 
   private final AccountCache accountCache;
@@ -148,25 +147,24 @@
   private final GitReferenceUpdated gitRefUpdated;
   private final GitRepositoryManager repoManager;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final LabelNormalizer labelNormalizer;
   private final MergedSender.Factory mergedSenderFactory;
   private final MergeSuperSet mergeSuperSet;
   private final MergeValidators.Factory mergeValidatorsFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ProjectCache projectCache;
-  private final Provider<InternalChangeQuery> queryProvider;
+  private final InternalChangeQuery internalChangeQuery;
   private final SchemaFactory<ReviewDb> schemaFactory;
-  private final Submit submit;
+  private final PersonIdent serverIdent;
   private final SubmitStrategyFactory submitStrategyFactory;
-  private final SubmoduleOp.Factory subOpFactory;
+  private final Provider<SubmoduleOp> subOpProvider;
   private final TagCache tagCache;
   private final WorkQueue workQueue;
 
+  private final Map<Change.Id, List<SubmitRecord>> records;
   private final Map<Change.Id, CodeReviewCommit> commits;
-  private final List<Change> toUpdate;
   private final PerThreadRequestScope.Scoper threadScoper;
-  private final ChangeSet changes;
-  private final IdentifiedUser caller;
-  private final String logPrefix;
+  private String logPrefix;
 
   private ProjectState destProject;
   private ReviewDb db;
@@ -192,20 +190,19 @@
       GitReferenceUpdated gitRefUpdated,
       GitRepositoryManager repoManager,
       IdentifiedUser.GenericFactory identifiedUserFactory,
+      LabelNormalizer labelNormalizer,
       MergedSender.Factory mergedSenderFactory,
       MergeSuperSet mergeSuperSet,
       MergeValidators.Factory mergeValidatorsFactory,
       PatchSetInfoFactory patchSetInfoFactory,
       ProjectCache projectCache,
-      Provider<InternalChangeQuery> queryProvider,
+      InternalChangeQuery internalChangeQuery,
       SchemaFactory<ReviewDb> schemaFactory,
-      Submit submit,
+      @GerritPersonIdent PersonIdent serverIdent,
       SubmitStrategyFactory submitStrategyFactory,
-      SubmoduleOp.Factory subOpFactory,
+      Provider<SubmoduleOp> subOpProvider,
       TagCache tagCache,
-      WorkQueue workQueue,
-      @Assisted ChangeSet changes,
-      @Assisted IdentifiedUser caller) {
+      WorkQueue workQueue) {
     this.accountCache = accountCache;
     this.approvalsUtil = approvalsUtil;
     this.changeControlFactory = changeControlFactory;
@@ -217,26 +214,24 @@
     this.gitRefUpdated = gitRefUpdated;
     this.repoManager = repoManager;
     this.identifiedUserFactory = identifiedUserFactory;
+    this.labelNormalizer = labelNormalizer;
     this.mergedSenderFactory = mergedSenderFactory;
     this.mergeSuperSet = mergeSuperSet;
     this.mergeValidatorsFactory = mergeValidatorsFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.projectCache = projectCache;
-    this.queryProvider = queryProvider;
+    this.internalChangeQuery = internalChangeQuery;
     this.schemaFactory = schemaFactory;
-    this.submit = submit;
+    this.serverIdent = serverIdent;
     this.submitStrategyFactory = submitStrategyFactory;
-    this.subOpFactory = subOpFactory;
+    this.subOpProvider = subOpProvider;
     this.tagCache = tagCache;
     this.workQueue = workQueue;
-    this.changes = changes;
-    this.caller = caller;
     commits = new HashMap<>();
-    toUpdate = Lists.newArrayList();
-    logPrefix = String.format("[%s]: ", String.valueOf(changes.hashCode()));
-
     pendingRefUpdates = new HashMap<>();
     openBranches = new HashMap<>();
+    pendingRefUpdates = new HashMap<>();
+    records = new HashMap<>();
     mergeTips = new HashMap<>();
 
     Injector child = injector.createChildInjector(new AbstractModule() {
@@ -393,45 +388,19 @@
       throws ResourceConflictException, OrmException {
     for (Change.Id id : cs.ids()) {
       ChangeData cd = changeDataFactory.create(db, id);
-      if (cd.change().getStatus() != Change.Status.NEW
-          && cd.change().getStatus() != Change.Status.SUBMITTED){
+      if (cd.change().getStatus() != Change.Status.NEW){
         throw new OrmException("Change " + cd.change().getChangeId()
             + " is in state " + cd.change().getStatus());
       } else {
-        checkSubmitRule(cd);
+        records.put(cd.change().getId(), checkSubmitRule(cd));
       }
     }
   }
 
-  // For historic reasons we will first go into the submitted state
-  // TODO(sbeller): remove this when we get rid of Change.Status.SUBMITTED
-  private void submitAllChanges(ChangeSet cs, boolean force)
-      throws OrmException, ResourceConflictException, IOException {
-    for (Change.Id id : cs.ids()) {
-      ChangeData cd = changeDataFactory.create(db, id);
-      switch (cd.change().getStatus()) {
-        case ABANDONED:
-          throw new ResourceConflictException("Change " + cd.getId() +
-              " was abandoned while processing this change set");
-        case DRAFT:
-          throw new ResourceConflictException("Cannot submit draft " + cd.getId());
-        case NEW:
-          RevisionResource rsrc =
-              new RevisionResource(new ChangeResource(cd.changeControl(), null),
-              cd.currentPatchSet());
-          logDebug("Submitting change id {}", cd.change().getId());
-          submit.submit(rsrc, caller, force);
-          break;
-        case MERGED:
-          // we're racing here, but having it already merged is fine.
-        case SUBMITTED:
-          // ok
-      }
-    }
-  }
-
-  public void merge(boolean checkPermissions) throws NoSuchChangeException,
+  public void merge(ChangeSet changes, IdentifiedUser caller,
+      boolean checkPermissions) throws NoSuchChangeException,
       OrmException, ResourceConflictException {
+    logPrefix = String.format("[%s]: ", String.valueOf(changes.hashCode()));
     logDebug("Beginning merge of {}", changes);
     try {
       openSchema();
@@ -439,17 +408,11 @@
       ChangeSet cs = mergeSuperSet.completeChangeSet(db, changes);
       logDebug("Calculated to merge {}", cs);
       if (checkPermissions) {
-        logDebug("Submitting all calculated changes while "
-            + "enforcing submit rules");
-        submitAllChanges(cs, false);
         logDebug("Checking permissions");
         checkPermissions(cs);
-      } else {
-        logDebug("Submitting all calculated changes ignoring submit rules");
-        submitAllChanges(cs, true);
       }
       try {
-        integrateIntoHistory(cs);
+        integrateIntoHistory(cs, caller);
       } catch (MergeException e) {
         logError("Merge Conflict", e);
         throw new ResourceConflictException("Merge Conflict", e);
@@ -464,10 +427,10 @@
     }
   }
 
-  private void integrateIntoHistory(ChangeSet cs)
+  private void integrateIntoHistory(ChangeSet cs, IdentifiedUser caller)
       throws MergeException, NoSuchChangeException, ResourceConflictException {
-    logDebug("Beginning merge attempt on {}", changes);
-    Map<Branch.NameKey, ListMultimap<SubmitType, Change>> toSubmit =
+    logDebug("Beginning merge attempt on {}", cs);
+    Map<Branch.NameKey, ListMultimap<SubmitType, ChangeData>> toSubmit =
         new HashMap<>();
     try {
       openSchema();
@@ -476,30 +439,32 @@
         openRepository(project);
         for (Branch.NameKey branch : cs.branchesByProject().get(project)) {
           setDestProject(branch);
-          ListMultimap<SubmitType, Change> submitting =
-              validateChangeList(queryProvider.get().submitted(branch));
+
+          List<ChangeData> cds = new ArrayList<>();
+          for (Change.Id id : cs.changesByBranch().get(branch)) {
+            cds.add(changeDataFactory.create(db, id));
+          }
+          ListMultimap<SubmitType, ChangeData> submitting =
+              validateChangeList(cds);
           toSubmit.put(branch, submitting);
 
           Set<SubmitType> submitTypes = new HashSet<>(submitting.keySet());
           for (SubmitType submitType : submitTypes) {
             SubmitStrategy strategy = createStrategy(branch, submitType,
-                getBranchTip(branch));
+                getBranchTip(branch), caller);
 
             MergeTip mergeTip = preMerge(strategy, submitting.get(submitType),
                 getBranchTip(branch));
             mergeTips.put(branch, mergeTip);
-            if (submitType != SubmitType.CHERRY_PICK) {
-              // For cherry picking we have relaxed atomic guarantees
-              // as traditionally Gerrit kept going cherry picking if one
-              // failed. We want to keep it for now.
-              updateChangeStatus(submitting.get(submitType), branch, true);
-            }
+            updateChangeStatus(submitting.get(submitType), branch,
+                true, caller);
           }
           inserter.flush();
         }
         closeRepository();
       }
       logDebug("Write out the new branch tips");
+      SubmoduleOp subOp = subOpProvider.get();
       for (Project.NameKey project : cs.projects()) {
         openRepository(project);
         for (Branch.NameKey branch : cs.branchesByProject().get(project)) {
@@ -508,11 +473,11 @@
           pendingRefUpdates.remove(branch);
 
           setDestProject(branch);
-          ListMultimap<SubmitType, Change> submitting = toSubmit.get(branch);
+          ListMultimap<SubmitType, ChangeData> submitting = toSubmit.get(branch);
           for (SubmitType submitType : submitting.keySet()) {
-            updateChangeStatus(submitting.get(submitType), branch, false);
-            updateSubscriptions(branch, submitting.get(submitType),
-                getBranchTip(branch));
+            updateChangeStatus(submitting.get(submitType), branch,
+                false, caller);
+            updateSubmoduleSubscriptions(subOp, branch, getBranchTip(branch));
           }
           if (update != null) {
             fireRefUpdated(branch, update);
@@ -520,6 +485,7 @@
         }
         closeRepository();
       }
+      updateSuperProjects(subOp, cs.branches());
       checkState(pendingRefUpdates.isEmpty(), "programmer error: "
           + "pending ref update list not emptied");
     } catch (NoSuchProjectException noProject) {
@@ -536,15 +502,15 @@
   }
 
   private MergeTip preMerge(SubmitStrategy strategy,
-      List<Change> submitted, CodeReviewCommit branchTip)
-      throws MergeException {
+      List<ChangeData> submitted, CodeReviewCommit branchTip)
+      throws MergeException, OrmException {
     logDebug("Running submit strategy {} for {} commits {}",
         strategy.getClass().getSimpleName(), submitted.size(), submitted);
     List<CodeReviewCommit> toMerge = new ArrayList<>(submitted.size());
-    for (Change c : submitted) {
-      CodeReviewCommit commit = commits.get(c.getId());
+    for (ChangeData cd : submitted) {
+      CodeReviewCommit commit = commits.get(cd.change().getId());
       checkState(commit != null,
-          "commit for %s not found by validateChangeList", c.getId());
+          "commit for %s not found by validateChangeList", cd.change().getId());
       toMerge.add(commit);
     }
     MergeTip mergeTip = strategy.run(branchTip, toMerge);
@@ -555,10 +521,10 @@
   }
 
   private SubmitStrategy createStrategy(Branch.NameKey destBranch,
-      SubmitType submitType, CodeReviewCommit branchTip)
+      SubmitType submitType, CodeReviewCommit branchTip, IdentifiedUser caller)
       throws MergeException, NoSuchProjectException {
     return submitStrategyFactory.create(submitType, db, repo, rw, inserter,
-        canMergeFlag, getAlreadyAccepted(branchTip), destBranch);
+        canMergeFlag, getAlreadyAccepted(branchTip), destBranch, caller);
   }
 
   private void openRepository(Project.NameKey name)
@@ -659,10 +625,10 @@
     return alreadyAccepted;
   }
 
-  private ListMultimap<SubmitType, Change> validateChangeList(
+  private ListMultimap<SubmitType, ChangeData> validateChangeList(
       List<ChangeData> submitted) throws MergeException {
     logDebug("Validating {} changes", submitted.size());
-    ListMultimap<SubmitType, Change> toSubmit = ArrayListMultimap.create();
+    ListMultimap<SubmitType, ChangeData> toSubmit = ArrayListMultimap.create();
 
     Map<String, Ref> allRefs;
     try {
@@ -687,14 +653,13 @@
         throw new MergeException("Failed to validate changes", e);
       }
       Change.Id changeId = cd.getId();
-      if (chg.getStatus() != Change.Status.SUBMITTED) {
-        logDebug("Change {} is not submitted: {}", changeId, chg.getStatus());
+      if (chg.getStatus() != Change.Status.NEW) {
+        logDebug("Change {} is not new: {}", changeId, chg.getStatus());
         continue;
       }
       if (chg.currentPatchSetId() == null) {
         logError("Missing current patch set on change " + changeId);
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
-        toUpdate.add(chg);
         continue;
       }
 
@@ -709,7 +674,6 @@
           || ps.getRevision().get() == null) {
         logError("Missing patch set or revision on change " + changeId);
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
-        toUpdate.add(chg);
         continue;
       }
 
@@ -720,7 +684,6 @@
       } catch (IllegalArgumentException iae) {
         logError("Invalid revision on patch set " + ps.getId());
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
-        toUpdate.add(chg);
         continue;
       }
 
@@ -737,7 +700,6 @@
         logError("Revision " + idstr + " of patch set " + ps.getId()
             + " is not contained in any ref");
         commits.put(changeId, CodeReviewCommit.revisionGone(ctl));
-        toUpdate.add(chg);
         continue;
       }
 
@@ -747,7 +709,6 @@
       } catch (IOException e) {
         logError("Invalid commit " + idstr + " on patch set " + ps.getId(), e);
         commits.put(changeId, CodeReviewCommit.revisionGone(ctl));
-        toUpdate.add(chg);
         continue;
       }
 
@@ -764,7 +725,6 @@
         logDebug("Revision {} of patch set {} failed validation: {}",
             idstr, ps.getId(), mve.getStatus());
         commit.setStatusCode(mve.getStatus());
-        toUpdate.add(chg);
         continue;
       }
 
@@ -774,12 +734,11 @@
         logError("No submit type for revision " + idstr + " of patch set "
             + ps.getId());
         commit.setStatusCode(CommitMergeStatus.NO_SUBMIT_TYPE);
-        toUpdate.add(chg);
         continue;
       }
 
       commit.add(canMergeFlag);
-      toSubmit.put(submitType, chg);
+      toSubmit.put(submitType, cd);
     }
     logDebug("Submitting on this run: {}", toSubmit);
     return toSubmit;
@@ -898,9 +857,10 @@
     return "";
   }
 
-  private void updateChangeStatus(List<Change> submitted,
-      Branch.NameKey destBranch, boolean dryRun)
-      throws NoSuchChangeException, MergeException, ResourceConflictException {
+  private void updateChangeStatus(List<ChangeData> submitted,
+      Branch.NameKey destBranch, boolean dryRun, IdentifiedUser caller)
+      throws NoSuchChangeException, MergeException, ResourceConflictException,
+      OrmException {
     if (!dryRun) {
       logDebug("Updating change status for {} changes", submitted.size());
     } else {
@@ -908,7 +868,8 @@
           submitted.size());
     }
     MergeTip mergeTip = mergeTips.get(destBranch);
-    for (Change c : submitted) {
+    for (ChangeData cd : submitted) {
+      Change c = cd.change();
       CodeReviewCommit commit = commits.get(c.getId());
       CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
       if (s == null) {
@@ -920,6 +881,14 @@
         continue;
       }
 
+      if (!dryRun) {
+        try {
+          setApproval(cd, caller);
+        } catch (IOException e) {
+          throw new OrmException(e);
+        }
+      }
+
       String txt = s.getMessage();
       logDebug("Status of change {} ({}) on {}: {}", c.getId(), commit.name(),
           c.getDest(), s);
@@ -993,26 +962,32 @@
     }
   }
 
-  private void updateSubscriptions(Branch.NameKey destBranch,
-      List<Change> submitted, CodeReviewCommit branchTip) {
+  private void updateSubmoduleSubscriptions(SubmoduleOp subOp,
+      Branch.NameKey destBranch, CodeReviewCommit branchTip) {
     MergeTip mergeTip = mergeTips.get(destBranch);
     if (mergeTip != null
         && (branchTip == null || branchTip != mergeTip.getCurrentTip())) {
-      logDebug("Updating submodule subscriptions for {} changes",
-          submitted.size());
-      SubmoduleOp subOp =
-          subOpFactory.create(destBranch, mergeTip.getCurrentTip(), rw, repo,
-              destProject.getProject(), submitted, commits,
-              getAccount(mergeTip.getCurrentTip()));
+      logDebug("Updating submodule subscriptions for branch {}", destBranch);
       try {
-        subOp.update();
+        subOp.updateSubmoduleSubscriptions(db, destBranch);
       } catch (SubmoduleException e) {
-        logError("The gitLinks were not updated according to the subscriptions",
-            e);
+        logError("The submodule subscriptions were not updated according"
+            + "to the .gitmodules files", e);
       }
     }
   }
 
+  private void updateSuperProjects(SubmoduleOp subOp,
+      Set<Branch.NameKey> branches) {
+    logDebug("Updating superprojects");
+    try {
+      subOp.updateSuperProjects(db, branches);
+    } catch (SubmoduleException e) {
+      logError("The gitlinks were not updated according to the "
+          + "subscriptions", e);
+    }
+  }
+
   private ChangeMessage message(Change c, String body) {
     String uuid;
     try {
@@ -1104,19 +1079,135 @@
     });
   }
 
+  private void setApproval(ChangeData cd, IdentifiedUser user)
+      throws OrmException, IOException {
+    Timestamp timestamp = TimeUtil.nowTs();
+    ChangeControl control = cd.changeControl();
+    PatchSet.Id psId = cd.currentPatchSet().getId();
+    PatchSet.Id psIdNewRev = commits.get(cd.change().getId())
+        .change().currentPatchSetId();
+
+    logDebug("Add approval for " + cd + " from user " + user);
+    ChangeUpdate update = updateFactory.create(control, timestamp);
+    List<SubmitRecord> record = records.get(cd.change().getId());
+    if (record != null) {
+      update.merge(record);
+    }
+    db.changes().beginTransaction(cd.change().getId());
+    try {
+      BatchMetaDataUpdate batch = approve(control, psId, user,
+          update, timestamp);
+      batch.write(update, new CommitBuilder());
+
+      // If the submit strategy created a new revision (rebase, cherry-pick)
+      // approve that as well
+      if (!psIdNewRev.equals(psId)) {
+        batch = approve(control, psIdNewRev, user,
+            update, timestamp);
+        // Write update commit after all normalized label commits.
+        batch.write(update, new CommitBuilder());
+      }
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+    indexer.index(db, cd.change());
+  }
+
+  private BatchMetaDataUpdate approve(ChangeControl control, PatchSet.Id psId,
+      IdentifiedUser user, ChangeUpdate update, Timestamp timestamp)
+          throws OrmException {
+    Map<PatchSetApproval.Key, PatchSetApproval> byKey = Maps.newHashMap();
+    for (PatchSetApproval psa :
+      approvalsUtil.byPatchSet(db, control, psId)) {
+      if (!byKey.containsKey(psa.getKey())) {
+        byKey.put(psa.getKey(), psa);
+      }
+    }
+
+    PatchSetApproval submit = new PatchSetApproval(
+          new PatchSetApproval.Key(
+              psId,
+              user.getAccountId(),
+              LabelId.SUBMIT),
+              (short) 1, TimeUtil.nowTs());
+    byKey.put(submit.getKey(), submit);
+    submit.setValue((short) 1);
+    submit.setGranted(timestamp);
+
+    // Flatten out existing approvals for this patch set based upon the current
+    // permissions. Once the change is closed the approvals are not updated at
+    // presentation view time, except for zero votes used to indicate a reviewer
+    // was added. So we need to make sure votes are accurate now. This way if
+    // permissions get modified in the future, historical records stay accurate.
+    LabelNormalizer.Result normalized =
+        labelNormalizer.normalize(control, byKey.values());
+
+    // TODO(dborowitz): Don't use a label in notedb; just check when status
+    // change happened.
+    update.putApproval(submit.getLabel(), submit.getValue());
+    logDebug("Adding submit label " + submit);
+
+    db.patchSetApprovals().upsert(normalized.getNormalized());
+    db.patchSetApprovals().delete(normalized.deleted());
+
+    try {
+      return saveToBatch(control, update, normalized, timestamp);
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  private BatchMetaDataUpdate saveToBatch(ChangeControl ctl,
+      ChangeUpdate callerUpdate, LabelNormalizer.Result normalized,
+      Timestamp timestamp) throws IOException {
+    Table<Account.Id, String, Optional<Short>> byUser = HashBasedTable.create();
+    for (PatchSetApproval psa : normalized.updated()) {
+      byUser.put(psa.getAccountId(), psa.getLabel(),
+          Optional.of(psa.getValue()));
+    }
+    for (PatchSetApproval psa : normalized.deleted()) {
+      byUser.put(psa.getAccountId(), psa.getLabel(), Optional.<Short> absent());
+    }
+
+    BatchMetaDataUpdate batch = callerUpdate.openUpdate();
+    for (Account.Id accountId : byUser.rowKeySet()) {
+      if (!accountId.equals(callerUpdate.getUser().getAccountId())) {
+        ChangeUpdate update = updateFactory.create(
+            ctl.forUser(identifiedUserFactory.create(accountId)), timestamp);
+        update.setSubject("Finalize approvals at submit");
+        putApprovals(update, byUser.row(accountId));
+
+        CommitBuilder commit = new CommitBuilder();
+        commit.setCommitter(new PersonIdent(serverIdent, timestamp));
+        batch.write(update, commit);
+      }
+    }
+
+    putApprovals(callerUpdate,
+        byUser.row(callerUpdate.getUser().getAccountId()));
+    return batch;
+  }
+
+  private static void putApprovals(ChangeUpdate update,
+      Map<String, Optional<Short>> approvals) {
+    for (Map.Entry<String, Optional<Short>> e : approvals.entrySet()) {
+      if (e.getValue().isPresent()) {
+        update.putApproval(e.getKey(), e.getValue().get());
+      } else {
+        update.removeApproval(e.getKey());
+      }
+    }
+  }
+
   private void sendMergedEmail(final Change c, final PatchSetApproval from) {
     workQueue.getDefaultQueue()
         .submit(new Runnable() {
       @Override
       public void run() {
         PatchSet patchSet;
-        try {
-          ReviewDb reviewDb = schemaFactory.open();
-          try {
-            patchSet = reviewDb.patchSets().get(c.currentPatchSetId());
-          } finally {
-            reviewDb.close();
-          }
+        try (ReviewDb reviewDb = schemaFactory.open()) {
+          patchSet = reviewDb.patchSets().get(c.currentPatchSetId());
         } catch (Exception e) {
           logError("Cannot send email for submitted patch set " + c.getId(), e);
           return;
@@ -1208,8 +1299,7 @@
       throws NoSuchChangeException {
     try {
       openSchema();
-      for (ChangeData cd
-          : queryProvider.get().byProjectOpen(destProject)) {
+      for (ChangeData cd : internalChangeQuery.byProjectOpen(destProject)) {
         abandonOneChange(cd.change());
       }
       db.close();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index da38a58..c5ead54 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -71,16 +71,13 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
-import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.TimeZone;
 
 public class MergeUtil {
   private static final Logger log = LoggerFactory.getLogger(MergeUtil.class);
@@ -167,10 +164,6 @@
     return result;
   }
 
-  public PatchSetApproval getSubmitter(CodeReviewCommit c) {
-    return approvalsUtil.getSubmitter(db.get(), c.notes(), c.getPatchsetId());
-  }
-
   public RevCommit createCherryPickFromCommit(Repository repo,
       ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit,
       PersonIdent cherryPickCommitterIdent, String commitMsg, RevWalk rw)
@@ -347,52 +340,6 @@
     return false;
   }
 
-  public PersonIdent computeMergeCommitAuthor(final PersonIdent myIdent,
-      final RevWalk rw, final List<CodeReviewCommit> codeReviewCommits) {
-    PatchSetApproval submitter = null;
-    for (final CodeReviewCommit c : codeReviewCommits) {
-      PatchSetApproval s = getSubmitter(c);
-      if (submitter == null
-          || (s != null && s.getGranted().compareTo(submitter.getGranted()) > 0)) {
-        submitter = s;
-      }
-    }
-
-    // Try to use the submitter's identity for the merge commit author.
-    // If all of the commits being merged are created by the submitter,
-    // prefer the identity line they used in the commits rather than the
-    // preferred identity stored in the user account. This way the Git
-    // commit records are more consistent internally.
-    //
-    PersonIdent authorIdent;
-    if (submitter != null) {
-      IdentifiedUser who =
-          identifiedUserFactory.create(submitter.getAccountId());
-      Set<String> emails = new HashSet<>();
-      for (RevCommit c : codeReviewCommits) {
-        try {
-          rw.parseBody(c);
-        } catch (IOException e) {
-          log.warn("Cannot parse commit " + c.name(), e);
-          continue;
-        }
-        emails.add(c.getAuthorIdent().getEmailAddress());
-      }
-
-      final Timestamp dt = submitter.getGranted();
-      final TimeZone tz = myIdent.getTimeZone();
-      if (emails.size() == 1 && who.hasEmailAddress(emails.iterator().next())) {
-        authorIdent =
-            new PersonIdent(codeReviewCommits.get(0).getAuthorIdent(), dt, tz);
-      } else {
-        authorIdent = who.newCommitterIdent(dt, tz);
-      }
-    } else {
-      authorIdent = myIdent;
-    }
-    return authorIdent;
-  }
-
   public boolean canMerge(final MergeSorter mergeSorter,
       final Repository repo, final CodeReviewCommit mergeTip,
       final CodeReviewCommit toMerge)
@@ -497,16 +444,15 @@
     };
   }
 
-  public CodeReviewCommit mergeOneCommit(final PersonIdent myIdent,
-      final Repository repo, final RevWalk rw, final ObjectInserter inserter,
-      final RevFlag canMergeFlag, final Branch.NameKey destBranch,
-      final CodeReviewCommit mergeTip, final CodeReviewCommit n)
-      throws MergeException {
+  public CodeReviewCommit mergeOneCommit(PersonIdent author,
+      PersonIdent committer, Repository repo, RevWalk rw,
+      ObjectInserter inserter, RevFlag canMergeFlag, Branch.NameKey destBranch,
+      CodeReviewCommit mergeTip, CodeReviewCommit n) throws MergeException {
     final ThreeWayMerger m = newThreeWayMerger(repo, inserter);
     try {
       if (m.merge(new AnyObjectId[] {mergeTip, n})) {
-        return writeMergeCommit(myIdent, rw, inserter, canMergeFlag, destBranch,
-            mergeTip, m.getResultTreeId(), n);
+        return writeMergeCommit(author, committer, rw, inserter, canMergeFlag,
+            destBranch, mergeTip, m.getResultTreeId(), n);
       } else {
         failed(rw, canMergeFlag, mergeTip, n, CommitMergeStatus.PATH_CONFLICT);
       }
@@ -549,12 +495,12 @@
     return failed;
   }
 
-  public CodeReviewCommit writeMergeCommit(final PersonIdent myIdent,
-      final RevWalk rw, final ObjectInserter inserter,
-      final RevFlag canMergeFlag, final Branch.NameKey destBranch,
-      final CodeReviewCommit mergeTip, final ObjectId treeId,
-      final CodeReviewCommit n) throws IOException,
-      MissingObjectException, IncorrectObjectTypeException {
+  public CodeReviewCommit writeMergeCommit(PersonIdent author,
+      PersonIdent committer, RevWalk rw, ObjectInserter inserter,
+      RevFlag canMergeFlag, Branch.NameKey destBranch,
+      CodeReviewCommit mergeTip, ObjectId treeId, CodeReviewCommit n)
+      throws IOException, MissingObjectException,
+      IncorrectObjectTypeException {
     final List<CodeReviewCommit> merged = new ArrayList<>();
     rw.resetRetain(canMergeFlag);
     rw.markStart(n);
@@ -582,13 +528,11 @@
       }
     }
 
-    PersonIdent authorIdent = computeMergeCommitAuthor(myIdent, rw, merged);
-
     final CommitBuilder mergeCommit = new CommitBuilder();
     mergeCommit.setTreeId(treeId);
     mergeCommit.setParentIds(mergeTip, n);
-    mergeCommit.setAuthor(authorIdent);
-    mergeCommit.setCommitter(myIdent);
+    mergeCommit.setAuthor(author);
+    mergeCommit.setCommitter(committer);
     mergeCommit.setMessage(msgbuf.toString());
 
     CodeReviewCommit mergeResult =
@@ -691,7 +635,7 @@
     return id;
   }
 
-  public PatchSetApproval markCleanMerges(final RevWalk rw,
+  public void markCleanMerges(final RevWalk rw,
       final RevFlag canMergeFlag, final CodeReviewCommit mergeTip,
       final Set<RevCommit> alreadyAccepted) throws MergeException {
     if (mergeTip == null) {
@@ -699,12 +643,10 @@
       // at the start of the merge process. We also elected to merge nothing,
       // probably due to missing dependencies. Nothing was cleanly merged.
       //
-      return null;
+      return;
     }
 
     try {
-      PatchSetApproval submitApproval = null;
-
       rw.resetRetain(canMergeFlag);
       rw.sort(RevSort.TOPO);
       rw.sort(RevSort.REVERSE, true);
@@ -717,13 +659,8 @@
       while ((c = (CodeReviewCommit) rw.next()) != null) {
         if (c.getPatchsetId() != null) {
           c.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
-          if (submitApproval == null) {
-            submitApproval = getSubmitter(c);
-          }
         }
       }
-
-      return submitApproval;
     } catch (IOException e) {
       throw new MergeException("Cannot mark clean merges", e);
     }
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 f1f89a5..c21e8c0 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
@@ -325,9 +325,9 @@
   private SetMultimap<ObjectId, Ref> refsById;
   private Map<String, Ref> allRefs;
 
-  private final SubmoduleOp.Factory subOpFactory;
+  private final Provider<SubmoduleOp> subOpProvider;
   private final Provider<Submit> submitProvider;
-  private final MergeOp.Factory mergeFactory;
+  private final Provider<MergeOp> mergeOpProvider;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final NotesMigration notesMigration;
   private final ChangeEditUtil editUtil;
@@ -375,9 +375,9 @@
       ReceiveConfig config,
       @Assisted final ProjectControl projectControl,
       @Assisted final Repository repo,
-      final SubmoduleOp.Factory subOpFactory,
+      final Provider<SubmoduleOp> subOpProvider,
       final Provider<Submit> submitProvider,
-      final MergeOp.Factory mergeFactory,
+      final Provider<MergeOp> mergeOpProvider,
       final ChangeKindCache changeKindCache,
       final DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       final NotesMigration notesMigration,
@@ -422,9 +422,9 @@
     this.rp = new ReceivePack(repo);
     this.rejectCommits = BanCommit.loadRejectCommitsMap(repo, rp.getRevWalk());
 
-    this.subOpFactory = subOpFactory;
+    this.subOpProvider = subOpProvider;
     this.submitProvider = submitProvider;
-    this.mergeFactory = mergeFactory;
+    this.mergeOpProvider = mergeOpProvider;
     this.pluginConfigEntries = pluginConfigEntries;
     this.notesMigration = notesMigration;
 
@@ -598,6 +598,7 @@
       rp.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER);
     }
 
+    Set<Branch.NameKey> branches = Sets.newHashSet();
     for (final ReceiveCommand c : commands) {
         if (c.getResult() == OK) {
           if (c.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward
@@ -613,6 +614,8 @@
               case UPDATE:
               case UPDATE_NONFASTFORWARD:
                 autoCloseChanges(c);
+                branches.add(new Branch.NameKey(project.getNameKey(),
+                    c.getRefName()));
                 break;
 
               case DELETE:
@@ -651,6 +654,16 @@
           }
         }
     }
+    // Update superproject gitlinks if required.
+    SubmoduleOp op = subOpProvider.get();
+    try {
+       op.updateSubmoduleSubscriptions(db, branches);
+       op.updateSuperProjects(db, branches);
+    } catch (SubmoduleException e) {
+      log.error("Can't update submodule subscriptions "
+          + "or update the superprojects", e);
+    }
+
     closeProgress.end();
     commandProgress.end();
     progress.end();
@@ -785,7 +798,7 @@
     try {
       List<CheckedFuture<?, RestApiException>> futures = Lists.newArrayList();
       for (ReplaceRequest replace : replaceByChange.values()) {
-        if (magicBranch != null && replace.inputCommand == magicBranch.cmd) {
+        if (replace.inputCommand == magicBranch.cmd) {
           futures.add(replace.insertPatchSet());
         }
       }
@@ -1769,19 +1782,13 @@
   }
 
   private void submit(ChangeControl changeCtl, PatchSet ps)
-      throws OrmException, IOException, ResourceConflictException {
+      throws OrmException, ResourceConflictException {
     Submit submit = submitProvider.get();
     RevisionResource rsrc = new RevisionResource(changes.parse(changeCtl), ps);
-    List<Change> changes;
+    List<Change> changes = Lists.newArrayList(rsrc.getChange());
     try {
-      // Force submit even if submit rule evaluation fails.
-      changes = submit.submit(rsrc, currentUser, true);
-    } catch (ResourceConflictException e) {
-      throw new IOException(e);
-    }
-    try {
-      mergeFactory.create(ChangeSet.create(changes),
-          (IdentifiedUser) changeCtl.getCurrentUser()).merge(false);
+      mergeOpProvider.get().merge(ChangeSet.create(changes),
+          (IdentifiedUser) changeCtl.getCurrentUser(), false);
     } catch (NoSuchChangeException e) {
       throw new OrmException(e);
     }
@@ -1789,9 +1796,6 @@
     for (Change c : changes) {
       c = db.changes().get(c.getId());
       switch (c.getStatus()) {
-        case SUBMITTED:
-          addMessage("Change " + c.getChangeId() + " submitted.");
-          break;
         case MERGED:
           addMessage("Change " + c.getChangeId() + " merged.");
           break;
@@ -2592,19 +2596,10 @@
           closeProgress.update(1);
         }
       }
-
-      // Update superproject gitlinks if required.
-      subOpFactory.create(
-          branch, newTip, rw, repo, project,
-          new ArrayList<Change>(),
-          new HashMap<Change.Id, CodeReviewCommit>(),
-          currentUser.getAccount()).update();
     } catch (RestApiException e) {
       log.error("Can't insert patchset", e);
     } catch (IOException | OrmException e) {
       log.error("Can't scan for changes to close", e);
-    } catch (SubmoduleException e) {
-      log.error("Can't complete git links check", e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SignedPushPreReceiveHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SignedPushPreReceiveHook.java
deleted file mode 100644
index 671c109..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SignedPushPreReceiveHook.java
+++ /dev/null
@@ -1,314 +0,0 @@
-// 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 org.bouncycastle.openpgp.PGPSignature.CERTIFICATION_REVOCATION;
-import static org.bouncycastle.openpgp.PGPSignature.DEFAULT_CERTIFICATION;
-import static org.bouncycastle.openpgp.PGPSignature.POSITIVE_CERTIFICATION;
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import org.bouncycastle.bcpg.ArmoredInputStream;
-import org.bouncycastle.openpgp.PGPException;
-import org.bouncycastle.openpgp.PGPObjectFactory;
-import org.bouncycastle.openpgp.PGPPublicKey;
-import org.bouncycastle.openpgp.PGPPublicKeyRing;
-import org.bouncycastle.openpgp.PGPSignature;
-import org.bouncycastle.openpgp.PGPSignatureList;
-import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
-import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.notes.Note;
-import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.PreReceiveHook;
-import org.eclipse.jgit.transport.PushCertificate;
-import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
-import org.eclipse.jgit.transport.PushCertificateIdent;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.transport.ReceivePack;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.nio.ByteBuffer;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-
-/**
- * Pre-receive hook to validate signed pushes.
- * <p>
- * If configured, prior to processing any push using {@link ReceiveCommits},
- * requires that any push certificate present must be valid.
- */
-@Singleton
-public class SignedPushPreReceiveHook implements PreReceiveHook {
-  private static final Logger log =
-      LoggerFactory.getLogger(SignedPushPreReceiveHook.class);
-
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsers;
-
-  @Inject
-  public SignedPushPreReceiveHook(
-      GitRepositoryManager repoManager,
-      AllUsersName allUsers) {
-    this.repoManager = repoManager;
-    this.allUsers = allUsers;
-  }
-
-  @Override
-  public void onPreReceive(ReceivePack rp,
-      Collection<ReceiveCommand> commands) {
-    try (Writer msgOut = new OutputStreamWriter(rp.getMessageOutputStream())) {
-      PushCertificate cert = rp.getPushCertificate();
-      if (cert == null) {
-        return;
-      }
-      if (cert.getNonceStatus() != NonceStatus.OK) {
-        rejectInvalid(commands);
-        return;
-      }
-      verifySignature(cert, commands, msgOut);
-    } catch (IOException e) {
-      log.error("Error verifying push certificate", e);
-      reject(commands, "push cert error");
-    }
-  }
-
-  private void verifySignature(PushCertificate cert,
-      Collection<ReceiveCommand> commands, Writer msgOut) throws IOException {
-    PGPSignature sig = readSignature(cert);
-    if (sig == null) {
-      msgOut.write("Invalid signature format\n");
-      rejectInvalid(commands);
-      return;
-    }
-    PGPPublicKey key = readPublicKey(sig.getKeyID(), cert.getPusherIdent());
-    if (key == null) {
-      msgOut.write("No valid public key found for ID "
-          + keyIdToString(sig.getKeyID()) + "\n");
-      rejectInvalid(commands);
-      return;
-    }
-    try {
-      sig.init(new BcPGPContentVerifierBuilderProvider(), key);
-      sig.update(Constants.encode(cert.toText()));
-      if (!sig.verify()) {
-        msgOut.write("Push certificate signature does not match\n");
-        rejectInvalid(commands);
-      }
-      return;
-    } catch (PGPException e) {
-      msgOut.write(
-          "Push certificate verification error: " + e.getMessage() + "\n");
-      rejectInvalid(commands);
-      return;
-    }
-  }
-
-  private PGPSignature readSignature(PushCertificate cert) throws IOException {
-    ArmoredInputStream in = new ArmoredInputStream(
-        new ByteArrayInputStream(Constants.encode(cert.getSignature())));
-    PGPObjectFactory factory = new BcPGPObjectFactory(in);
-    PGPSignature sig = null;
-
-    Object obj;
-    while ((obj = factory.nextObject()) != null) {
-      if (!(obj instanceof PGPSignatureList)) {
-        log.error("Unexpected packet in push cert: {}",
-            obj.getClass().getSimpleName());
-        return null;
-      }
-      if (sig != null) {
-        log.error("Multiple signature packets found in push cert");
-        return null;
-      }
-      PGPSignatureList sigs = (PGPSignatureList) obj;
-      if (sigs.size() != 1) {
-        log.error("Expected 1 signature in push cert, found {}", sigs.size());
-        return null;
-      }
-      sig = sigs.get(0);
-    }
-    return sig;
-  }
-
-  private PGPPublicKey readPublicKey(long keyId,
-      PushCertificateIdent expectedIdent) throws IOException {
-    try (Repository repo = repoManager.openRepository(allUsers);
-        RevWalk rw = new RevWalk(repo)) {
-      Ref ref = repo.getRefDatabase().exactRef(RefNames.REFS_GPG_KEYS);
-      if (ref == null) {
-        return null;
-      }
-      NoteMap notes = NoteMap.read(
-          rw.getObjectReader(), rw.parseCommit(ref.getObjectId()));
-      Note note = notes.getNote(keyObjectId(keyId));
-      if (note == null) {
-        return null;
-      }
-
-      try (InputStream objIn =
-              rw.getObjectReader().open(note.getData(), OBJ_BLOB).openStream();
-          ArmoredInputStream in = new ArmoredInputStream(objIn)) {
-        PGPObjectFactory factory = new BcPGPObjectFactory(in);
-        PGPPublicKey matched = null;
-        Object obj;
-        while ((obj = factory.nextObject()) != null) {
-          if (!(obj instanceof PGPPublicKeyRing)) {
-            // TODO(dborowitz): Support assertions signed by a trusted key.
-            log.info("Ignoring {} packet in {}",
-                obj.getClass().getSimpleName(), note.getName());
-            continue;
-          }
-          PGPPublicKeyRing keyRing = (PGPPublicKeyRing) obj;
-          PGPPublicKey key = keyRing.getPublicKey(keyId);
-          if (key == null) {
-            log.warn("Public key ring in {} does not contain key ID {}",
-                note.getName(), keyObjectId(keyId));
-            continue;
-          }
-          if (matched != null) {
-            // TODO(dborowitz): Try all keys.
-            log.warn("Ignoring key with duplicate ID: {}", toString(key));
-            continue;
-          }
-          if (!verifyPublicKey(key, expectedIdent)) {
-            continue;
-          }
-          matched = key;
-        }
-        return matched;
-      }
-    }
-  }
-
-  private boolean verifyPublicKey(PGPPublicKey key,
-      PushCertificateIdent ident) {
-    if (key.isRevoked()) {
-      // TODO(dborowitz): isRevoked is overeager:
-      // http://www.bouncycastle.org/jira/browse/BJB-45
-      log.warn("Key is revoked: {}", toString(key));
-      return false;
-    } else if (key.getValidSeconds() == 0) {
-      log.warn("Key is expired: {}", toString(key));
-      return false;
-    }
-    return verifyPublicKeyCertifications(key, ident);
-  }
-
-  private boolean verifyPublicKeyCertifications(PGPPublicKey key,
-      PushCertificateIdent ident) {
-    @SuppressWarnings("unchecked")
-    Iterator<PGPSignature> sigs = key.getSignaturesForID(ident.getUserId());
-    if (sigs == null) {
-      sigs = Collections.emptyIterator();
-    }
-    boolean valid = false;
-    boolean revoked = false;
-    try {
-      while (sigs.hasNext()) {
-        PGPSignature sig = sigs.next();
-        if (sig.getKeyID() != key.getKeyID()) {
-          // TODO(dborowitz): Support certifications by other trusted keys?
-          continue;
-        } else if (sig.getSignatureType() != DEFAULT_CERTIFICATION
-            && sig.getSignatureType() != POSITIVE_CERTIFICATION
-            && sig.getSignatureType() != CERTIFICATION_REVOCATION) {
-          continue;
-        }
-        sig.init(new BcPGPContentVerifierBuilderProvider(), key);
-        if (sig.verifyCertification(ident.getUserId(), key)) {
-          if (sig.getSignatureType() == CERTIFICATION_REVOCATION) {
-            revoked = true;
-          } else {
-            valid = true;
-          }
-        } else {
-          log.warn("Invalid signature for pusher identity {} in key: {}",
-              ident.getUserId(), toString(key));
-        }
-      }
-    } catch (PGPException e) {
-      log.warn("Error in signature verification for public key", e);
-    }
-
-    if (revoked) {
-      log.warn("Pusher identity {} is revoked in key {}",
-          ident.getUserId(), toString(key));
-      return false;
-    } else if (!valid) {
-      log.warn(
-          "Key does not contain valid certification for pusher identity {}: {}",
-          ident.getUserId(), toString(key));
-      return false;
-    }
-    return true;
-  }
-
-  static ObjectId keyObjectId(long keyId) {
-    // Right-pad key IDs in network byte order to ObjectId length. This allows
-    // us to reuse the fanout code in NoteMap for free. (If we ever fix the
-    // fanout code to work with variable-length byte strings, we will need to
-    // fall back to this key format during a transition period.)
-    ByteBuffer buf = ByteBuffer.wrap(new byte[Constants.OBJECT_ID_LENGTH]);
-    buf.putLong(keyId);
-    return ObjectId.fromRaw(buf.array());
-  }
-
-  static String toString(PGPPublicKey key) {
-    @SuppressWarnings("unchecked")
-    Iterator<String> it = key.getUserIDs();
-    ByteBuffer buf = ByteBuffer.wrap(key.getFingerprint());
-    return String.format(
-        "%s %s(%04X %04X %04X %04X %04X  %04X %04X %04X %04X %04X)",
-        keyIdToString(key.getKeyID()),
-        it.hasNext() ? it.next() + " " : "",
-        buf.getShort(), buf.getShort(), buf.getShort(), buf.getShort(),
-        buf.getShort(), buf.getShort(), buf.getShort(), buf.getShort(),
-        buf.getShort(), buf.getShort());
-  }
-
-  private static void reject(Collection<ReceiveCommand> commands,
-      String reason) {
-    for (ReceiveCommand cmd : commands) {
-      if (cmd.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
-        cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, reason);
-      }
-    }
-  }
-
-  static String keyIdToString(long keyId) {
-    // Match key ID format from gpg --list-keys.
-    return String.format("%08X", (int) keyId);
-  }
-
-  private static void rejectInvalid(Collection<ReceiveCommand> commands) {
-    reject(commands, "invalid push cert");
-  }
-}
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 0f66da4..34370f9 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
@@ -14,15 +14,13 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -30,14 +28,13 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.util.SubmoduleSectionParser;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -62,67 +59,34 @@
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 public class SubmoduleOp {
-  public interface Factory {
-    SubmoduleOp create(Branch.NameKey destBranch, RevCommit mergeTip,
-        RevWalk rw, Repository db, Project destProject, List<Change> submitted,
-        Map<Change.Id, CodeReviewCommit> commits, Account account);
-  }
-
   private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class);
   private static final String GIT_MODULES = ".gitmodules";
 
-  private final Branch.NameKey destBranch;
-  private RevCommit mergeTip;
-  private RevWalk rw;
   private final Provider<String> urlProvider;
-  private ReviewDb schema;
-  private Repository db;
-  private Project destProject;
-  private List<Change> submitted;
-  private final Map<Change.Id, CodeReviewCommit> commits;
   private final PersonIdent myIdent;
   private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated gitRefUpdated;
-  private final SchemaFactory<ReviewDb> schemaFactory;
   private final Set<Branch.NameKey> updatedSubscribers;
   private final Account account;
   private final ChangeHooks changeHooks;
   private final SubmoduleSectionParser.Factory subSecParserFactory;
 
   @Inject
-  public SubmoduleOp(@Assisted Branch.NameKey destBranch,
-      @Assisted RevCommit mergeTip,
-      @Assisted RevWalk rw,
+  public SubmoduleOp(
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      SchemaFactory<ReviewDb> sf,
-      @Assisted Repository db,
-      @Assisted Project destProject,
-      @Assisted List<Change> submitted,
-      @Assisted Map<Change.Id,
-      CodeReviewCommit> commits,
       @GerritPersonIdent PersonIdent myIdent,
       GitRepositoryManager repoManager,
       GitReferenceUpdated gitRefUpdated,
-      @Nullable @Assisted Account account,
+      @Nullable Account account,
       ChangeHooks changeHooks,
       SubmoduleSectionParser.Factory subSecParserFactory) {
-    this.destBranch = destBranch;
-    this.mergeTip = mergeTip;
-    this.rw = rw;
     this.urlProvider = urlProvider;
-    this.schemaFactory = sf;
-    this.db = db;
-    this.destProject = destProject;
-    this.submitted = submitted;
-    this.commits = commits;
     this.myIdent = myIdent;
     this.repoManager = repoManager;
     this.gitRefUpdated = gitRefUpdated;
@@ -133,49 +97,42 @@
     updatedSubscribers = new HashSet<>();
   }
 
-  public void update() throws SubmoduleException {
-    try {
-      schema = schemaFactory.open();
-
-      updateSubmoduleSubscriptions();
-      updateSuperProjects(destBranch, rw, mergeTip.getId().toObjectId(), null);
-    } catch (OrmException | IOException e) {
-      throw new SubmoduleException("Cannot open database", e);
-    } finally {
-      if (schema != null) {
-        schema.close();
-        schema = null;
-      }
+  void updateSubmoduleSubscriptions(ReviewDb db, Set<Branch.NameKey> branches)
+      throws SubmoduleException {
+    for (Branch.NameKey branch : branches) {
+      updateSubmoduleSubscriptions(db, branch);
     }
   }
 
-  private void updateSubmoduleSubscriptions() throws SubmoduleException {
+  void updateSubmoduleSubscriptions(ReviewDb db, Branch.NameKey destBranch)
+      throws SubmoduleException {
     if (urlProvider.get() == null) {
-      logAndThrowSubmoduleException("Cannot establish canonical web url used to access gerrit."
-              + " It should be provided in gerrit.config file.");
+      logAndThrowSubmoduleException("Cannot establish canonical web url used "
+          + "to access gerrit. It should be provided in gerrit.config file.");
     }
+    try (Repository repo = repoManager.openRepository(
+            destBranch.getParentKey());
+        RevWalk rw = new RevWalk(repo)) {
 
-    try {
+      ObjectId id = repo.resolve(destBranch.get());
+      RevCommit commit = rw.parseCommit(id);
+
       Set<SubmoduleSubscription> oldSubscriptions =
-          Sets.newHashSet(schema.submoduleSubscriptions()
+          Sets.newHashSet(db.submoduleSubscriptions()
               .bySuperProject(destBranch));
 
       Set<SubmoduleSubscription> newSubscriptions;
-      TreeWalk tw = TreeWalk.forPath(db, GIT_MODULES, mergeTip.getTree());
+      TreeWalk tw = TreeWalk.forPath(repo, GIT_MODULES, commit.getTree());
       if (tw != null
           && (FileMode.REGULAR_FILE.equals(tw.getRawMode(0)) ||
               FileMode.EXECUTABLE_FILE.equals(tw.getRawMode(0)))) {
         BlobBasedConfig bbc =
-            new BlobBasedConfig(null, db, mergeTip, GIT_MODULES);
+            new BlobBasedConfig(null, repo, commit, GIT_MODULES);
 
         String thisServer = new URI(urlProvider.get()).getHost();
 
-        Branch.NameKey target =
-            new Branch.NameKey(new Project.NameKey(destProject.getName()),
-                destBranch.get());
-
-        newSubscriptions = subSecParserFactory.create(bbc, thisServer, target)
-            .parseAllSections();
+        newSubscriptions = subSecParserFactory.create(bbc, thisServer,
+            destBranch).parseAllSections();
       } else {
         newSubscriptions = Collections.emptySet();
       }
@@ -191,10 +148,10 @@
       newSubscriptions.removeAll(alreadySubscribeds);
 
       if (!oldSubscriptions.isEmpty()) {
-        schema.submoduleSubscriptions().delete(oldSubscriptions);
+        db.submoduleSubscriptions().delete(oldSubscriptions);
       }
       if (!newSubscriptions.isEmpty()) {
-        schema.submoduleSubscriptions().insert(newSubscriptions);
+        db.submoduleSubscriptions().insert(newSubscriptions);
       }
 
     } catch (OrmException e) {
@@ -216,73 +173,31 @@
     }
   }
 
-  private void updateSuperProjects(final Branch.NameKey updatedBranch, RevWalk myRw,
-      final ObjectId mergedCommit, final String msg) throws SubmoduleException,
-      IOException {
+  protected void updateSuperProjects(ReviewDb db,
+      Set<Branch.NameKey> updatedBranches) throws SubmoduleException {
     try {
-      final List<SubmoduleSubscription> subscribers =
-          schema.submoduleSubscriptions().bySubmodule(updatedBranch).toList();
+      // These (repo/branch) will be updated later with all the given
+      // individual submodule subscriptions
+      Multimap<Branch.NameKey, SubmoduleSubscription> targets =
+          HashMultimap.create();
 
-      if (!subscribers.isEmpty()) {
-        // Initialize the message buffer
-        StringBuilder sb = new StringBuilder();
-        if (msg != null) {
-          sb.append(msg);
-        } else {
-          // The first updatedBranch on a cascade event of automatic
-          // updates of repos is added to updatedSubscribers set so
-          // if we face a situation having
-          // submodule-a(master)-->super(master)-->submodule-a(master),
-          // it will be detected we have a circular subscription
-          // when updateSuperProjects is called having as updatedBranch
-          // the super(master) value.
-          updatedSubscribers.add(updatedBranch);
-
-          for (final Change chg : submitted) {
-            final CodeReviewCommit c = commits.get(chg.getId());
-            if (c != null
-                && (c.getStatusCode() == CommitMergeStatus.CLEAN_MERGE
-                    || c.getStatusCode() == CommitMergeStatus.CLEAN_PICK
-                    || c.getStatusCode() == CommitMergeStatus.CLEAN_REBASE)) {
-              myRw.parseBody(c);
-              sb.append("\n")
-                .append(c.getFullMessage());
-            }
-          }
+      for (Branch.NameKey updatedBranch : updatedBranches) {
+        for (SubmoduleSubscription sub : db.submoduleSubscriptions()
+            .bySubmodule(updatedBranch)) {
+          targets.put(sub.getSuperProject(), sub);
         }
-
-        // update subscribers of this module
-        List<SubmoduleSubscription> incorrectSubscriptions = Lists.newLinkedList();
-        for (final SubmoduleSubscription s : subscribers) {
-          try {
-            if (!updatedSubscribers.add(s.getSuperProject())) {
-              log.error("Possible circular subscription involving " + s);
-            } else {
-
-            Map<Branch.NameKey, ObjectId> modules = new HashMap<>(1);
-              modules.put(updatedBranch, mergedCommit);
-
-            Map<Branch.NameKey, String> paths = new HashMap<>(1);
-              paths.put(updatedBranch, s.getPath());
-              updateGitlinks(s.getSuperProject(), myRw, modules, paths, sb.toString());
-            }
-          } catch (SubmoduleException e) {
-              log.warn("Cannot update gitlinks for " + s + " due to " + e.getMessage());
-              incorrectSubscriptions.add(s);
-          } catch (Exception e) {
-              log.error("Cannot update gitlinks for " + s, e);
+      }
+      updatedSubscribers.addAll(updatedBranches);
+      // Update subscribers.
+      for (Branch.NameKey dest : targets.keySet()) {
+        try {
+          if (!updatedSubscribers.add(dest)) {
+            log.error("Possible circular subscription involving " + dest);
+          } else {
+            updateGitlinks(db, dest, targets.get(dest));
           }
-        }
-
-        if (!incorrectSubscriptions.isEmpty()) {
-          try {
-            schema.submoduleSubscriptions().delete(incorrectSubscriptions);
-            log.info("Deleted incorrect submodule subscription(s) "
-                + incorrectSubscriptions);
-          } catch (OrmException e) {
-            log.error("Cannot delete submodule subscription(s) "
-                + incorrectSubscriptions, e);
-          }
+        } catch (SubmoduleException e) {
+          log.warn("Cannot update gitlinks for " + dest, e);
         }
       }
     } catch (OrmException e) {
@@ -290,78 +205,109 @@
     }
   }
 
-  private void updateGitlinks(final Branch.NameKey subscriber, RevWalk myRw,
-      final Map<Branch.NameKey, ObjectId> modules,
-      final Map<Branch.NameKey, String> paths, final String msg)
-      throws SubmoduleException {
+  /**
+   * Update the submodules in one branch of one repository.
+   *
+   * @param subscriber the branch of the repository which should be changed.
+   * @param updates submodule updates which should be updated to.
+   * @throws SubmoduleException
+   */
+  private void updateGitlinks(ReviewDb db, Branch.NameKey subscriber,
+      Collection<SubmoduleSubscription> updates) throws SubmoduleException {
     PersonIdent author = null;
 
-    final StringBuilder msgbuf = new StringBuilder("Updated git submodules\n");
     Repository pdb = null;
     RevWalk recRw = null;
 
+    StringBuilder msgbuf = new StringBuilder("Updated git submodules\n\n");
     try {
       boolean sameAuthorForAll = true;
 
-      for (final Map.Entry<Branch.NameKey, ObjectId> me : modules.entrySet()) {
-        RevCommit c = myRw.parseCommit(me.getValue());
-        if (c == null) {
-          continue;
-        }
-
-        msgbuf.append("\nProject: ");
-        msgbuf.append(me.getKey().getParentKey().get());
-        msgbuf.append("  ").append(me.getValue().getName());
-        msgbuf.append("\n");
-        if (modules.size() == 1) {
-          if (!Strings.isNullOrEmpty(msg)) {
-            msgbuf.append(msg);
-          } else {
-            msgbuf.append("\n");
-            msgbuf.append(c.getFullMessage());
-          }
-        } else {
-          msgbuf.append(c.getShortMessage());
-        }
-        msgbuf.append("\n");
-
-        if (author == null) {
-          author = c.getAuthorIdent();
-        } else if (!author.equals(c.getAuthorIdent())) {
-          sameAuthorForAll = false;
-        }
-      }
-
-      if (!sameAuthorForAll || author == null) {
-        author = myIdent;
-      }
-
       pdb = repoManager.openRepository(subscriber.getParentKey());
       if (pdb.getRef(subscriber.get()) == null) {
         throw new SubmoduleException(
             "The branch was probably deleted from the subscriber repository");
       }
 
-      final ObjectId currentCommitId =
-          pdb.getRef(subscriber.get()).getObjectId();
-
       DirCache dc = readTree(pdb, pdb.getRef(subscriber.get()));
       DirCacheEditor ed = dc.editor();
-      for (final Map.Entry<Branch.NameKey, ObjectId> me : modules.entrySet()) {
-        ed.add(new PathEdit(paths.get(me.getKey())) {
-          @Override
-          public void apply(DirCacheEntry ent) {
-            ent.setFileMode(FileMode.GITLINK);
-            ent.setObjectId(me.getValue().copy());
+
+      for (SubmoduleSubscription s : updates) {
+        try (Repository subrepo = repoManager.openRepository(
+            s.getSubmodule().getParentKey());
+            RevWalk rw = CodeReviewCommit.newRevWalk(subrepo)) {
+          Ref ref = subrepo.getRefDatabase().exactRef(s.getSubmodule().get());
+          if (ref == null) {
+            ed.add(new DeletePath(s.getPath()));
+            continue;
           }
-        });
+
+          final ObjectId updateTo = ref.getObjectId();
+          RevCommit newCommit = rw.parseCommit(updateTo);
+
+          if (author == null) {
+            author = newCommit.getAuthorIdent();
+          } else if (!author.equals(newCommit.getAuthorIdent())) {
+            sameAuthorForAll = false;
+          }
+
+          DirCacheEntry dce = dc.getEntry(s.getPath());
+          ObjectId oldId = null;
+          if (dce != null) {
+            if (!dce.getFileMode().equals(FileMode.GITLINK)) {
+              log.error("Requested to update gitlink " + s.getPath() + " in "
+                  + s.getSubmodule().getParentKey().get() + " but entry "
+                  + "doesn't have gitlink file mode.");
+              continue;
+            }
+            oldId = dce.getObjectId();
+          } else {
+            // This submodule did not exist before. We do not want to add
+            // the full submodule history to the commit message, so omit it.
+            oldId = updateTo;
+          }
+
+          ed.add(new PathEdit(s.getPath()) {
+            @Override
+            public void apply(DirCacheEntry ent) {
+              ent.setFileMode(FileMode.GITLINK);
+              ent.setObjectId(updateTo);
+            }
+          });
+
+          msgbuf.append("Project: " + s.getSubmodule().getParentKey().get());
+          msgbuf.append(" " + s.getSubmodule().getShortName());
+          msgbuf.append(" " + updateTo.getName());
+          msgbuf.append("\n\n");
+
+          try {
+            rw.markStart(newCommit);
+
+            if (oldId != null) {
+              rw.markUninteresting(rw.parseCommit(oldId));
+            }
+            for (RevCommit c : rw) {
+              msgbuf.append(c.getFullMessage() + "\n\n");
+            }
+          } catch (IOException e) {
+            logAndThrowSubmoduleException("Could not perform a revwalk to "
+                + "create superproject commit message", e);
+          }
+        }
       }
       ed.finish();
 
+      if (!sameAuthorForAll || author == null) {
+        author = myIdent;
+      }
+
       ObjectInserter oi = pdb.newObjectInserter();
       ObjectId tree = dc.writeTree(oi);
 
-      final CommitBuilder commit = new CommitBuilder();
+      ObjectId currentCommitId =
+          pdb.getRef(subscriber.get()).getObjectId();
+
+      CommitBuilder commit = new CommitBuilder();
       commit.setTreeId(tree);
       commit.setParentIds(new ObjectId[] {currentCommitId});
       commit.setAuthor(author);
@@ -390,14 +336,12 @@
         default:
           throw new IOException(rfu.getResult().name());
       }
-
       recRw = new RevWalk(pdb);
-
       // Recursive call: update subscribers of the subscriber
-      updateSuperProjects(subscriber, recRw, commitId, msgbuf.toString());
+      updateSuperProjects(db, Sets.newHashSet(subscriber));
     } catch (IOException e) {
-        throw new SubmoduleException("Cannot update gitlinks for "
-            + subscriber.get(), e);
+      throw new SubmoduleException("Cannot update gitlinks for "
+          + subscriber.get(), e);
     } finally {
       if (recRw != null) {
         recRw.close();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/CheckResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/CheckResult.java
new file mode 100644
index 0000000..71321ba
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/CheckResult.java
@@ -0,0 +1,59 @@
+// 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.gpg;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/** Result of checking an object like a key or signature. */
+public class CheckResult {
+  private final List<String> problems;
+
+  CheckResult(String... problems) {
+    this(Arrays.asList(problems));
+  }
+
+  CheckResult(List<String> problems) {
+    this.problems = Collections.unmodifiableList(new ArrayList<>(problems));
+  }
+
+  /**
+   * @return whether the result is entirely ok, i.e. has passed any verification
+   *     or validation checks.
+   */
+  public boolean isOk() {
+    return problems.isEmpty();
+  }
+
+  /** @return any problems encountered during checking. */
+  public List<String> getProblems() {
+    return problems;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder(getClass().getSimpleName())
+        .append('[');
+    for (int i = 0; i < problems.size(); i++) {
+      if (i > 0) {
+        sb.append(", ");
+      }
+      sb.append(problems.get(i));
+    }
+    return sb.append(']').toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PublicKeyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PublicKeyChecker.java
new file mode 100644
index 0000000..5806e8e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PublicKeyChecker.java
@@ -0,0 +1,69 @@
+// 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.gpg;
+
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString;
+
+import org.bouncycastle.openpgp.PGPPublicKey;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Checker for GPG public keys for use in a push certificate. */
+public class PublicKeyChecker {
+  /**
+   * Check a public key.
+   *
+   * @param key the public key.
+   * @param expectedKeyId the key ID that the caller expects.
+   */
+  public final CheckResult check(PGPPublicKey key, long expectedKeyId) {
+    List<String> problems = new ArrayList<>();
+    if (key.getKeyID() != expectedKeyId) {
+      problems.add(
+          "Public key does not match ID " + keyIdToString(expectedKeyId));
+    }
+    if (key.isRevoked()) {
+      // TODO(dborowitz): isRevoked is overeager:
+      // http://www.bouncycastle.org/jira/browse/BJB-45
+      problems.add("Key is revoked");
+    }
+
+    long validSecs = key.getValidSeconds();
+    if (validSecs != 0) {
+      long createdSecs = key.getCreationTime().getTime() / 1000;
+      long nowSecs = System.currentTimeMillis() / 1000;
+      if (nowSecs - createdSecs > validSecs) {
+        problems.add("Key is expired");
+      }
+    }
+    checkCustom(key, expectedKeyId, problems);
+    return new CheckResult(problems);
+  }
+
+  /**
+   * Perform custom checks.
+   * <p>
+   * Default implementation does nothing, but may be overridden by subclasses.
+   *
+   * @param key the public key.
+   * @param expectedKeyId the key ID that the caller expects.
+   * @param problems list to which any problems should be added.
+   */
+  public void checkCustom(PGPPublicKey key, long expectedKeyId,
+      List<String> problems) {
+    // Default implementation does nothing.
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PublicKeyStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PublicKeyStore.java
new file mode 100644
index 0000000..7327c87
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PublicKeyStore.java
@@ -0,0 +1,170 @@
+// 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.gpg;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Store of GPG public keys in git notes.
+ * <p>
+ * Keys are stored in filenames based on their hex key ID, padded out to 40
+ * characters to match the length of a SHA-1. (This is to easily reuse existing
+ * fanout code in {@link NoteMap}, and may be changed later after an appropriate
+ * transition.)
+ * <p>
+ * The contents of each file is an ASCII armored stream containing one or more
+ * public key rings matching the ID. Multiple keys are supported because forging
+ * a key ID is possible, but such a key cannot be used to verify signatures
+ * produced with the correct key.
+ * <p>
+ * No additional checks are performed on the key after reading; callers should
+ * only trust keys after checking with a {@link PublicKeyChecker}.
+ */
+public class PublicKeyStore implements AutoCloseable {
+  private final Repository repo;
+  private ObjectReader reader;
+  private NoteMap notes;
+
+  /** @param repo repository to read keys from. */
+  public PublicKeyStore(Repository repo) {
+    this.repo = repo;
+  }
+
+  @Override
+  public void close() {
+    if (reader != null) {
+      reader.close();
+      reader = null;
+      notes = null;
+    }
+  }
+
+  private void load() throws IOException {
+    close();
+    reader = repo.newObjectReader();
+
+    Ref ref = repo.getRefDatabase().exactRef(RefNames.REFS_GPG_KEYS);
+    if (ref == null) {
+      return;
+    }
+    try (RevWalk rw = new RevWalk(reader)) {
+      notes = NoteMap.read(reader, rw.parseCommit(ref.getObjectId()));
+    }
+  }
+
+  /**
+   * Read public keys with the given key ID.
+   * <p>
+   * Keys should not be trusted unless checked with {@link PublicKeyChecker}.
+   * <p>
+   * Multiple calls to this method use the same state of the key ref; to reread
+   * the ref, call {@link #close()} first.
+   *
+   * @param keyId key ID.
+   * @return any keys found that could be successfully parsed.
+   * @throws PGPException if an error occurred parsing the key data.
+   * @throws IOException if an error occurred reading the repository data.
+   */
+  public PGPPublicKeyRingCollection get(long keyId)
+      throws PGPException, IOException {
+    if (reader == null) {
+      load();
+    }
+    if (notes == null) {
+      return empty();
+    }
+    Note note = notes.getNote(keyObjectId(keyId));
+    if (note == null) {
+      return empty();
+    }
+
+    List<PGPPublicKeyRing> keys = new ArrayList<>();
+    try (InputStream in = reader.open(note.getData(), OBJ_BLOB).openStream()) {
+      while (true) {
+        @SuppressWarnings("unchecked")
+        Iterator<Object> it =
+            new BcPGPObjectFactory(new ArmoredInputStream(in)).iterator();
+        if (!it.hasNext()) {
+          break;
+        }
+        Object obj = it.next();
+        if (obj instanceof PGPPublicKeyRing) {
+          keys.add((PGPPublicKeyRing) obj);
+        }
+        checkState(!it.hasNext(),
+            "expected one PGP object per ArmoredInputStream");
+      }
+      return new PGPPublicKeyRingCollection(keys);
+    }
+  }
+
+  // TODO(dborowitz): put method.
+
+  private static PGPPublicKeyRingCollection empty()
+      throws PGPException, IOException {
+    return new PGPPublicKeyRingCollection(
+        Collections.<PGPPublicKeyRing> emptyList());
+  }
+
+  static String keyToString(PGPPublicKey key) {
+    @SuppressWarnings("unchecked")
+    Iterator<String> it = key.getUserIDs();
+    ByteBuffer buf = ByteBuffer.wrap(key.getFingerprint());
+    return String.format(
+        "%s %s(%04X %04X %04X %04X %04X  %04X %04X %04X %04X %04X)",
+        keyIdToString(key.getKeyID()),
+        it.hasNext() ? it.next() + " " : "",
+        buf.getShort(), buf.getShort(), buf.getShort(), buf.getShort(),
+        buf.getShort(), buf.getShort(), buf.getShort(), buf.getShort(),
+        buf.getShort(), buf.getShort());
+  }
+
+  static String keyIdToString(long keyId) {
+    // Match key ID format from gpg --list-keys.
+    return String.format("%08X", (int) keyId);
+  }
+
+  static ObjectId keyObjectId(long keyId) {
+    ByteBuffer buf = ByteBuffer.wrap(new byte[Constants.OBJECT_ID_LENGTH]);
+    buf.putLong(keyId);
+    return ObjectId.fromRaw(buf.array());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PushCertificateChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PushCertificateChecker.java
new file mode 100644
index 0000000..fcef3a3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PushCertificateChecker.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.gpg;
+
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyToString;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureList;
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Checker for push certificates. */
+public abstract class PushCertificateChecker {
+  private final PublicKeyChecker publicKeyChecker;
+
+  protected PushCertificateChecker(PublicKeyChecker publicKeyChecker) {
+    this.publicKeyChecker = publicKeyChecker;
+  }
+
+  /**
+   * Check a push certificate.
+   *
+   * @return result of the check.
+   * @throws PGPException if an error occurred during GPG checks.
+   * @throws IOException if an error occurred reading from the repository.
+   */
+  public final CheckResult check(PushCertificate cert) throws PGPException, IOException {
+    if (cert.getNonceStatus() != NonceStatus.OK) {
+      return new CheckResult("Invalid nonce");
+    }
+    PGPSignature sig = readSignature(cert);
+    if (sig == null) {
+      return new CheckResult("Invalid signature format");
+    }
+    Repository repo = getRepository();
+    List<String> problems = new ArrayList<>();
+    try (PublicKeyStore store = new PublicKeyStore(repo)) {
+      checkSignature(sig, cert, store.get(sig.getKeyID()), problems);
+      checkCustom(repo, problems);
+      return new CheckResult(problems);
+    } finally {
+      if (shouldClose(repo)) {
+        repo.close();
+      }
+    }
+  }
+
+  /**
+   * Get the repository that this checker should operate on.
+   * <p>
+   * This method is called once per call to {@link #check(PushCertificate)}.
+   *
+   * @return the repository.
+   * @throws IOException if an error occurred reading the repository.
+   */
+  protected abstract Repository getRepository() throws IOException;
+
+  /**
+   * @param repo a repository previously returned by {@link #getRepository()}.
+   * @return whether this repository should be closed before returning from
+   *     {@link #check(PushCertificate)}.
+   */
+  protected abstract boolean shouldClose(Repository repo);
+
+  /**
+   * Perform custom checks.
+   * <p>
+   * Default implementation does nothing, but may be overridden by subclasses.
+   *
+   * @param repo a repository previously returned by {@link #getRepository()}.
+   * @param problems list to which any problems should be added.
+   */
+  protected void checkCustom(Repository repo, List<String> problems) {
+    // Default implementation does nothing.
+  }
+
+  private PGPSignature readSignature(PushCertificate cert) throws IOException {
+    ArmoredInputStream in = new ArmoredInputStream(
+        new ByteArrayInputStream(Constants.encode(cert.getSignature())));
+    PGPObjectFactory factory = new BcPGPObjectFactory(in);
+    Object obj;
+    while ((obj = factory.nextObject()) != null) {
+      if (obj instanceof PGPSignatureList) {
+        PGPSignatureList sigs = (PGPSignatureList) obj;
+        if (!sigs.isEmpty()) {
+          return sigs.get(0);
+        }
+      }
+    }
+    return null;
+  }
+
+  private void checkSignature(PGPSignature sig,
+      PushCertificate cert, PGPPublicKeyRingCollection keys,
+      List<String> problems) {
+    List<String> deferredProblems = new ArrayList<>();
+    boolean anyKeys = false;
+    for (PGPPublicKeyRing kr : keys) {
+      PGPPublicKey k = kr.getPublicKey();
+      anyKeys = true;
+      try {
+        sig.init(new BcPGPContentVerifierBuilderProvider(), k);
+        sig.update(Constants.encode(cert.toText()));
+        if (!sig.verify()) {
+          // TODO(dborowitz): Privacy issues with exposing fingerprint/user ID
+          // of keys having the same ID as the pusher's key?
+          deferredProblems.add(
+              "Signature not valid with public key: " + keyToString(k));
+          continue;
+        }
+        CheckResult result = publicKeyChecker.check(k, sig.getKeyID());
+        if (result.isOk()) {
+          return;
+        }
+        StringBuilder err = new StringBuilder("Invalid public key (")
+            .append(keyToString(k))
+            .append("):");
+        for (int i = 0; i < result.getProblems().size(); i++) {
+          err.append('\n').append("  ").append(result.getProblems().get(i));
+        }
+        problems.add(err.toString());
+        return;
+      } catch (PGPException e) {
+        deferredProblems.add(
+            "Error checking signature with public key (" + keyToString(k)
+            + ": " + e.getMessage());
+      }
+    }
+    if (!anyKeys) {
+      problems.add(
+          "No public keys found for Key ID " + keyIdToString(sig.getKeyID()));
+    } else {
+      problems.addAll(deferredProblems);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SignedPushModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/SignedPushModule.java
similarity index 97%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/SignedPushModule.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/SignedPushModule.java
index 88a918d..6e7cc5f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SignedPushModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/SignedPushModule.java
@@ -12,13 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.git.gpg;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.ReceivePackInitializer;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.BouncyCastleUtil;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/SignedPushPreReceiveHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/SignedPushPreReceiveHook.java
new file mode 100644
index 0000000..4da91d3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/SignedPushPreReceiveHook.java
@@ -0,0 +1,98 @@
+// 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.gpg;
+
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ReceiveCommits;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PreReceiveHook;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceivePack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * Pre-receive hook to check signed pushes.
+ * <p>
+ * If configured, prior to processing any push using {@link ReceiveCommits},
+ * requires that any push certificate present must be valid.
+ */
+@Singleton
+public class SignedPushPreReceiveHook implements PreReceiveHook {
+  private static final Logger log =
+      LoggerFactory.getLogger(SignedPushPreReceiveHook.class);
+
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsers;
+
+  @Inject
+  public SignedPushPreReceiveHook(
+      GitRepositoryManager repoManager,
+      AllUsersName allUsers) {
+    this.repoManager = repoManager;
+    this.allUsers = allUsers;
+  }
+
+  @Override
+  public void onPreReceive(ReceivePack rp,
+      Collection<ReceiveCommand> commands) {
+    try {
+      PushCertificate cert = rp.getPushCertificate();
+      if (cert == null) {
+        return;
+      }
+      PushCertificateChecker checker = new PushCertificateChecker(
+          new PublicKeyChecker()) {
+            @Override
+            protected Repository getRepository() throws IOException {
+              return repoManager.openRepository(allUsers);
+            }
+
+            @Override
+            protected boolean shouldClose(Repository repo) {
+              return true;
+            }
+          };
+      CheckResult result = checker.check(cert);
+      if (!result.isOk()) {
+        for (String problem : result.getProblems()) {
+          rp.sendMessage(problem);
+        }
+        reject(commands, "invalid push cert");
+      }
+    } catch (PGPException | IOException e) {
+      log.error("Error checking push certificate", e);
+      reject(commands, "push cert error");
+    }
+  }
+
+  private static void reject(Collection<ReceiveCommand> commands,
+      String reason) {
+    for (ReceiveCommand cmd : commands) {
+      if (cmd.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
+        cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, reason);
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 2d229a9..0e740f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
@@ -132,14 +131,15 @@
       if (args.rw.isMergedInto(mergeTip.getCurrentTip(), n)) {
         mergeTip.moveTipTo(n, n);
       } else {
-        CodeReviewCommit result = args.mergeUtil.mergeOneCommit(
-            args.serverIdent.get(), args.repo, args.rw, args.inserter,
+        PersonIdent myIdent = args.serverIdent.get();
+        CodeReviewCommit result = args.mergeUtil.mergeOneCommit(myIdent,
+            myIdent, args.repo, args.rw, args.inserter,
             args.canMergeFlag, args.destBranch, mergeTip.getCurrentTip(), n);
         mergeTip.moveTipTo(result, n);
       }
-      PatchSetApproval submitApproval = args.mergeUtil.markCleanMerges(args.rw,
-          args.canMergeFlag, mergeTip.getCurrentTip(), args.alreadyAccepted);
-      setRefLogIdent(submitApproval);
+      args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+          mergeTip.getCurrentTip(), args.alreadyAccepted);
+      setRefLogIdent();
     } else {
       // One or more dependencies were not met. The status was already marked on
       // the commit so we have nothing further to perform at this time.
@@ -153,33 +153,19 @@
 
     args.rw.parseBody(n);
 
-    PatchSetApproval submitAudit = args.mergeUtil.getSubmitter(n);
-
-    IdentifiedUser cherryPickUser;
-    PersonIdent serverNow = args.serverIdent.get();
-    PersonIdent cherryPickCommitterIdent;
-    if (submitAudit != null) {
-      cherryPickUser =
-          args.identifiedUserFactory.create(submitAudit.getAccountId());
-      cherryPickCommitterIdent = cherryPickUser.newCommitterIdent(
-          serverNow.getWhen(), serverNow.getTimeZone());
-    } else {
-      cherryPickUser = args.identifiedUserFactory.create(n.change().getOwner());
-      cherryPickCommitterIdent = serverNow;
-    }
-
     String cherryPickCmtMsg = args.mergeUtil.createCherryPickCommitMessage(n);
 
+    PersonIdent committer = args.caller.newCommitterIdent(
+        TimeUtil.nowTs(), args.serverIdent.get().getTimeZone());
     CodeReviewCommit newCommit =
         (CodeReviewCommit) args.mergeUtil.createCherryPickFromCommit(args.repo,
-            args.inserter, mergeTip, n, cherryPickCommitterIdent,
-            cherryPickCmtMsg, args.rw);
+            args.inserter, mergeTip, n, committer, cherryPickCmtMsg, args.rw);
 
     PatchSet.Id id =
         ChangeUtil.nextPatchSetId(args.repo, n.change().currentPatchSetId());
     PatchSet ps = new PatchSet(id);
     ps.setCreatedOn(TimeUtil.nowTs());
-    ps.setUploader(cherryPickUser.getAccountId());
+    ps.setUploader(args.caller.getAccountId());
     ps.setRevision(new RevId(newCommit.getId().getName()));
 
     RefUpdate ru;
@@ -220,9 +206,9 @@
     newCommit.copyFrom(n);
     newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
     newCommit.setControl(
-        args.changeControlFactory.controlFor(n.change(), cherryPickUser));
+        args.changeControlFactory.controlFor(n.change(), args.caller));
     newCommits.put(newCommit.getPatchsetId().getParentKey(), newCommit);
-    setRefLogIdent(submitAudit);
+    setRefLogIdent();
     return newCommit;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
index 7ff2107..f7d8ab1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
 import com.google.gerrit.server.git.MergeException;
@@ -43,10 +42,9 @@
       n.setStatusCode(CommitMergeStatus.NOT_FAST_FORWARD);
     }
 
-    PatchSetApproval submitApproval =
-        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, newMergeTipCommit,
-            args.alreadyAccepted);
-    setRefLogIdent(submitApproval);
+    args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+        newMergeTipCommit, args.alreadyAccepted);
+    setRefLogIdent();
 
     return mergeTip;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
index 3c13af9..d3a72e3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.MergeTip;
 
+import org.eclipse.jgit.lib.PersonIdent;
+
 import java.util.Collection;
 import java.util.List;
 
@@ -42,17 +43,19 @@
     }
     while (!sorted.isEmpty()) {
       CodeReviewCommit mergedFrom = sorted.remove(0);
+      PersonIdent serverIdent = args.serverIdent.get();
+      PersonIdent caller = args.caller.newCommitterIdent(
+          serverIdent.getWhen(), serverIdent.getTimeZone());
       CodeReviewCommit newTip =
-          args.mergeUtil.mergeOneCommit(args.serverIdent.get(), args.repo, args.rw,
-              args.inserter, args.canMergeFlag, args.destBranch, mergeTip.getCurrentTip(),
-              mergedFrom);
+          args.mergeUtil.mergeOneCommit(caller, serverIdent,
+              args.repo, args.rw, args.inserter, args.canMergeFlag,
+              args.destBranch, mergeTip.getCurrentTip(), mergedFrom);
       mergeTip.moveTipTo(newTip, mergedFrom);
     }
 
-    final PatchSetApproval submitApproval =
-        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, mergeTip.getCurrentTip(),
-            args.alreadyAccepted);
-    setRefLogIdent(submitApproval);
+    args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+        mergeTip.getCurrentTip(), args.alreadyAccepted);
+    setRefLogIdent();
 
     return mergeTip;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
index b49cb0a..688fa3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.MergeTip;
 
+import org.eclipse.jgit.lib.PersonIdent;
+
 import java.util.Collection;
 import java.util.List;
 
@@ -48,18 +49,19 @@
     // For every other commit do a pair-wise merge.
     while (!sorted.isEmpty()) {
       CodeReviewCommit mergedFrom = sorted.remove(0);
+      PersonIdent serverIdent = args.serverIdent.get();
+      PersonIdent caller = args.caller.newCommitterIdent(
+          serverIdent.getWhen(), serverIdent.getTimeZone());
       branchTip =
-          args.mergeUtil.mergeOneCommit(args.serverIdent.get(), args.repo,
-              args.rw, args.inserter, args.canMergeFlag, args.destBranch,
-              branchTip, mergedFrom);
+          args.mergeUtil.mergeOneCommit(caller, serverIdent,
+              args.repo, args.rw, args.inserter, args.canMergeFlag,
+              args.destBranch, branchTip, mergedFrom);
       mergeTip.moveTipTo(branchTip, mergedFrom);
     }
 
-    final PatchSetApproval submitApproval =
-        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, branchTip,
-            args.alreadyAccepted);
-    setRefLogIdent(submitApproval);
-
+    args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, branchTip,
+        args.alreadyAccepted);
+    setRefLogIdent();
     return mergeTip;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
index dd981ad..f9102ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
 import com.google.gerrit.server.change.RebaseChange;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -33,6 +32,7 @@
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 
 import java.io.IOException;
 import java.util.Collection;
@@ -84,15 +84,11 @@
 
         } else {
           try {
-            IdentifiedUser uploader =
-                args.identifiedUserFactory.create(args.mergeUtil
-                    .getSubmitter(n).getAccountId());
             PatchSet newPatchSet =
                 rebaseChange.rebase(args.repo, args.rw, args.inserter,
-                    n.change(), n.getPatchsetId(), uploader,
+                    n.change(), n.getPatchsetId(), args.caller,
                     mergeTip.getCurrentTip(), args.mergeUtil,
                     args.serverIdent.get(), false, ValidatePolicy.NONE);
-
             List<PatchSetApproval> approvals = Lists.newArrayList();
             for (PatchSetApproval a : args.approvalsUtil.byPatchSet(args.db,
                 n.getControl(), n.getPatchsetId())) {
@@ -109,13 +105,13 @@
                     newPatchSet.getId()));
             mergeTip.getCurrentTip().copyFrom(n);
             mergeTip.getCurrentTip().setControl(
-                args.changeControlFactory.controlFor(n.change(), uploader));
+                args.changeControlFactory.controlFor(n.change(), args.caller));
             mergeTip.getCurrentTip().setPatchsetId(newPatchSet.getId());
             mergeTip.getCurrentTip().setStatusCode(
                 CommitMergeStatus.CLEAN_REBASE);
             newCommits.put(newPatchSet.getId().getParentKey(),
                 mergeTip.getCurrentTip());
-            setRefLogIdent(args.mergeUtil.getSubmitter(n));
+            setRefLogIdent();
           } catch (MergeConflictException e) {
             n.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
           } catch (NoSuchChangeException | OrmException | IOException
@@ -135,15 +131,15 @@
           if (args.rw.isMergedInto(mergeTip.getCurrentTip(), n)) {
             mergeTip.moveTipTo(n, n);
           } else {
+            PersonIdent myIdent = args.serverIdent.get();
             mergeTip.moveTipTo(
-                args.mergeUtil.mergeOneCommit(args.serverIdent.get(),
+                args.mergeUtil.mergeOneCommit(myIdent, myIdent,
                     args.repo, args.rw, args.inserter, args.canMergeFlag,
                     args.destBranch, mergeTip.getCurrentTip(), n), n);
           }
-          PatchSetApproval submitApproval =
-              args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
-                  mergeTip.getCurrentTip(), args.alreadyAccepted);
-          setRefLogIdent(submitApproval);
+          args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+              mergeTip.getCurrentTip(), args.alreadyAccepted);
+          setRefLogIdent();
         } catch (IOException e) {
           throw new MergeException("Cannot merge " + n.name(), e);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index b25b17e..4034abd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.git.strategy;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
@@ -66,6 +67,7 @@
     protected final MergeUtil mergeUtil;
     protected final ChangeIndexer indexer;
     protected final MergeSorter mergeSorter;
+    protected final IdentifiedUser caller;
 
     Arguments(IdentifiedUser.GenericFactory identifiedUserFactory,
         Provider<PersonIdent> serverIdent, ReviewDb db,
@@ -73,7 +75,7 @@
         RevWalk rw, ObjectInserter inserter, RevFlag canMergeFlag,
         Set<RevCommit> alreadyAccepted, Branch.NameKey destBranch,
         ApprovalsUtil approvalsUtil, MergeUtil mergeUtil,
-        ChangeIndexer indexer) {
+        ChangeIndexer indexer, IdentifiedUser caller) {
       this.identifiedUserFactory = identifiedUserFactory;
       this.serverIdent = serverIdent;
       this.db = db;
@@ -89,6 +91,7 @@
       this.mergeUtil = mergeUtil;
       this.indexer = indexer;
       this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
+      this.caller = caller;
     }
   }
 
@@ -115,6 +118,7 @@
   public final MergeTip run(final CodeReviewCommit currentTip,
       final Collection<CodeReviewCommit> toMerge) throws MergeException {
     refLogIdent = null;
+    checkState(args.caller != null);
     return _run(currentTip, toMerge);
   }
 
@@ -124,7 +128,7 @@
 
   /**
    * Checks whether the given commit can be merged.
-   *
+   * <p>
    * Implementations must ensure that invoking this method modifies neither the
    * git repository nor the Gerrit database.
    *
@@ -156,7 +160,7 @@
    * <p>
    * By default this method returns an empty map, but subclasses may override
    * this method to provide any newly created commits.
-   *
+   * <p>
    * This method may only be called after {@link #run(CodeReviewCommit,
    * Collection)}.
    *
@@ -182,13 +186,11 @@
 
   /**
    * Set the ref log identity if it wasn't set yet.
-   *
-   * @param submitApproval the approval that submitted the patch set
    */
-  protected final void setRefLogIdent(PatchSetApproval submitApproval) {
-    if (refLogIdent == null && submitApproval != null) {
+  protected final void setRefLogIdent() {
+    if (refLogIdent == null) {
       refLogIdent = args.identifiedUserFactory.create(
-          submitApproval.getAccountId()) .newRefLogIdent();
+          args.caller.getAccountId()).newRefLogIdent();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
index ffe351b..b87499a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -88,14 +88,14 @@
   public SubmitStrategy create(final SubmitType submitType, final ReviewDb db,
       final Repository repo, final RevWalk rw, final ObjectInserter inserter,
       final RevFlag canMergeFlag, final Set<RevCommit> alreadyAccepted,
-      final Branch.NameKey destBranch)
+      final Branch.NameKey destBranch, final IdentifiedUser caller)
       throws MergeException, NoSuchProjectException {
     ProjectState project = getProject(destBranch);
     final SubmitStrategy.Arguments args =
         new SubmitStrategy.Arguments(identifiedUserFactory, myIdent, db,
             changeControlFactory, repo, rw, inserter, canMergeFlag,
             alreadyAccepted, destBranch,approvalsUtil,
-            mergeUtilFactory.create(project), indexer);
+            mergeUtilFactory.create(project), indexer, caller);
     switch (submitType) {
       case CHERRY_PICK:
         return new CherryPick(args, patchSetInfoFactory, gitRefUpdated);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
index 59ca272..32a051b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -101,30 +101,27 @@
   @Override
   public GroupDescription.Basic get(AccountGroup.UUID uuid) {
     final GroupReference ref = getGroup(uuid);
-    if (ref != null) {
-      return new GroupDescription.Basic() {
-        @Override
-        public String getName() {
-          return ref.getName();
-        }
+    return new GroupDescription.Basic() {
+      @Override
+      public String getName() {
+        return ref.getName();
+      }
 
-        @Override
-        public AccountGroup.UUID getGroupUUID() {
-          return ref.getUUID();
-        }
+      @Override
+      public AccountGroup.UUID getGroupUUID() {
+        return ref.getUUID();
+      }
 
-        @Override
-        public String getUrl() {
-          return null;
-        }
+      @Override
+      public String getUrl() {
+        return null;
+      }
 
-        @Override
-        public String getEmailAddress() {
-          return null;
-        }
-      };
-    }
-    return null;
+      @Override
+      public String getEmailAddress() {
+        return null;
+      }
+    };
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 43c232b..cb35619 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -164,7 +164,7 @@
   }
 
   public void setStatus(Change.Status status) {
-    checkArgument(status != Change.Status.SUBMITTED,
+    checkArgument(status != Change.Status.MERGED,
         "use submit(Iterable<PatchSetApproval>)");
     this.status = status;
   }
@@ -177,8 +177,8 @@
     approvals.put(label, Optional.<Short> absent());
   }
 
-  public void submit(Iterable<SubmitRecord> submitRecords) {
-    status = Change.Status.SUBMITTED;
+  public void merge(Iterable<SubmitRecord> submitRecords) {
+    this.status = Change.Status.MERGED;
     this.submitRecords = ImmutableList.copyOf(submitRecords);
     checkArgument(!this.submitRecords.isEmpty(),
         "no submit records specified at submit time");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
index 8da8cc1..ea81f17 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
@@ -58,7 +58,7 @@
   }
 
   @Override
-  void stop(PluginGuiceEnvironment env) {
+  protected void stop(PluginGuiceEnvironment env) {
     if (manager != null) {
       manager.stop();
       httpInjector = null;
@@ -83,7 +83,7 @@
   }
 
   @Override
-  boolean canReload() {
+  protected boolean canReload() {
     return true;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index 6b84c21..c2b28cb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -130,9 +130,9 @@
     return disabled;
   }
 
-  abstract void start(PluginGuiceEnvironment env) throws Exception;
+  protected abstract void start(PluginGuiceEnvironment env) throws Exception;
 
-  abstract void stop(PluginGuiceEnvironment env);
+  protected abstract void stop(PluginGuiceEnvironment env);
 
   public abstract PluginContentScanner getContentScanner();
 
@@ -168,7 +168,7 @@
     return "Plugin [" + name + "]";
   }
 
-  abstract boolean canReload();
+  protected abstract boolean canReload();
 
   boolean isModified(Path jar) {
     return snapshot.lastModified() != lastModified(jar);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 2887a00..6b458aa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -145,7 +145,7 @@
         || (httpMaps != null && httpMaps.containsKey(type));
   }
 
-  Module getSysModule() {
+  public Module getSysModule() {
     return sysModule;
   }
 
@@ -210,15 +210,15 @@
     return httpGen.get();
   }
 
-  RequestContext enter(Plugin plugin) {
+  public RequestContext enter(Plugin plugin) {
     return local.setContext(new PluginRequestContext(plugin.getPluginUser()));
   }
 
-  void exit(RequestContext old) {
+  public void exit(RequestContext old) {
     local.setContext(old);
   }
 
-  void onStartPlugin(Plugin plugin) {
+  public void onStartPlugin(Plugin plugin) {
     RequestContext oldContext = enter(plugin);
     try {
       attachItem(sysItems, plugin.getSysInjector(), plugin);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
index 28d57b2..14c1185 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -151,7 +151,7 @@
   }
 
   @Override
-  boolean canReload() {
+  protected boolean canReload() {
     Attributes main = manifest.getMainAttributes();
     String v = main.getValue("Gerrit-ReloadMode");
     if (Strings.isNullOrEmpty(v) || "reload".equalsIgnoreCase(v)) {
@@ -167,7 +167,7 @@
   }
 
   @Override
-  void start(PluginGuiceEnvironment env) throws Exception {
+  protected void start(PluginGuiceEnvironment env) throws Exception {
     RequestContext oldContext = env.enter(this);
     try {
       startPlugin(env);
@@ -241,7 +241,7 @@
   }
 
   @Override
-  void stop(PluginGuiceEnvironment env) {
+  protected void stop(PluginGuiceEnvironment env) {
     if (serverManager != null) {
       RequestContext oldContext = env.enter(this);
       try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
index 28700b3..1801712 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
@@ -30,8 +30,8 @@
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.extensions.webui.UiActions;
-import com.google.gerrit.server.git.SignedPushModule;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.gpg.SignedPushModule;
 import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.Config;
@@ -169,14 +169,19 @@
             cfgFactory.getFromProjectConfigWithInheritance(project,
                 e.getPluginName());
         p.inheritable = true;
-        p.value = cfgWithInheritance.getString(e.getExportName(), configEntry.getDefaultValue());
+        p.value = configEntry.onRead(project,
+            cfgWithInheritance.getString(e.getExportName(),
+                configEntry.getDefaultValue()));
         p.configuredValue = configuredValue;
         p.inheritedValue = getInheritedValue(project, cfgFactory, e);
       } else {
         if (configEntry.getType() == ProjectConfigEntry.Type.ARRAY) {
-          p.values = Arrays.asList(cfg.getStringList(e.getExportName()));
+          p.values = configEntry.onRead(project,
+              Arrays.asList(cfg.getStringList(e.getExportName())));
         } else {
-          p.value = configuredValue != null ? configuredValue : configEntry.getDefaultValue();
+          p.value = configEntry.onRead(project, configuredValue != null
+              ? configuredValue
+              : configEntry.getDefaultValue());
         }
       }
       Map<String, ConfigParameterInfo> pc = pluginConfig.get(e.getPluginName());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index da7df8c..d73a95d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -79,7 +79,7 @@
   }
 
   private final Config gerritConfig;
-  private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final ProjectCache projectCache;
   private final GitRepositoryManager gitMgr;
   private final ProjectState.Factory projectStateFactory;
@@ -93,7 +93,7 @@
 
   @Inject
   PutConfig(@GerritServerConfig Config gerritConfig,
-      MetaDataUpdate.User metaDataUpdateFactory,
+      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       ProjectCache projectCache,
       GitRepositoryManager gitMgr,
       ProjectState.Factory projectStateFactory,
@@ -138,7 +138,7 @@
 
     final MetaDataUpdate md;
     try {
-      md = metaDataUpdateFactory.create(projectName);
+      md = metaDataUpdateFactory.get().create(projectName);
     } catch (RepositoryNotFoundException notFound) {
       throw new ResourceNotFoundException(projectName.get());
     } catch (IOException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 2070746..e63b05c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -117,7 +117,7 @@
                 args.submitStrategyFactory.create(submitType,
                     db.get(), repo, rw, null, canMergeFlag,
                     getAlreadyAccepted(repo, rw, commit),
-                    otherChange.getDest());
+                    otherChange.getDest(), null);
             CodeReviewCommit otherCommit =
                 (CodeReviewCommit) rw.parseCommit(other);
             otherCommit.add(canMergeFlag);
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 a4d4467..77a24cc 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
@@ -108,17 +108,6 @@
     return query(project(project));
   }
 
-  public List<ChangeData> submitted(Branch.NameKey branch) throws OrmException {
-    return query(and(
-        ref(branch),
-        project(branch.getParentKey()),
-        status(Change.Status.SUBMITTED)));
-  }
-
-  public List<ChangeData> allSubmitted() throws OrmException {
-    return query(status(Change.Status.SUBMITTED));
-  }
-
   public List<ChangeData> byBranchOpen(Branch.NameKey branch)
       throws OrmException {
     return query(and(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index c1be4f8..038da50 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_108> C = Schema_108.class;
+  public static final Class<Schema_109> C = Schema_109.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_109.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_109.java
new file mode 100644
index 0000000..fa8af3e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_109.java
@@ -0,0 +1,39 @@
+// 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.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_109 extends SchemaVersion {
+
+  @Inject
+  Schema_109(Provider<Schema_108> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    String cmd = "UPDATE changes SET status = 'n' WHERE status = 's';";
+    ui.message("Running " + cmd);
+    try (StatementExecutor e = newExecutor(db)) {
+      e.execute(cmd);
+    }
+    ui.message("done");
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SignedPushPreReceiveHookTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SignedPushPreReceiveHookTest.java
deleted file mode 100644
index f0cfe18..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/SignedPushPreReceiveHookTest.java
+++ /dev/null
@@ -1,90 +0,0 @@
-// 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 com.google.gerrit.server.git.SignedPushPreReceiveHook.keyIdToString;
-
-import org.bouncycastle.bcpg.ArmoredInputStream;
-import org.bouncycastle.openpgp.PGPPublicKey;
-import org.bouncycastle.openpgp.PGPPublicKeyRing;
-import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
-import org.eclipse.jgit.lib.Constants;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.ByteArrayInputStream;
-
-public class SignedPushPreReceiveHookTest {
-  // ./pubring.gpg
-  // -------------
-  // pub   1024R/30A5A053 2015-06-16 [expires: 2015-06-17]
-  //       Key fingerprint = 96D6 DE78 E6D8 DA49 9387  1F31 FA09 A0C4 30A5 A053
-  // uid                  A U. Thor <a_u_thor@example.com>
-  // sub   1024R/D6831DC8 2015-06-16 [expires: 2015-06-17]
-  private static final String PUBKEY =
-      "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-      + "Version: GnuPG v1\n"
-      + "\n"
-      + "mI0EVYCBUQEEALCKzuY6M68RRRm6PS1F322lpHSHTdW9PIURm5B//tbfS32EN6lM\n"
-      + "ISwJxhanpZanv2o4mbV3V8oLT3jMVDPJ3dqmOZJdJs37l+dxCVJ3ycFe1LHtT2oT\n"
-      + "eRyC5PxD7UY5PdDe97mjp7yrp/bx1hE6XqGV0nDGrkJXc8A35u3WzIF5ABEBAAG0\n"
-      + "IEEgVS4gVGhvciA8YV91X3Rob3JAZXhhbXBsZS5jb20+iL4EEwECACgFAlWAgVEC\n"
-      + "GwMFCQABUYAGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEPoJoMQwpaBTjhoD\n"
-      + "/0MRCX1zBjEKIfzFYeSEg/OcSLbAkUD7un5YTfpgds3oUNIKlIgovWO24TQxrCCu\n"
-      + "5pSzN/WfRSzPFhj9HahY/5yh+EGd6HmIU2v/k5I3LwTPEOcZUi1SzOScSv6JOO9Q\n"
-      + "3srVilCu3h6TNW1UGBNjfOr1NdmkWfsUZcjsEc/XrfBGuI0EVYCBUQEEAL0UP9jJ\n"
-      + "eLj3klCCa2tmwdgyFiSf9T+Yoed4I3v3ag2F0/CWrCJr3e1ogSs4Bdts0WptI+Nu\n"
-      + "QIq40AYszewq55dTcB4lbNAYE4svVYQ5AGz78iKzljaBFhyT6ePdZ5wfb+8Jqu1l\n"
-      + "7wRwzRI5Jn3OXCmdGm/dmoUNG136EA9A4ZLLABEBAAGIpQQYAQIADwUCVYCBUQIb\n"
-      + "DAUJAAFRgAAKCRD6CaDEMKWgU5JTA/9XjwPFZ5NseNROMhYZMmje1/ixISb2jaVc\n"
-      + "9m9RLCl8Y3RCY9NNdU5FinTIX9LsRTrJlW6FSG5sin8mwx9jq0eGE1TBEKND5klT\n"
-      + "TmsG0jx1dZG9kWDy6lPnIWw2/4W+N0fK/Cw6WEL1Xg7RLi4NQ9Bi2WoxJii9bWMv\n"
-      + "yy35U6UfPQ==\n"
-      + "=0GL9\n"
-      + "-----END PGP PUBLIC KEY BLOCK-----\n";
-
-  private PGPPublicKey key;
-
-  @Before
-  public void setUp() throws Exception {
-    ArmoredInputStream in = new ArmoredInputStream(
-        new ByteArrayInputStream(Constants.encode(PUBKEY)));
-    PGPPublicKeyRing keyRing =
-        new PGPPublicKeyRing(in, new BcKeyFingerprintCalculator());
-    key = keyRing.getPublicKey();
-  }
-
-  @Test
-  public void testKeyIdToString() throws Exception {
-    assertThat(keyIdToString(key.getKeyID()))
-        .isEqualTo("30A5A053");
-  }
-
-  @Test
-  public void testKeyToString() throws Exception {
-    assertThat(SignedPushPreReceiveHook.toString(key))
-        .isEqualTo("30A5A053 A U. Thor <a_u_thor@example.com>"
-          + " (96D6 DE78 E6D8 DA49 9387  1F31 FA09 A0C4 30A5 A053)");
-  }
-
-  @Test
-  public void testKeyObjectId() throws Exception {
-    String objId = SignedPushPreReceiveHook.keyObjectId(key.getKeyID()).name();
-    assertThat(objId).isEqualTo("fa09a0c430a5a053000000000000000000000000");
-    assertThat(objId.substring(8, 16))
-        .isEqualTo(keyIdToString(key.getKeyID()).toLowerCase());
-  }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PublicKeyCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PublicKeyCheckerTest.java
new file mode 100644
index 0000000..a82619c
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PublicKeyCheckerTest.java
@@ -0,0 +1,66 @@
+// 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.gpg;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class PublicKeyCheckerTest {
+  private PublicKeyChecker checker;
+
+  @Before
+  public void setUp() {
+    checker = new PublicKeyChecker();
+  }
+
+  @Test
+  public void validKey() throws Exception {
+    assertProblems(TestKey.key1());
+  }
+
+  @Test
+  public void wrongKeyId() throws Exception {
+    TestKey k = TestKey.key1();
+    long badId = k.getKeyId() + 1;
+    CheckResult result = checker.check(k.getPublicKey(), badId);
+    assertEquals(
+        Arrays.asList("Public key does not match ID 46328A8D"),
+        result.getProblems());
+  }
+
+  @Test
+  public void keyExpiringInFuture() throws Exception {
+    assertProblems(TestKey.key2());
+  }
+
+  @Test
+  public void expiredKey() throws Exception {
+    assertProblems(TestKey.key3(), "Key is expired");
+  }
+
+  @Test
+  public void selfRevokedKey() throws Exception {
+    assertProblems(TestKey.key4(), "Key is revoked");
+  }
+
+  private void assertProblems(TestKey tk, String... expected) throws Exception {
+    CheckResult result = checker.check(tk.getPublicKey(), tk.getKeyId());
+    assertEquals(Arrays.asList(expected), result.getProblems());
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PublicKeyStoreTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PublicKeyStoreTest.java
new file mode 100644
index 0000000..f42e8b3
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PublicKeyStoreTest.java
@@ -0,0 +1,116 @@
+// 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.gpg;
+
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyObjectId;
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyToString;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+public class PublicKeyStoreTest {
+  private TestRepository<?> tr;
+  private PublicKeyStore store;
+
+  @Before
+  public void setUp() throws Exception {
+    tr = new TestRepository<>(new InMemoryRepository(
+        new DfsRepositoryDescription("pubkeys")));
+    store = new PublicKeyStore(tr.getRepository());
+  }
+
+  @Test
+  public void testKeyIdToString() throws Exception {
+    PGPPublicKey key = TestKey.key1().getPublicKey();
+    assertEquals("46328A8C", keyIdToString(key.getKeyID()));
+  }
+
+  @Test
+  public void testKeyToString() throws Exception {
+    PGPPublicKey key = TestKey.key1().getPublicKey();
+    assertEquals("46328A8C Testuser One <test1@example.com>"
+          + " (04AE A7ED 2F82 1133 E5B1  28D1 ED06 25DC 4632 8A8C)",
+        keyToString(key));
+  }
+
+  @Test
+  public void testKeyObjectId() throws Exception {
+    PGPPublicKey key = TestKey.key1().getPublicKey();
+    String objId = keyObjectId(key.getKeyID()).name();
+    assertEquals("ed0625dc46328a8c000000000000000000000000", objId);
+    assertEquals(keyIdToString(key.getKeyID()).toLowerCase(),
+        objId.substring(8, 16));
+  }
+
+  @Test
+  public void testGet() throws Exception {
+    TestKey key1 = TestKey.key1();
+    tr.branch(RefNames.REFS_GPG_KEYS)
+        .commit()
+        .add(keyObjectId(key1.getKeyId()).name(),
+          key1.getPublicKeyArmored())
+        .create();
+    TestKey key2 = TestKey.key2();
+    tr.branch(RefNames.REFS_GPG_KEYS)
+        .commit()
+        .add(keyObjectId(key2.getKeyId()).name(),
+          key2.getPublicKeyArmored())
+        .create();
+
+    assertKeys(key1.getKeyId(), key1);
+    assertKeys(key2.getKeyId(), key2);
+  }
+
+  @Test
+  public void testGetMultiple() throws Exception {
+    TestKey key1 = TestKey.key1();
+    TestKey key2 = TestKey.key2();
+    tr.branch(RefNames.REFS_GPG_KEYS)
+        .commit()
+        .add(keyObjectId(key1.getKeyId()).name(),
+            key1.getPublicKeyArmored()
+              // Mismatched for this key ID, but we can still read it out.
+              + key2.getPublicKeyArmored())
+        .create();
+    assertKeys(key1.getKeyId(), key1, key2);
+  }
+
+  private void assertKeys(long keyId, TestKey... expected)
+      throws Exception {
+    Set<String> expectedStrings = new TreeSet<>();
+    for (TestKey k : expected) {
+      expectedStrings.add(keyToString(k.getPublicKey()));
+    }
+    PGPPublicKeyRingCollection actual = store.get(keyId);
+    Set<String> actualStrings = new TreeSet<>();
+    for (PGPPublicKeyRing k : actual) {
+      actualStrings.add(keyToString(k.getPublicKey()));
+    }
+    assertEquals(expectedStrings, actualStrings);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PushCertificateCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PushCertificateCheckerTest.java
new file mode 100644
index 0000000..aa2a2c7
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PushCertificateCheckerTest.java
@@ -0,0 +1,153 @@
+// 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.gpg;
+
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyToString;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.bcpg.BCPGOutputStream;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureGenerator;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.PushCertificateIdent;
+import org.eclipse.jgit.transport.PushCertificateParser;
+import org.eclipse.jgit.transport.SignedPushConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.Arrays;
+
+public class PushCertificateCheckerTest {
+  private TestRepository<?> tr;
+  private SignedPushConfig signedPushConfig;
+  private PushCertificateChecker checker;
+
+  @Before
+  public void setUp() throws Exception {
+    TestKey key1 = TestKey.key1();
+    TestKey key3 = TestKey.key3();
+    tr = new TestRepository<>(new InMemoryRepository(
+        new DfsRepositoryDescription("repo")));
+    tr.branch(RefNames.REFS_GPG_KEYS).commit()
+        .add(PublicKeyStore.keyObjectId(key1.getPublicKey().getKeyID()).name(),
+            key1.getPublicKeyArmored())
+        .add(PublicKeyStore.keyObjectId(key3.getPublicKey().getKeyID()).name(),
+            key3.getPublicKeyArmored())
+        .create();
+    signedPushConfig = new SignedPushConfig();
+    signedPushConfig.setCertNonceSeed("sekret");
+    signedPushConfig.setCertNonceSlopLimit(60 * 24);
+
+    checker = new PushCertificateChecker(new PublicKeyChecker()) {
+      @Override
+      protected Repository getRepository() {
+        return tr.getRepository();
+      }
+
+      @Override
+      protected boolean shouldClose(Repository repo) {
+        return false;
+      }
+    };
+  }
+
+  @Test
+  public void validCert() throws Exception {
+    PushCertificate cert = newSignedCert(validNonce(), TestKey.key1());
+    assertProblems(cert);
+  }
+
+  @Test
+  public void invalidNonce() throws Exception {
+    PushCertificate cert = newSignedCert("invalid-nonce", TestKey.key1());
+    assertProblems(cert, "Invalid nonce");
+  }
+
+  @Test
+  public void missingKey() throws Exception {
+    TestKey key2 = TestKey.key2();
+    PushCertificate cert = newSignedCert(validNonce(), key2);
+    assertProblems(cert,
+        "No public keys found for Key ID " + keyIdToString(key2.getKeyId()));
+  }
+
+  @Test
+  public void invalidKey() throws Exception {
+    TestKey key3 = TestKey.key3();
+    PushCertificate cert = newSignedCert(validNonce(), key3);
+    assertProblems(cert,
+        "Invalid public key (" + keyToString(key3.getPublicKey())
+          + "):\n  Key is expired");
+  }
+
+  private String validNonce() {
+    return signedPushConfig.getNonceGenerator()
+        .createNonce(tr.getRepository(), System.currentTimeMillis() / 1000);
+  }
+
+  private PushCertificate newSignedCert(String nonce, TestKey signingKey)
+      throws Exception {
+    PushCertificateIdent ident = new PushCertificateIdent(
+        signingKey.getFirstUserId(), System.currentTimeMillis(), -7 * 60);
+    String payload = "certificate version 0.1\n"
+      + "pusher " + ident.getRaw() + "\n"
+      + "pushee test://localhost/repo.git\n"
+      + "nonce " + nonce + "\n"
+      + "\n"
+      + "0000000000000000000000000000000000000000"
+      + " deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
+      + " refs/heads/master\n";
+    PGPSignatureGenerator gen = new PGPSignatureGenerator(
+        new BcPGPContentSignerBuilder(
+          signingKey.getPublicKey().getAlgorithm(), PGPUtil.SHA1));
+    gen.init(PGPSignature.BINARY_DOCUMENT, signingKey.getPrivateKey());
+    gen.update(payload.getBytes(UTF_8));
+    PGPSignature sig = gen.generate();
+
+    ByteArrayOutputStream bout = new ByteArrayOutputStream();
+    try (BCPGOutputStream out = new BCPGOutputStream(
+        new ArmoredOutputStream(bout))) {
+      sig.encode(out);
+    }
+
+    String cert = payload + new String(bout.toByteArray(), UTF_8);
+    Reader reader =
+        new InputStreamReader(new ByteArrayInputStream(cert.getBytes(UTF_8)));
+    PushCertificateParser parser =
+        new PushCertificateParser(tr.getRepository(), signedPushConfig);
+    return parser.parse(reader);
+  }
+
+  private void assertProblems(PushCertificate cert, String... expected)
+      throws Exception {
+    CheckResult result = checker.check(cert);
+    assertEquals(Arrays.asList(expected), result.getProblems());
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/TestKey.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/TestKey.java
new file mode 100644
index 0000000..69362ab
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/TestKey.java
@@ -0,0 +1,496 @@
+// 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.gpg;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPrivateKey;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.PGPSecretKeyRing;
+import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
+import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder;
+import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
+import org.eclipse.jgit.lib.Constants;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+class TestKey {
+  /**
+   * A valid key with no expiration.
+   *
+   * <pre>
+   * pub   2048R/46328A8C 2015-07-08
+   *       Key fingerprint = 04AE A7ED 2F82 1133 E5B1  28D1 ED06 25DC 4632 8A8C
+   * uid                  Testuser One &lt;test1@example.com&gt;
+   * sub   2048R/F0AF69C0 2015-07-08
+   * </pre>
+   */
+  static TestKey key1() throws PGPException, IOException {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBFWdTIkBCADOaygDKjLuRX6LXAvBAYB91cmTf1MSlmEy+qsG3c9ijjQixPkr\n"
+        + "atdYkocrrT2S0R9UGjksTOI2WN5S0lQfLA1RSk63KURQE+OF+IfNqdD6nQdLBs1w\n"
+        + "va+GDj/uvuI05I0oXf/M7POdFphutrS4EUDBnFPj6ns/0C2sTRTxliD+Y9Y9a84V\n"
+        + "DfVVUbJB6wc3LP3L6ImT+cSM7dLq3hZHya+9FNeYPmPYnBrkJyqf2NDd38Sddsro\n"
+        + "7smw/GgCZHnnuVNS4C7NsHr6900VKC+JDtdx+fqptixcAEJWiGoQfWqU+hYmia3p\n"
+        + "9+Xw02+3FcjOT6ONUCmHX+xlz0pXW4iIYlPpABEBAAG0IFRlc3R1c2VyIE9uZSA8\n"
+        + "dGVzdDFAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJVnUyJAhsDBgsJCAcDAgYVCAIJ\n"
+        + "CgsEFgIDAQIeAQIXgAAKCRDtBiXcRjKKjHblB/9RaFO5+GTDIphAL/aVj2u+d8Lq\n"
+        + "yUpBrDp3P06QDGpKGFMAovBuh+NLH76VKNIzQLQC8rdTj651fLcLMuJ1enQ3Rblg\n"
+        + "RKr1oc+wqqtFHr4QyOQjE/N3C9GQjEzfqn4qnp5KtZxYFnlvU5NGehid7M1HTZMx\n"
+        + "jRcHbM9KQnsE5Z4fh4wmN5ynG+5nbaF4O9otPOpFzYRvIhxFmHscWyOgRaMZiYEX\n"
+        + "7Qkzze+scAlc9E/EWRJQIFcxnxV/SYIT4qCTT1g2aKA8OCBO/ZTOleH8SzvTODjy\n"
+        + "W0lGHnh/ZqH6XGVcGUaJZZ2uHTck1+czuVVShNcXPW1W20T6E9UqzHbJHN0guQEN\n"
+        + "BFWdTIkBCACoLVdPr3gpQwzI+2NGXjdtoyqYoPlgfeyI2M1XQD/7+rLZTbi14ZjN\n"
+        + "vYkS/+/oGtVEmiYOiAVTwmkjCYkKGDgNcCiJVekiPAN6JryVv488wRc999b5LpFE\n"
+        + "fhLGwI0YxjcS4KFFnpMC3wSb6tJUnHRLVoE5d8icdiaOpgYdp7uqWkSx2oxqHgIb\n"
+        + "nuyrk3ydEcS4ZeGD+w+taIxMc9F1DS9kiXALD7xWgUkmqZLEQoNgF6KlwCHXRd3m\n"
+        + "rBCo97sE95yKcq98ZMIWuQtTcEccZsN/6jlsei+9RI0tqs+FbZnIFm/go9zk11Vl\n"
+        + "IQ9QFSj6ruqoKrYvNZuDDLD1lHvZPD4/ABEBAAGJAR8EGAECAAkFAlWdTIkCGwwA\n"
+        + "CgkQ7QYl3EYyiox+HAf/Z/OCQO3jxALAcn3oUb1g/IlHm6qZv7RJOFUsj/16fGiF\n"
+        + "rRTP15zMXzyqV+L/LGV/owvOsdD/o7boZz4C/U98COx0Nl1jOrmPATOl+xqsgpEj\n"
+        + "Fhk+eAR7exO2XxW+u2g4cYoSMosIOX5w1GrdsxQeaZDwiSJMEOR2cVLs3YI19Ci/\n"
+        + "FuzActZ0wJNk0nlNF6l8CAbzwN6pM9OIc/iBIwDjz92KUco0NF8XKZnxqhH4wfHB\n"
+        + "PGkTx8RwOvELUTDMtvYnG5R0QtND0RbOnmp4ZVZmeOjKSLo1mZliUZB1H2PPSxrA\n"
+        + "0oLr8+wLntz1SU7uS4ddvhSQW+j2M/0pa352KUwmrw==\n"
+        + "=o/aU\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=MuAn\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A valid key expiring in 2065.
+   *
+   * <pre>
+   * pub   2048R/378A0AED 2015-07-08 [expires: 2065-06-25]
+   *       Key fingerprint = C378 369A CBCD 34CC 138D  90B1 4531 1A6F 378A 0AED
+   * uid                  Testuser Two &lt;test2@example.com&gt;
+   * sub   2048R/46D4F204 2015-07-08 [expires: 2065-06-25]
+   * </pre>
+   */
+  static final TestKey key2() throws PGPException, IOException {
+    return new TestKey(
+        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBFWdTP8BCADRxNpasIv0jtNXTK6VYIS2VJ2Xk0ZD6gtxeoXCpjQ+TsB9fxh3\n"
+        + "vAMPt2Zu5LqoGwygKOJj1zquG8xk7GUCCHJk3+qG8xxB1xGtSz2vLyfRm7fOZmHj\n"
+        + "3W/C/25lynSPDrfvcwvwA4PN8iP5EWbWU10L6WOZGMwwwtVDUSEouSOw2LEepxLV\n"
+        + "rkKuZcyHaivheDbUlZliwe9rGXd4hh1h4qyNQWG3q+ytlL28sVkOzUh6IMBTvqhe\n"
+        + "IRsvxvaVSLV8jRVKfUTqw0g57ft4ZD2/L46yUTXzr9aUCBjTNxvWLlyboqql/D8P\n"
+        + "inp51h3cvAg7NW5RdG1GEYmylH8SygT5utPxABEBAAG0IFRlc3R1c2VyIFR3byA8\n"
+        + "dGVzdDJAZXhhbXBsZS5jb20+iQE+BBMBAgAoBQJVnUz/AhsDBQld/A8ABgsJCAcD\n"
+        + "AgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBFMRpvN4oK7UZqCACWwQL/YvBK4b0m+R0d\n"
+        + "UdvAXeBx7DwOAnAodis9ZVqChb7RxcZQxF1Ti9mtCBPPQGuEs5wE2Ocrrq+L13r6\n"
+        + "bgW+1WOB1tZSDVxwL1PnZFw/SyADRIDCZrOHiAkp82UnZwWAkk39GzNJtt1wTYDZ\n"
+        + "FMTFUr2SPscXk1k7muS+ZfEFwNPD4tODo/poJKDYEJ80Z5UXXFQLDtsfdeIXMFIT\n"
+        + "449CYoq8XBMBfvyWl/LLpw0r3JI6pV/YdH3Oeuz8XkkEVzRxaxB6Zmeo5jSwjR/T\n"
+        + "8TKDGwwiuwiiT3SfkFSVdcjKulRuXSRNs1Ouf7/UC3cq4bG2WXWa85X1+HQRm7iu\n"
+        + "RHSOuQENBFWdTP8BCADhhGxAA0pX5yBHwIgM1j0gw2h5nSsopDrO6t/sbRUcNxnR\n"
+        + "tBScgKZnP0sjRTYEUIwmZuseHMBohtVCuMaDt06qyZDvDk/98j3AeE5t2dgFnOIe\n"
+        + "qCrm/6aejbFcQOpxe6U29KJRCAxuwNtB15X1VH1Kj7B0gRSTu13n/5sUsi2lunoZ\n"
+        + "oIvpIe9tZH4aXitCY2MCQH+hTyCyNBzlEa44kWz6LxUsPdo7I6rXkTr6Ot7wQh+9\n"
+        + "7HCe042GIq65h0apgujyjhJidjch5ur1mngaSNSEyvbji2MGC+cd3wAIstG5a7xP\n"
+        + "d9MncY5Q/eH+hn96694k5bckottSyGm/3f2Ihfj1ABEBAAGJASUEGAECAA8FAlWd\n"
+        + "TP8CGwwFCV38DwAACgkQRTEabzeKCu1FNwgAif4eK2v7R3QubL2S6wmb1nsgRMgV\n"
+        + "YoxGBeUk2EK6WZ5IPor93ySd0ixRVNMRmJ8BLH3EMjZQTzkDG+BH6zFyxo6lLHw9\n"
+        + "NxQjI06tqQWgyyK0mEweVwB/zqtxiB4lNUpsNbqOZWnBJ3d6o1SsnD2Q3uwvP5fb\n"
+        + "fSIgdmUk3c0VMdgA+KzWjPD/PJIPujE+ckHhjn5cbDNw35/FuyhkLJfqlOG7SPvM\n"
+        + "NmCdJ1Pcqju9t7sf6b0BGPDOCL4gpuWKK7HJz9WxngNb3FSziLbyPLk13ynADO+v\n"
+        + "EOR44LPyXE9kVxPusazsXlt9ayTOhELhwzw7sGFFu8E17Cpn7GnVj3tN9A==\n"
+        + "=1e/A\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "9A==\n"
+        + "=qbV3\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A key that expired in 2006.
+   *
+   * <pre>
+   * pub   2048R/17DE1ACD 2005-07-08 [expired: 2006-07-08]
+   *       Key fingerprint = 1D9E EB79 DD38 B049 939D  9CAF 3CEC 781B 17DE 1ACD
+   * uid                  Testuser Three &lt;test3@example.com&gt;
+   * </pre>
+   */
+  static final TestKey key3() throws PGPException, IOException {
+    return new TestKey(
+        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBELOp0UBCACxholOPWuKhK+TYb88nvLUSCMvTLIFEpb5u3Eavr0wiluEzq6H\n"
+        + "55nswAD3dQm8DWxA7yUlEYjPr5btpw7V9441bb1+qtgZMJ10RTdEb/WjyctdGA99\n"
+        + "uOKBEarWbt8W+w6lyJ9NXy5bS/x5EwHHfoTFp4ff6ffHI5hbx1a00K8oxmitgd0X\n"
+        + "Mx86UmauFNJYupZOZG9gEcP4RbRp7e2pm4Jy1WLEOeg9Fdgm5e5Hj2nMkCSZ9BKV\n"
+        + "cxuOllSVzM/Zp0/4+RS9R57jKo3/V74Whwh9yQNgL9UxdNk7L0eGqvaT3EjXxjOc\n"
+        + "RCeJiucGN/0W2iq+V01/QGspp4SKtAogWBozABEBAAG0IlRlc3R1c2VyIFRocmVl\n"
+        + "IDx0ZXN0M0BleGFtcGxlLmNvbT6JAT4EEwECACgFAkLOp0UCGwMFCQHhM4AGCwkI\n"
+        + "BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEDzseBsX3hrNYg0H/2CMm5/JDQNSuRFC\n"
+        + "ECWLrcOeimuvwbmkonNzOkvKbGXl73GStISAksRWAHBQED1rEPC0NkFCDeVZO7df\n"
+        + "SYLlsqKwV6uSh05Ra0F5XeniC12YpAyzoQyCGRS2wLaS822j0zUPXA8XLaO2blCu\n"
+        + "R+8sNu/oecMRcFK4S9NaApi3vdqBNhLiN1/Lpqn1LfB8uIO+eaUf4PmCWbaPgzSk\n"
+        + "qcPfKZmocNXdgLV5Q80n3hc2y2nrl+vDW2M+eVZuDHAok2BOD9uGKFfLAbaXLbX5\n"
+        + "btBW2L0UHtoEyiqkRfD6lX2laSLQmA6+eup7e4GS+s0vXBuVh8XEYddV6Yjt8H7/\n"
+        + "2thO41K5AQ0EQs6nRQEIAM/833UHK1DuFlOm7/n18dRMvs7BkXvg+hPquKWMG3be\n"
+        + "eE4sh1NG5DbRCdo6iacZLarWr3FDz7J9+wswRhtHCh3pGHEuaJk52vRjQxlkNh5F\n"
+        + "p5u2R4WF546bWqX45xPdLfHVTPyWB9q7aVxE+6Q+MHa6lMoyTVnTVCOy3nshiihw\n"
+        + "dxLsxaga+QmaL0bAR+dRcO6ucj7TDQXz1AJAVp26c0LXV9iErhFuuybUZKT0a9Aj\n"
+        + "FoumMZ6l+k30sSdjSjpBMsNvPos0dTPPRXUMu77o5sj+pHa4o8WctgB3o7BHQELp\n"
+        + "KgujZ2sKC9Nm395u6Q4cqUWihzb/Y7rIRuNHJarI7vUAEQEAAYkBJQQYAQIADwUC\n"
+        + "Qs6nRQIbDAUJAeEzgAAKCRA87HgbF94azRiBB/4vAyOOjUjK3lDWjHGs7mvEWJI/\n"
+        + "1MeLlGPswCSInJBa+HMiMI4tzq+hu5ejGThojNbmnL96GdzfDkMlP4Feyxb2rjtb\n"
+        + "NrD/R5tlXHmjX/QLzep4LCeMziP80fu8qUeiOej/Ecdny0w365PlMdt10RaYR8VE\n"
+        + "ZX/DAie6JfElnfQcG5q8TIOH3i71qxV+kIoPqKWfQ0MXrNEJ3BYFfDGdUt8U1Kq9\n"
+        + "OuIHVRgGS7mMSyjgNqqp7MBeMY+PFFZaZel5yoYVjb9d3L8XvVv2eoa/jPj5FUEU\n"
+        + "kE9uxNmwaD1PiV8DvBTYI+eQL4qzfu+3NTG2SfgQYtj5oiGHw8aL3U6QHDJb\n"
+        + "=d/Xp\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "HDJb\n"
+        + "=RrXv\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A self-revoked key with no expiration.
+   *
+   * <pre>
+   * pub   2048R/7CA87821 2015-07-08 [revoked: 2015-07-08]
+   *       Key fingerprint = E328 CAB1 1F7E B1BC 1451  ABA5 0855 2A17 7CA8 7821
+   * uid                  Testuser Four &lt;test4@example.com&gt;
+   * </pre>
+   */
+  static final TestKey key4() throws PGPException, IOException {
+    return new TestKey(
+        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=477N\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=5aNq\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  // TODO(dborowitz): Figure out how to get gpg to revoke a key for someone
+  // else.
+
+  private final String pubArmored;
+  private final String secArmored;
+  private final PGPPublicKey pub;
+  private final PGPSecretKey sec;
+
+  private TestKey(String pubArmored, String secArmored)
+      throws PGPException, IOException {
+    this.pubArmored = pubArmored;
+    this.secArmored = secArmored;
+    BcKeyFingerprintCalculator fc = new BcKeyFingerprintCalculator();
+    this.pub = new PGPPublicKeyRing(newStream(pubArmored), fc).getPublicKey();
+    this.sec = new PGPSecretKeyRing(newStream(secArmored), fc).getSecretKey();
+  }
+
+  String getPublicKeyArmored() {
+    return pubArmored;
+  }
+
+  String getSecretKeyArmored() {
+    return secArmored;
+  }
+
+  PGPPublicKey getPublicKey() {
+    return pub;
+  }
+
+  PGPSecretKey getSecretKey() {
+    return sec;
+  }
+
+  long getKeyId() {
+    return pub.getKeyID();
+  }
+
+  String getFirstUserId() {
+    return (String) pub.getUserIDs().next();
+  }
+
+  PGPPrivateKey getPrivateKey() throws PGPException {
+    return sec.extractPrivateKey(
+        new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider())
+          // All test keys have no passphrase.
+          .build(new char[0]));
+  }
+
+  private static ArmoredInputStream newStream(String armored)
+      throws IOException {
+    return new ArmoredInputStream(
+        new ByteArrayInputStream(Constants.encode(armored)));
+  }
+
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
index 8d7866d..ac6d805 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
@@ -19,7 +19,6 @@
 import static com.google.gerrit.reviewdb.client.Change.Status.DRAFT;
 import static com.google.gerrit.reviewdb.client.Change.Status.MERGED;
 import static com.google.gerrit.reviewdb.client.Change.Status.NEW;
-import static com.google.gerrit.reviewdb.client.Change.Status.SUBMITTED;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
@@ -183,17 +182,17 @@
   public void testGetPossibleStatus() throws Exception {
     assertEquals(EnumSet.allOf(Change.Status.class), status("file:a"));
     assertEquals(EnumSet.of(NEW), status("is:new"));
-    assertEquals(EnumSet.of(SUBMITTED, DRAFT, MERGED, ABANDONED),
+    assertEquals(EnumSet.of(DRAFT, MERGED, ABANDONED),
         status("-is:new"));
     assertEquals(EnumSet.of(NEW, MERGED), status("is:new OR is:merged"));
 
     EnumSet<Change.Status> none = EnumSet.noneOf(Change.Status.class);
     assertEquals(none, status("is:new is:merged"));
-    assertEquals(none, status("(is:new is:draft) (is:merged is:submitted)"));
-    assertEquals(none, status("(is:new is:draft) (is:merged is:submitted)"));
+    assertEquals(none, status("(is:new is:draft) (is:merged)"));
+    assertEquals(none, status("(is:new is:draft) (is:merged)"));
 
-    assertEquals(EnumSet.of(MERGED, SUBMITTED),
-        status("(is:new is:draft) OR (is:merged OR is:submitted)"));
+    assertEquals(EnumSet.of(MERGED),
+        status("(is:new is:draft) OR (is:merged)"));
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 01f964b..f19987f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -287,7 +287,7 @@
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubject("Submit patch set 1");
 
-    update.submit(ImmutableList.of(
+    update.merge(ImmutableList.of(
         submitRecord("NOT_READY", null,
           submitLabel("Verified", "OK", changeOwner.getAccountId()),
           submitLabel("Code-Review", "NEED", null)),
@@ -314,7 +314,7 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubject("Submit patch set 1");
-    update.submit(ImmutableList.of(
+    update.merge(ImmutableList.of(
         submitRecord("OK", null,
           submitLabel("Code-Review", "OK", otherUser.getAccountId()))));
     update.commit();
@@ -322,7 +322,7 @@
     incrementPatchSet(c);
     update = newUpdate(c, changeOwner);
     update.setSubject("Submit patch set 2");
-    update.submit(ImmutableList.of(
+    update.merge(ImmutableList.of(
         submitRecord("OK", null,
           submitLabel("Code-Review", "OK", changeOwner.getAccountId()))));
     update.commit();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 49b61cc..c206902 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -108,7 +108,7 @@
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubject("Submit patch set 1");
 
-    update.submit(ImmutableList.of(
+    update.merge(ImmutableList.of(
         submitRecord("NOT_READY", null,
           submitLabel("Verified", "OK", changeOwner.getAccountId()),
           submitLabel("Code-Review", "NEED", null)),
@@ -121,7 +121,7 @@
     assertBodyEquals("Submit patch set 1\n"
         + "\n"
         + "Patch-set: 1\n"
-        + "Status: submitted\n"
+        + "Status: merged\n"
         + "Submitted-with: NOT_READY\n"
         + "Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
         + "Submitted-with: NEED: Code-Review\n"
@@ -173,14 +173,14 @@
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubject("Submit patch set 1");
 
-    update.submit(ImmutableList.of(
+    update.merge(ImmutableList.of(
         submitRecord("RULE_ERROR", "Problem with patch set:\n1")));
     update.commit();
 
     assertBodyEquals("Submit patch set 1\n"
         + "\n"
         + "Patch-set: 1\n"
-        + "Status: submitted\n"
+        + "Status: merged\n"
         + "Submitted-with: RULE_ERROR Problem with patch set: 1\n",
         update.getRevision());
   }
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
index 2bef633..dcff98e 100644
--- a/gerrit-sshd/BUCK
+++ b/gerrit-sshd/BUCK
@@ -8,6 +8,7 @@
     '//gerrit-cache-h2:cache-h2',
     '//gerrit-common:annotations',
     '//gerrit-common:server',
+    '//gerrit-lucene:lucene',
     '//gerrit-patch-jgit:server',
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index c44dd88..06b1cf5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.server.config.DownloadConfig;
 import com.google.gerrit.sshd.CommandModule;
 import com.google.gerrit.sshd.CommandName;
 import com.google.gerrit.sshd.Commands;
@@ -23,14 +25,18 @@
 
 /** Register the commands a Gerrit server supports. */
 public class DefaultCommandModule extends CommandModule {
-  public DefaultCommandModule(boolean slave) {
+  private final DownloadConfig downloadConfig;
+
+  public DefaultCommandModule(boolean slave, DownloadConfig downloadCfg) {
     slaveMode = slave;
+    downloadConfig = downloadCfg;
   }
 
   @Override
   protected void configure() {
     final CommandName git = Commands.named("git");
     final CommandName gerrit = Commands.named("gerrit");
+    CommandName index = Commands.named(gerrit, "index");
     final CommandName logging = Commands.named(gerrit, "logging");
     final CommandName plugin = Commands.named(gerrit, "plugin");
     final CommandName testSubmit = Commands.named(gerrit, "test-submit");
@@ -51,8 +57,12 @@
     command(gerrit, StreamEvents.class);
     command(gerrit, VersionCommand.class);
     command(gerrit, GarbageCollectionCommand.class);
-    command(gerrit, "plugin").toProvider(new DispatchCommandProvider(plugin));
 
+    command(index).toProvider(new DispatchCommandProvider(index));
+    command(index, IndexActivateCommand.class);
+    command(index, IndexStartCommand.class);
+
+    command(gerrit, "plugin").toProvider(new DispatchCommandProvider(plugin));
     command(plugin, PluginLsCommand.class);
     command(plugin, PluginEnableCommand.class);
     command(plugin, PluginInstallCommand.class);
@@ -68,10 +78,12 @@
     command("scp").to(ScpCommand.class);
 
     // Honor the legacy hyphenated forms as aliases for the non-hyphenated forms
-    command("git-upload-pack").to(Commands.key(git, "upload-pack"));
-    command(git, "upload-pack").to(Upload.class);
-    command("git-upload-archive").to(Commands.key(git, "upload-archive"));
-    command(git, "upload-archive").to(UploadArchive.class);
+    if (sshEnabled()) {
+      command("git-upload-pack").to(Commands.key(git, "upload-pack"));
+      command(git, "upload-pack").to(Upload.class);
+      command("git-upload-archive").to(Commands.key(git, "upload-archive"));
+      command(git, "upload-archive").to(UploadArchive.class);
+    }
     command("suexec").to(SuExec.class);
     listener().to(ShowCaches.StartupListener.class);
 
@@ -87,9 +99,11 @@
       command(git, "receive-pack").to(NotSupportedInSlaveModeFailureCommand.class);
       command(gerrit, "test-submit").to(NotSupportedInSlaveModeFailureCommand.class);
     } else {
-      command("git-receive-pack").to(Commands.key(git, "receive-pack"));
-      command("gerrit-receive-pack").to(Commands.key(git, "receive-pack"));
-      command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack"));
+      if (sshEnabled()) {
+        command("git-receive-pack").to(Commands.key(git, "receive-pack"));
+        command("gerrit-receive-pack").to(Commands.key(git, "receive-pack"));
+        command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack"));
+      }
       command(gerrit, "test-submit").toProvider(
           new DispatchCommandProvider(testSubmit));
     }
@@ -114,4 +128,10 @@
     alias(logging, "ls", ListLoggingLevelCommand.class);
     alias(logging, "set", SetLoggingLevelCommand.class);
   }
+
+  private boolean sshEnabled() {
+    return downloadConfig.getDownloadSchemes().contains(DownloadScheme.SSH)
+        || downloadConfig.getDownloadSchemes().contains(
+            DownloadScheme.DEFAULT_DOWNLOADS);
+  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
new file mode 100644
index 0000000..dc67ac3
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
@@ -0,0 +1,49 @@
+// 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.sshd.commands;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.lucene.LuceneVersionManager;
+import com.google.gerrit.lucene.ReindexerAlreadyRunningException;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "activate",
+  description = "Activate the latest index version available",
+  runsAt = MASTER)
+public class IndexActivateCommand extends SshCommand {
+
+  @Inject
+  private LuceneVersionManager luceneVersionManager;
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    try {
+      if (luceneVersionManager.activateLatestIndex()) {
+        stdout.println("Activated latest index version");
+      } else {
+        stdout.println("Not activating index, already using latest version");
+      }
+    } catch (ReindexerAlreadyRunningException e) {
+      throw new UnloggedFailure("Failed to activate latest index: "
+          + e.getMessage());
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
new file mode 100644
index 0000000..1b3b819
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
@@ -0,0 +1,47 @@
+// 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.sshd.commands;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.lucene.LuceneVersionManager;
+import com.google.gerrit.lucene.ReindexerAlreadyRunningException;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "start", description = "Start the online reindexer",
+  runsAt = MASTER)
+public class IndexStartCommand extends SshCommand {
+
+  @Inject
+  private LuceneVersionManager luceneVersionManager;
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    try {
+      if (luceneVersionManager.startReindexer()) {
+        stdout.println("Reindexer started");
+      } else {
+        stdout.println("Nothing to reindex, index is already the latest version");
+      }
+    } catch (ReindexerAlreadyRunningException e) {
+      throw new UnloggedFailure("Failed to start reindexer: " + e.getMessage());
+    }
+  }
+}
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 8968c81..22f3f0b 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
+import com.google.gerrit.server.config.DownloadConfig;
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerConfigModule;
@@ -330,7 +331,8 @@
     final List<Module> modules = new ArrayList<>();
     modules.add(sysInjector.getInstance(SshModule.class));
     modules.add(new SshHostKeyModule());
-    modules.add(new DefaultCommandModule(false));
+    modules.add(new DefaultCommandModule(false,
+        sysInjector.getInstance(DownloadConfig.class)));
     return sysInjector.createChildInjector(modules);
   }
 
diff --git a/lib/jgit/BUCK b/lib/jgit/BUCK
index a84197f..1c2d6a3 100644
--- a/lib/jgit/BUCK
+++ b/lib/jgit/BUCK
@@ -1,13 +1,13 @@
 include_defs('//lib/maven.defs')
 
 REPO = GERRIT # Leave here even if set to MAVEN_CENTRAL.
-VERS = '4.0.1.201506240215-r.65-g3c33d09'
+VERS = '4.0.1.201506240215-r.78-g19b45da'
 
 maven_jar(
   name = 'jgit',
   id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
-  bin_sha1 = '44b6233933768ed82ccab7dddf620b1c57c0af75',
-  src_sha1 = '1b50482ca2cf525592e4c5bd4024c58ddaf9e56f',
+  bin_sha1 = '05f061b49f595036cde5154cf2fdaa48f6685091',
+  src_sha1 = '9f32cdda73404326546ad853b942e7e4f596c873',
   license = 'jgit',
   repository = REPO,
   unsign = True,
@@ -22,7 +22,7 @@
 maven_jar(
   name = 'jgit-servlet',
   id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
-  sha1 = '44efd7bc2c4bf9054bd37a28d28f20f15fa32e6a',
+  sha1 = '0a9da2e634f2c741627207773ce45ba15effeeec',
   license = 'jgit',
   repository = REPO,
   deps = [':jgit'],
@@ -36,7 +36,7 @@
 maven_jar(
   name = 'jgit-archive',
   id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
-  sha1 = '8096326e060578bfa74435e9d758ca0b1f1e384b',
+  sha1 = '6f37fa68ba81e9e669f20383d248f4098a23fb19',
   license = 'jgit',
   repository = REPO,
   deps = [':jgit',
@@ -53,7 +53,7 @@
 maven_jar(
   name = 'junit',
   id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
-  sha1 = 'e4442bdcc60672257a825d326bfe457218014ec3',
+  sha1 = 'eab0158a98c9bbe966a1d2f2d8ec830dc23b2d9e',
   license = 'DO_NOT_DISTRIBUTE',
   repository = REPO,
   unsign = True,
diff --git a/plugins/download-commands b/plugins/download-commands
index de482c4..334f725 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit de482c42d46bc6254370ccb33fe9700697c05e26
+Subproject commit 334f7253855551bea40f0f27b68e3d7aa71fe4af
diff --git a/plugins/replication b/plugins/replication
index 852dc51..7a3a89f 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 852dc51d1a9d41f69f3263631767de41f1bc81dd
+Subproject commit 7a3a89fc983b9bcb5b2c965affd7a83bb6b10bb2
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 5d6522a..d81e2d6 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 5d6522a4adf13dc34a9b9fe573c0a7370d9cf94e
+Subproject commit d81e2d6d3edc27c7aceea47518cbb03fb5590f11
diff --git a/tools/download_file.py b/tools/download_file.py
index 061d67c..97d982f 100755
--- a/tools/download_file.py
+++ b/tools/download_file.py
@@ -25,7 +25,11 @@
 from zipfile import ZipFile, BadZipfile, LargeZipFile
 
 GERRIT_HOME = path.expanduser('~/.gerritcodereview')
-CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache')
+CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache', 'downloaded-artifacts')
+# LEGACY_CACHE_DIR is only used to allow existing workspaces to move already
+# downloaded files to the new cache directory.
+# Please remove after 3 months (2015-10-07).
+LEGACY_CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache')
 LOCAL_PROPERTIES = 'local.properties'
 
 
@@ -85,6 +89,15 @@
   name = '%s-%s' % (path.basename(args.o), h)
   return path.join(CACHE_DIR, name)
 
+# Please remove after 3 months (2015-10-07). See LEGACY_CACHE_DIR above.
+def legacy_cache_entry(args):
+  if args.v:
+    h = args.v
+  else:
+    h = sha1(args.u.encode('utf-8')).hexdigest()
+  name = '%s-%s' % (path.basename(args.o), h)
+  return path.join(LEGACY_CACHE_DIR, name)
+
 
 opts = OptionParser()
 opts.add_option('-o', help='local output file')
@@ -103,8 +116,19 @@
 
 redirects = download_properties(root_dir)
 cache_ent = cache_entry(args)
+legacy_cache_ent = legacy_cache_entry(args)
 src_url = resolve_url(args.u, redirects)
 
+# Please remove after 3 months (2015-10-07). See LEGACY_CACHE_DIR above.
+if not path.exists(cache_ent) and path.exists(legacy_cache_ent):
+  try:
+    safe_mkdirs(path.dirname(cache_ent))
+  except OSError as err:
+    print('error creating directory %s: %s' %
+          (path.dirname(cache_ent), err), file=stderr)
+    exit(1)
+  shutil.move(legacy_cache_ent, cache_ent)
+
 if not path.exists(cache_ent):
   try:
     safe_mkdirs(path.dirname(cache_ent))
diff --git a/tools/version.py b/tools/version.py
index eb1e076..e2d9ead 100755
--- a/tools/version.py
+++ b/tools/version.py
@@ -19,12 +19,6 @@
 import re
 import sys
 
-version_text = """# Maven style API version (e.g. '2.x-SNAPSHOT').
-# Used by :api_install and :api_deploy targets
-# when talking to the destination repository.
-#
-GERRIT_VERSION = '%s'
-"""
 parser = OptionParser()
 opts, args = parser.parse_args()
 
@@ -33,31 +27,34 @@
 elif len(args) > 1:
   parser.error('too many arguments')
 
-new_version = args[0]
-pattern = re.compile(r'(\s*)<version>[-.\w]+</version>')
+DEST_PATTERN = r'\g<1>%s\g<3>' % args[0]
 
+
+def replace_in_file(filename, src_pattern):
+  try:
+    f = open(filename, "r")
+    s = f.read()
+    f.close()
+    s = re.sub(src_pattern, DEST_PATTERN, s)
+    f = open(filename, "w")
+    f.write(s)
+    f.close()
+  except IOError as err:
+    print('error updating %s: %s' % (filename, err), file=sys.stderr)
+
+
+src_pattern = re.compile(r'^(\s*<version>)([-.\w]+)(</version>\s*)$',
+                         re.MULTILINE)
 for project in ['gerrit-extension-api', 'gerrit-plugin-api',
                 'gerrit-plugin-archetype', 'gerrit-plugin-gwt-archetype',
                 'gerrit-plugin-gwtui', 'gerrit-plugin-js-archetype',
                 'gerrit-war']:
   pom = os.path.join(project, 'pom.xml')
-  try:
-    outxml = ""
-    found = False
-    for line in open(pom, "r"):
-      m = pattern.match(line)
-      if m and not found:
-        outxml += "%s<version>%s</version>\n" % (m.group(1), new_version)
-        found = True
-      else:
-        outxml += line
-    with open(pom, "w") as outfile:
-      outfile.write(outxml)
-  except IOError as err:
-    print('error updating %s: %s' % (pom, err), file=sys.stderr)
+  replace_in_file(pom, src_pattern)
 
-try:
-  with open('VERSION', "w") as version_file:
-    version_file.write(version_text % new_version)
-except IOError as err:
-  print('error updating VERSION: %s' % err, file=sys.stderr)
+src_pattern = re.compile(r"^(GERRIT_VERSION = ')([-.\w]+)(')$", re.MULTILINE)
+replace_in_file('VERSION', src_pattern)
+
+src_pattern = re.compile(r'^(\s*-DarchetypeVersion=)([-.\w]+)(\s*\\)$',
+                         re.MULTILINE)
+replace_in_file(os.path.join('Documentation', 'dev-plugins.txt'), src_pattern)