Merge "Make ChangeControl#canDelete(ReviewDb, Change.Status) private"
diff --git a/Documentation/cmd-apropos.txt b/Documentation/cmd-apropos.txt
index 31d21c1..2ef71bf 100644
--- a/Documentation/cmd-apropos.txt
+++ b/Documentation/cmd-apropos.txt
@@ -15,7 +15,7 @@
 from the matched documents.
 
 == ACCESS
-Any user who has configured an SSH key.
+Any user who has SSH access to Gerrit.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index 503bd12..4d1ea05 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -102,6 +102,7 @@
 * FAST_FORWARD_ONLY: produces a strictly linear history.
 * MERGE_IF_NECESSARY: create a merge commit when required.
 * REBASE_IF_NECESSARY: rebase the commit when required.
+* REBASE_ALWAYS: always rebase the commit including dependencies.
 * MERGE_ALWAYS: always create a merge commit.
 * CHERRY_PICK: always cherry-pick the commit.
 
diff --git a/Documentation/cmd-ls-groups.txt b/Documentation/cmd-ls-groups.txt
index d8eef8b..6d4bdc5 100644
--- a/Documentation/cmd-ls-groups.txt
+++ b/Documentation/cmd-ls-groups.txt
@@ -23,7 +23,7 @@
 all groups are listed.
 
 == ACCESS
-Any user who has configured an SSH key.
+Any user who has SSH access to Gerrit.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-ls-members.txt b/Documentation/cmd-ls-members.txt
index a6d492c..273451b 100644
--- a/Documentation/cmd-ls-members.txt
+++ b/Documentation/cmd-ls-members.txt
@@ -16,7 +16,7 @@
 shown tab-separated.
 
 == ACCESS
-Any user who has configured an SSH key.
+Any user who has SSH access to Gerrit.
 
 == SCRIPTING
 This command is intended to be used in scripts. Output is either an error
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index e2e71ff..486ca44 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -25,7 +25,7 @@
 group, all projects are listed.
 
 == ACCESS
-Any user who has configured an SSH key, or by an user over HTTP.
+Any user who has SSH access to Gerrit.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index 1faf1b0..90e5cdd 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -108,7 +108,7 @@
 	will be used to cut the result set.
 
 == ACCESS
-Any user who has configured an SSH key.
+Any user who has SSH access to Gerrit.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-receive-pack.txt b/Documentation/cmd-receive-pack.txt
index 798f872..b62b9a9 100644
--- a/Documentation/cmd-receive-pack.txt
+++ b/Documentation/cmd-receive-pack.txt
@@ -37,7 +37,7 @@
 	Deprecated, use `refs/for/branch%cc=address` instead.
 
 == ACCESS
-Any user who has configured an SSH key.
+Any user who has SSH access to Gerrit.
 
 == EXAMPLES
 
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 4e24701..8f40d6c 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -150,7 +150,7 @@
   invocations of the SSH command are required.
 
 == ACCESS
-Any user who has configured an SSH key.
+Any user who has SSH access to Gerrit.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-set-members.txt b/Documentation/cmd-set-members.txt
index ae44843..5fb2bb9 100644
--- a/Documentation/cmd-set-members.txt
+++ b/Documentation/cmd-set-members.txt
@@ -49,7 +49,7 @@
 order: `--remove`, `--exclude`, `--add`, `--include`
 
 == ACCESS
-Any user who has configured an SSH key.
+Any user who has SSH access to Gerrit.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-set-project.txt b/Documentation/cmd-set-project.txt
index 62d6e92..7282e28 100644
--- a/Documentation/cmd-set-project.txt
+++ b/Documentation/cmd-set-project.txt
@@ -53,6 +53,7 @@
 * FAST_FORWARD_ONLY: produces a strictly linear history.
 * MERGE_IF_NECESSARY: create a merge commit when required.
 * REBASE_IF_NECESSARY: rebase the commit when required.
+* REBASE_ALWAYS: always rebase the commit including dependencies.
 * MERGE_ALWAYS: always create a merge commit.
 * CHERRY_PICK: always cherry-pick the commit.
 
diff --git a/Documentation/cmd-set-reviewers.txt b/Documentation/cmd-set-reviewers.txt
index 3d53456..0a757fd 100644
--- a/Documentation/cmd-set-reviewers.txt
+++ b/Documentation/cmd-set-reviewers.txt
@@ -47,7 +47,7 @@
 	Display site-specific usage information
 
 == ACCESS
-Any user who has configured an SSH key.
+Any user who has SSH access to Gerrit.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-version.txt b/Documentation/cmd-version.txt
index cc797cc..85b0491 100644
--- a/Documentation/cmd-version.txt
+++ b/Documentation/cmd-version.txt
@@ -26,7 +26,7 @@
 `<n>` is computed.
 
 == ACCESS
-Any user who has configured an SSH key.
+Any user who has SSH access to Gerrit.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 1dc43dd..fccc32e 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -42,6 +42,51 @@
   url = jdbc:postgresql://<host>:<port>/<db_name>?user=<user>&password=<password>
 ----
 
+[[accountPatchReviewDb.poolLimit]]accountPatchReviewDb.poolLimit::
++
+Maximum number of open database connections.  If the server needs
+more than this number, request processing threads will wait up
+to <<accountPatchReviewDb.poolMaxWait, poolMaxWait>> seconds for a
+connection to be released before they abort with an exception.
+This limit must be several units higher than the total number of
+httpd and sshd threads as some request processing code paths may
+need multiple connections.
++
+Default is <<sshd.threads, sshd.threads>>
+ + <<httpd.maxThreads, httpd.maxThreads>> + 2.
++
+
+[[accountPatchReviewDb.poolMinIdle]]database.poolMinIdle::
++
+Minimum number of connections to keep idle in the pool.
+Default is 4.
++
+
+[[accountPatchReviewDb.poolMaxIdle]]accountPatchReviewDb.poolMaxIdle::
++
+Maximum number of connections to keep idle in the pool.  If there
+are more idle connections, connections will be closed instead of
+being returned back to the pool.
+Default is min(<<accountPatchReviewDb.poolLimit, accountPatchReviewDb.poolLimit>>, 16).
++
+
+[[accountPatchReviewDb.poolMaxWait]]accountPatchReviewDb.poolMaxWait::
++
+Maximum amount of time a request processing thread will wait to
+acquire a database connection from the pool.  If no connection is
+released within this time period, the processing thread will abort
+its current operations and return an error to the client.
+Values should use common unit suffixes to express their setting:
++
+* ms, milliseconds
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
+
++
+If a unit suffix is not specified, `milliseconds` is assumed.
+Default is `30 seconds`.
+
 [[accounts]]
 === Section accounts
 
@@ -707,8 +752,6 @@
 * `accounts`
 +
 * `account_group_members`
-+
-* `account_external_ids`
 
 +
 If direct updates are made to any of these database tables, this
@@ -1146,16 +1189,6 @@
 +
 The default limit is 1024kB.
 
-[[change.privateByDefault]]change.privateByDefault::
-+
-If set to true, every change created will be private by default.
-+
-Note that the newly created change will be public if the `is_private` field in
-link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
-or the `remove-private` link:user-upload.html#private[PushOption] is used in the push.
-+
-The default is false.
-
 [[changeCleanup]]
 === Section changeCleanup
 
@@ -3617,7 +3650,9 @@
 +
 The default submit type for newly created projects. Supported values
 are `MERGE_IF_NECESSARY`, `FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`,
-`MERGE_ALWAYS` and `CHERRY_PICK`.
+`REBASE_ALWAYS`, `MERGE_ALWAYS` and `CHERRY_PICK`.
++
+For more details see link:project-configuration.html#submit_type[Submit Types].
 +
 By default, `MERGE_IF_NECESSARY`.
 
@@ -4557,6 +4592,21 @@
 +
 By default this is true.
 
+[[submodule.maxCombinedCommitMessageSize]]submodule.maxCombinedCommitMessageSize::
++
+This allows to limit the length of the commit message for a submodule.
++
+By default this is 262144 (256 KiB).
++
+Common unit suffixes of k, m, or g are supported.
+
+[[submodule.maxCommitMessages]]submodule.maxCommitMessages::
++
+This allows to limit the number of commit messages that should be combined when creating
+a commit message for a submodule.
++
+By default this is 1000.
+
 [[user]]
 === Section user
 
@@ -4584,8 +4634,9 @@
 +
 By default "Anonymous Coward" is used.
 
+[[secure.config]]
+== File `etc/secure.config`
 
-== [[secure.config]]File `etc/secure.config`
 The optional file `'$site_path'/etc/secure.config` overrides (or
 supplements) the settings supplied by `'$site_path'/etc/gerrit.config`.
 The file should be readable only by the daemon process and can be
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index e4b9a83..532b8c42 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -11,11 +11,10 @@
 [[label_Code-Review]]
 == Label: Code-Review
 
-The code review label is the second of two default labels that is
-configured upon the creation of a Gerrit instance.  It may have any
-meaning the project desires.  It was originally invented by the Android
-Open Source Project to mean 'I read the code and it seems reasonably
-correct'.
+The Code-Review label is configured upon the creation of a Gerrit
+instance.  It may have any meaning the project desires.  It was
+originally invented by the Android Open Source Project to mean
+'I read the code and it seems reasonably correct'.
 
 The range of values is:
 
@@ -87,8 +86,10 @@
 Project to mean 'compiles, passes basic unit tests'.  Some CI tools
 expect to use the Verified label to vote on a change after running.
 
-Administrators can install the Verified label by adding the following
-text to `project.config`:
+During site initialization the administrator may have chosen to
+configure the default Verified label for all projects.  In case it is
+desired to configure it at a later time, administrators can do this by
+adding the following to `project.config` in `All-Projects`:
 
 ----
   [label "Verified"]
@@ -96,6 +97,7 @@
       value = -1 Fails
       value =  0 No score
       value = +1 Verified
+      copyAllScoresIfNoCodeChange = true
 ----
 
 The range of values is:
@@ -315,8 +317,8 @@
 the commit message is different. This can be used to enable sticky
 approvals on labels that only depend on the code, reducing turn-around
 if only the commit message is changed prior to submitting a change.
-For the Verified label that is installed by the link:pgm-init.html[init]
-site program this is enabled by default.
+For the Verified label that is optionally installed by the
+link:pgm-init.html[init] site program this is enabled by default.
 
 Defaults to false.
 
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 0183534..22f785e 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -199,6 +199,24 @@
 Default is `INHERIT`, which means that this property is inherited from
 the parent project.
 
+[[change-section]]
+=== Change section
+
+The change section includes configuration for project-specific change settings:
+
+[[change.privateByDefault]]change.privateByDefault::
++
+Controls whether all new changes in the project are set as private by default.
++
+Note that a new change will be public if the `is_private` field in
+link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
+when calling the link:rest-api-changes.html#create-change[CreateChange] REST API
+or the `remove-private` link:user-upload.html#private[PushOption] is used during
+the Git push.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
 [[submit-section]]
 === Submit section
 
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index e82e16a..26193f4 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -89,22 +89,14 @@
 . link:#merge-stable[Merge `stable` into `master`]
 
 
-[[update-versions]]
-=== Update Versions and Create Release Tag
+[[update-version]]
+=== Update Version and Create Release Tag
 
 Before doing the release build, the `GERRIT_VERSION` in the `version.bzl`
 file must be updated, e.g. change it from `2.5-SNAPSHOT` to `2.5`.
 
-In addition the version must be updated in a number of pom.xml files.
-
-To do this run the `./tools/version.py` script and provide the new
-version as parameter, e.g.:
-
-----
-  ./tools/version.py 2.5
-----
-
-Commit the changes and create a signed release tag on the new commit:
+Commit the change in `version.bzl` and create a signed release tag on the
+new commit:
 
 ----
   git tag -s -m "v2.5" v2.5
@@ -147,9 +139,8 @@
 link:dev-release-deploy-config.html#deploy-configuration-setting-maven-central[
 configuration] for deploying to Maven Central
 
-* Make sure that the version is updated in the `version.bzl` file and in
-the `pom.xml` files as described in the link:#update-versions[Update
-Versions and Create Release Tag] section.
+* Make sure that the version is updated in the `version.bzl` file as described
+in the link:#update-version[Update Version and Create Release Tag] section.
 
 * Push the WAR to Maven Central:
 +
diff --git a/Documentation/error-permission-denied.txt b/Documentation/error-permission-denied.txt
index 574818d..879273d 100644
--- a/Documentation/error-permission-denied.txt
+++ b/Documentation/error-permission-denied.txt
@@ -3,15 +3,20 @@
 With this error message an SSH command to Gerrit is rejected if the
 SSH authentication is not successful.
 
-The link:http://en.wikipedia.org/wiki/Secure_Shell[SSH] protocol uses link:http://en.wikipedia.org/wiki/Public-key_cryptography[Public-key Cryptography] for authentication.
-This means for a successful SSH authentication you need your private
-SSH key and the corresponding public SSH key must be known to Gerrit.
+The link:http://en.wikipedia.org/wiki/Secure_Shell[SSH] protocol can use
+link:http://en.wikipedia.org/wiki/Public-key_cryptography[Public-key Cryptography]
+for authentication.
+In general configurations, Gerrit will authenticate you by the public keys
+known to you. Optionally, it can be configured by the administrator to allow
+for link:config-gerrit.html#sshd.kerberosKeytab[kerberos] authentication
+instead.
 
-If you are facing this problem, do the following:
+In any case, verify that you are using the correct username for the SSH command
+and that it is typed correctly (case sensitive). You can look up your username
+in the Gerrit Web UI under 'Settings' -> 'Profile'.
 
-. Verify that you are using the correct username for the SSH command
-  and that it is typed correctly (case sensitive). You can look up
-  your username in the Gerrit Web UI under 'Settings' -> 'Profile'.
+If you are facing this problem and using an SSH keypair, do the following:
+
 . Verify that you have uploaded your public SSH key for your Gerrit
   account. To do this go in the Gerrit Web UI to 'Settings' ->
   'SSH Public Keys' and check that your public SSH key is there. If
@@ -21,6 +26,19 @@
   described below. From the trace you should see which private SSH
   key is used.
 
+Debugging kerberos issues can be quite hard given the complexity of the
+protocol. In case you are using kerberos authentication, do the following:
+
+. Verify that you have acquired a valid initial ticket. On a Linux machine, you
+  can acquire one using the `kinit` command. List all your tickets using the
+  `klist` command. It should list all principals for which you have acquired a
+  ticket and include a principal name corresponding to your Gerrit server, for
+  example `HOST/gerrit.mydomain.tld@MYDOMAIN.TLD`.
+  Note that tickets can expire and require you to re-run `kinit` periodically.
+. Verify that your SSH client is using kerberos authentication. For OpenSSH
+  clients this can be controlled using the `GSSAPIAuthentication` setting.
+  For more information see
+  link:user-upload.html#configure_ssh_kerberos[SSH kerberos configuration].
 
 == Test SSH authentication
 
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 9aa0a3b..948ec25 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -56,8 +56,8 @@
 and the link:user-upload.html#http[HTTP/HTTPS] protocols.
 
 [NOTE]
-To use SSH you must link:user-upload.html#configure_ssh[generate an SSH
-key pair and upload the public SSH key to Gerrit].
+To use SSH you may need to link:user-upload.html#ssh[configure your SSH public
+key in your `Settings`].
 
 [[code-review]]
 == Code Review Workflow
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 8cddce2..901f15a 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -86,6 +86,10 @@
 
 * `batch_update/execute_change_ops`: BatchUpdate change update latency,
 excluding reindexing
+* `batch_update/retry_attempt_counts`: Distribution of number of attempts made
+by RetryHelper (1 == single attempt, no retry)
+* `batch_update/retry_timeout_count`: Number of executions of RetryHelper that
+ultimately timed out
 
 === NoteDb
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 557fb3c..09fea83 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -517,61 +517,6 @@
   }
 ----
 
-[[create-merge-patch-set-for-change]]
-=== Create Merge Patch Set For Change
---
-'POST /changes/link:#change-id[\{change-id\}]/merge'
---
-
-Update an existing change by using a
-link:#merge-patch-set-input[MergePatchSetInput] entity.
-
-Gerrit will create a merge commit based on the information of
-MergePatchSetInput and add a new patch set to the change corresponding
-to the new merge commit.
-
-.Request
-----
-  POST /changes/test~master~Ic5466d107c5294414710935a8ef3b0180fb848dc/merge  HTTP/1.0
-  Content-Type: application/json; charset=UTF-8
-
-  {
-    "source": "refs/12/1234/1"
-  }
-----
-
-As response a link:#change-info[ChangeInfo] entity with current revision is
-returned that describes the resulting change.
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  {
-    "id": "test~master~Ic5466d107c5294414710935a8ef3b0180fb848dc",
-    "project": "test",
-    "branch": "master",
-    "hashtags": [],
-    "change_id": "Ic5466d107c5294414710935a8ef3b0180fb848dc",
-    "subject": "Merge dev_branch into master",
-    "status": "NEW",
-    "created": "2016-09-23 18:08:53.238000000",
-    "updated": "2016-09-23 18:09:25.934000000",
-    "submit_type": "MERGE_IF_NECESSARY",
-    "mergeable": true,
-    "insertions": 5,
-    "deletions": 0,
-    "_number": 72,
-    "owner": {
-      "_account_id": 1000000
-    },
-    "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822"
-  }
-----
-
 [[get-change-detail]]
 === Get Change Detail
 --
@@ -803,6 +748,97 @@
   }
 ----
 
+[[create-merge-patch-set-for-change]]
+=== Create Merge Patch Set For Change
+--
+'POST /changes/link:#change-id[\{change-id\}]/merge'
+--
+
+Update an existing change by using a
+link:#merge-patch-set-input[MergePatchSetInput] entity.
+
+Gerrit will create a merge commit based on the information of
+MergePatchSetInput and add a new patch set to the change corresponding
+to the new merge commit.
+
+.Request
+----
+  POST /changes/test~master~Ic5466d107c5294414710935a8ef3b0180fb848dc/merge  HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "subject": "Merge dev_branch into master",
+    "merge": {
+      "source": "refs/12/1234/1"
+    }
+  }
+----
+
+As response a link:#change-info[ChangeInfo] entity with current revision is
+returned that describes the resulting change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "test~master~Ic5466d107c5294414710935a8ef3b0180fb848dc",
+    "project": "test",
+    "branch": "master",
+    "hashtags": [],
+    "change_id": "Ic5466d107c5294414710935a8ef3b0180fb848dc",
+    "subject": "Merge dev_branch into master",
+    "status": "NEW",
+    "created": "2016-09-23 18:08:53.238000000",
+    "updated": "2016-09-23 18:09:25.934000000",
+    "submit_type": "MERGE_IF_NECESSARY",
+    "mergeable": true,
+    "insertions": 5,
+    "deletions": 0,
+    "_number": 72,
+    "owner": {
+      "_account_id": 1000000
+    },
+    "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822"
+  }
+----
+
+[[set-message]]
+=== Set Commit Message
+--
+'PUT /changes/link:#change-id[\{change-id\}]/message'
+--
+
+Creates a new patch set with a new commit message.
+
+The new commit message must be provided in the request body inside a
+link:#commit-message-input[CommitMessageInput] entity and contain the change ID footer if
+link:project-configuration.html#require-change-id[Require Change-Id] was specified.
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/message HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "message": "New Commit message \n\nChange-Id: I10394472cbd17dd12454f229e4f6de00b143a444\n"
+  }
+----
+
+.Notifications
+
+An email will be sent using the "newpatchset" template.
+
+[options="header",cols="1,1"]
+|=============================
+|WIP State       |Default
+|Ready for review|owner, reviewers, CCs, stars, NEW_PATCHSETS watchers
+|Work in progress|owner
+|=============================
+
 [[get-topic]]
 === Get Topic
 --
@@ -927,6 +963,8 @@
 Returns a list of every user ever assigned to a change, in the order in which
 they were first assigned.
 
+[NOTE] Past assignees are only available when NoteDb is enabled.
+
 .Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/past_assignees HTTP/1.0
@@ -2312,6 +2350,80 @@
   PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unmute HTTP/1.0
 ----
 
+[[get-hashtags]]
+=== Get Hashtags
+--
+'GET /changes/link:#change-id[\{change-id\}]/hashtags'
+--
+
+Gets the hashtags associated with a change.
+
+[NOTE] Hashtags are only available when NoteDb is enabled.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/hashtags HTTP/1.0
+----
+
+As response the change's hashtags are returned as a list of strings.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    "hashtag1",
+    "hashtag2"
+  ]
+----
+
+[[set-hashtags]]
+=== Set Hashtags
+--
+'POST /changes/link:#change-id[\{change-id\}]/hashtags'
+--
+
+Adds and/or removes hashtags from a change.
+
+[NOTE] Hashtags are only available when NoteDb is enabled.
+
+The hashtags to add or remove must be provided in the request body inside a
+link:#hashtags-input[HashtagsInput] entity.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/hashtags HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "add" : [
+      "hashtag3"
+    ],
+    "remove" : [
+      "hashtag2"
+    ]
+  }
+----
+
+As response the change's hashtags are returned as a list of strings.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    "hashtag1",
+    "hashtag3"
+  ]
+----
+
+
 [[edit-endpoints]]
 == Change Edit Endpoints
 
@@ -2681,78 +2793,6 @@
   HTTP/1.1 204 No Content
 ----
 
-[[get-hashtags]]
-=== Get Hashtags
---
-'GET /changes/link:#change-id[\{change-id\}]/hashtags'
---
-
-Gets the hashtags associated with a change.
-
-[NOTE] Hashtags are only available when NoteDb is enabled.
-
-.Request
-----
-  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/hashtags HTTP/1.0
-----
-
-As response the change's hashtags are returned as a list of strings.
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  [
-    "hashtag1",
-    "hashtag2"
-  ]
-----
-
-[[set-hashtags]]
-=== Set Hashtags
---
-'POST /changes/link:#change-id[\{change-id\}]/hashtags'
---
-
-Adds and/or removes hashtags from a change.
-
-[NOTE] Hashtags are only available when NoteDb is enabled.
-
-The hashtags to add or remove must be provided in the request body inside a
-link:#hashtags-input[HashtagsInput] entity.
-
-.Request
-----
-  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/hashtags HTTP/1.0
-  Content-Type: application/json; charset=UTF-8
-
-  {
-    "add" : [
-      "hashtag3"
-    ],
-    "remove" : [
-      "hashtag2"
-    ]
-  }
-----
-
-As response the change's hashtags are returned as a list of strings.
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  [
-    "hashtag1",
-    "hashtag3"
-  ]
-----
 
 [[reviewer-endpoints]]
 == Reviewer Endpoints
@@ -3135,38 +3175,6 @@
   HTTP/1.1 204 No Content
 ----
 
-[[set-message]]
-=== Set Commit Message
---
-'PUT /changes/link:#change-id[\{change-id\}]/message'
---
-
-Creates a new patch set with a new commit message.
-
-The new commit message must be provided in the request body inside a
-link:#commit-message-input[CommitMessageInput] entity and contain the change ID footer if
-link:project-configuration.html#require-change-id[Require Change-Id] was specified.
-
-.Request
-----
-  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/message HTTP/1.0
-  Content-Type: application/json; charset=UTF-8
-
-  {
-    "message": "New Commit message \n\nChange-Id: I10394472cbd17dd12454f229e4f6de00b143a444\n"
-  }
-----
-
-.Notifications
-
-An email will be sent using the "newpatchset" template.
-
-[options="header",cols="1,1"]
-|=============================
-|WIP State       |Default
-|Ready for review|owner, reviewers, CCs, stars, NEW_PATCHSETS watchers
-|Work in progress|owner
-|=============================
 
 [[revision-endpoints]]
 == Revision Endpoints
@@ -5628,6 +5636,12 @@
 The name of the target branch. +
 The `refs/heads/` prefix is omitted.
 |`topic`              |optional|The topic to which this change belongs.
+|`assignee`           |optional|
+The assignee of the change as an link:rest-api-accounts.html#account-info[
+AccountInfo] entity.
+|`hashtags`           |optional|
+List of hashtags that are set on the change (only populated when NoteDb
+is enabled).
 |`change_id`          ||The Change-Id of the change.
 |`subject`            ||
 The subject of the change (header line of the commit message).
@@ -5642,6 +5656,9 @@
 |`submitted`          |only set for merged changes|
 The link:rest-api.html#timestamp[timestamp] of when the change was
 submitted.
+|`submitter`          |only set for merged changes|
+The user who submitted the change, as an
+link:rest-api-accounts.html#account-info[ AccountInfo] entity.
 |`starred`            |not set if `false`|
 Whether the calling user has starred this change with the default label.
 |`stars`              |optional|
@@ -6401,7 +6418,7 @@
 |Field Name      ||Description
 |`submit_type`   ||
 Submit type used for this change, can be `MERGE_IF_NECESSARY`,
-`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
+`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `REBASE_ALWAYS`, `MERGE_ALWAYS` or
 `CHERRY_PICK`.
 |`strategy`     |optional|
 The strategy of the merge, can be `recursive`, `resolve`,
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 3180eb4..7ee7336 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2573,13 +2573,16 @@
 |`reject_implicit_merges`|optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 implicit merges should be rejected on changes pushed to the project.
+|`private_by_default`     ||
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+all new changes are set as private by default.
 |`max_object_size_limit`     ||
 The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
 limit] of this project as a link:#max-object-size-limit-info[
 MaxObjectSizeLimitInfo] entity.
 |`submit_type`               ||
 The default submit type of the project, can be `MERGE_IF_NECESSARY`,
-`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
+`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `REBASE_ALWAYS`, `MERGE_ALWAYS` or
 `CHERRY_PICK`.
 |`match_author_to_committer_date` |optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that indicates whether
@@ -2657,7 +2660,7 @@
 If not set, this setting is not updated.
 |`submit_type`                             |optional|
 The default submit type of the project, can be `MERGE_IF_NECESSARY`,
-`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
+`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `REBASE_ALWAYS`, `MERGE_ALWAYS` or
 `CHERRY_PICK`. +
 If not set, the submit type is not updated.
 |`state`                                   |optional|
@@ -2963,8 +2966,8 @@
 Whether an empty initial commit should be created.
 |`submit_type`               |optional|
 The submit type that should be set for the project
-(`MERGE_IF_NECESSARY`, `REBASE_IF_NECESSARY`, `FAST_FORWARD_ONLY`,
-`MERGE_ALWAYS`, `CHERRY_PICK`). +
+(`MERGE_IF_NECESSARY`, `REBASE_IF_NECESSARY`, `REBASE_ALWAYS`,
+`FAST_FORWARD_ONLY`, `MERGE_ALWAYS`, `CHERRY_PICK`). +
 If not set, `MERGE_IF_NECESSARY` is set as submit type unless
 link:config-gerrit.html#repository.name.defaultSubmitType[
 repository.<name>.defaultSubmitType] is set to a different value.
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index deec660..cb00a84 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -9,8 +9,8 @@
 All three methods rely on authentication, which must first be configured
 by the uploading user.
 
-Gerrit supports two methods of authenticating the uploading user.  SSH
-public key, and HTTP/HTTPS.
+Gerrit supports two protocols for uploading changes; SSH and HTTP/HTTPS. These
+may not all be available for you, depending on the server configuration.
 
 [[http]]
 == HTTP/HTTPS
@@ -41,13 +41,15 @@
 [[ssh]]
 == SSH
 
-Each user uploading changes to Gerrit must configure one or more SSH
-public keys.  The per-user SSH key list can be accessed over the web
-within Gerrit by `Settings`, and then accessing the `SSH Public Keys`
-tab.
+To upload changes over SSH, Gerrit supports two forms of authentication: a
+user's public key or kerberos.
 
-[[configure_ssh]]
-=== Configuration
+Unless your Gerrit instance is configured to support
+link:config-gerrit.html#sshd.kerberosKeytab[kerberos] in your domain, only
+public key authentication can be used.
+
+[[configure_ssh_public_keys]]
+=== Public keys
 
 To register a new SSH key for use with Gerrit, paste the contents of
 your `id_rsa.pub` or `id_dsa.pub` file into the text box and click
@@ -79,10 +81,29 @@
 documentation, for more details on configuration of the agent
 process and how to add the private key.
 
+[[configure_ssh_kerberos]]
+=== Kerberos
+
+A kerberos-enabled server configuration allows for zero configuration in an
+existing single-sign-on environment.
+
+Your SSH client should be configured to enable kerberos authentication. For
+OpenSSH clients, this is controlled by the option `GSSAPIAuthentication` which
+should be set to `yes`.
+
+Some Linux distributions have packaged OpenSSH to enable this by default (e.g.
+Debian, Ubuntu). If this is not the case for your distribution, enable it for
+Gerrit with this entry in your local SSH configuration:
+
+----
+Host gerrit.mydomain.tld
+    GSSAPIAuthentication yes
+----
+
 [[test_ssh]]
 === Testing Connections
 
-To verify your SSH key is working correctly, try using an SSH client
+To verify your SSH authentication is working correctly, try using an SSH client
 to connect to Gerrit's SSHD port.  By default Gerrit runs on
 port 29418, using the same hostname as the web server:
 
diff --git a/gerrit-extension-api/pom.xml b/fake_pom.xml
similarity index 95%
rename from gerrit-extension-api/pom.xml
rename to fake_pom.xml
index 9468b67..a91178b4 100644
--- a/gerrit-extension-api/pom.xml
+++ b/fake_pom.xml
@@ -1,8 +1,8 @@
 <project>
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
-  <artifactId>gerrit-extension-api</artifactId>
-  <version>2.15-SNAPSHOT</version>
+  <artifactId>gerrit</artifactId>
+  <version>1</version> <!-- Do not edit; see version.bzl. -->
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
deleted file mode 100644
index 747f5d4..0000000
--- a/gerrit-acceptance-framework/pom.xml
+++ /dev/null
@@ -1,86 +0,0 @@
-<project>
-  <modelVersion>4.0.0</modelVersion>
-  <groupId>com.google.gerrit</groupId>
-  <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>2.15-SNAPSHOT</version>
-  <packaging>jar</packaging>
-  <name>Gerrit Code Review - Acceptance Test Framework</name>
-  <description>Framework for Gerrit's acceptance tests</description>
-  <url>https://www.gerritcodereview.com/</url>
-
-  <licenses>
-    <license>
-      <name>The Apache Software License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>
-    </license>
-  </licenses>
-
-  <scm>
-    <url>https://gerrit.googlesource.com/gerrit</url>
-    <connection>https://gerrit.googlesource.com/gerrit</connection>
-  </scm>
-
-  <developers>
-    <developer>
-      <name>Alice Kober-Sotzek</name>
-    </developer>
-    <developer>
-      <name>Andrew Bonventre</name>
-    </developer>
-    <developer>
-      <name>Becky Siegel</name>
-    </developer>
-    <developer>
-      <name>Dave Borowitz</name>
-    </developer>
-    <developer>
-      <name>David Ostrovsky</name>
-    </developer>
-    <developer>
-      <name>David Pursehouse</name>
-    </developer>
-    <developer>
-      <name>Edwin Kempin</name>
-    </developer>
-    <developer>
-      <name>Hugo Arès</name>
-    </developer>
-    <developer>
-      <name>Kasper Nilsson</name>
-    </developer>
-    <developer>
-      <name>Logan Hanks</name>
-    </developer>
-    <developer>
-      <name>Martin Fick</name>
-    </developer>
-    <developer>
-      <name>Saša Živkov</name>
-    </developer>
-    <developer>
-      <name>Shawn Pearce</name>
-    </developer>
-    <developer>
-      <name>Viktar Donich</name>
-    </developer>
-    <developer>
-      <name>Wyatt Allen</name>
-    </developer>
-  </developers>
-
-  <mailingLists>
-    <mailingList>
-      <name>Repo and Gerrit Discussion</name>
-      <post>repo-discuss@googlegroups.com</post>
-      <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe>
-      <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe>
-      <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive>
-    </mailingList>
-  </mailingLists>
-
-  <issueManagement>
-    <url>https://bugs.chromium.org/p/gerrit/issues/list</url>
-    <system>Gerrit Issue Tracker</system>
-  </issueManagement>
-</project>
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 3aef476..6a1e3b9 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -342,6 +342,20 @@
     server.getTestInjector().injectMembers(this);
     Transport.register(inProcessProtocol);
     toClose = Collections.synchronizedList(new ArrayList<Repository>());
+
+    // All groups which were added during the server start (e.g. in SchemaCreator) aren't contained
+    // in the instance of the group index which is available here and in tests. There are two
+    // reasons:
+    // 1) No group index is available in SchemaCreator when using an in-memory database. (This could
+    // be fixed by using the IndexManagerOnInit in InMemoryDatabase similar as BaseInit uses it.)
+    // 2) During the on-init part of the server start, we use another instance of the index than
+    // later on. As test indexes are non-permanent, closing an instance and opening another one
+    // removes all indexed data.
+    // As a workaround, we simply reindex all available groups here.
+    for (AccountGroup group : groupCache.all()) {
+      groupCache.evict(group);
+    }
+
     admin = accountCreator.admin();
     user = accountCreator.user();
 
@@ -1120,6 +1134,12 @@
     return name;
   }
 
+  protected String createAccount(String name, String group) throws Exception {
+    name = name(name);
+    accountCreator.create(name, group);
+    return name;
+  }
+
   protected RevCommit getHead(Repository repo, String name) throws Exception {
     try (RevWalk rw = new RevWalk(repo)) {
       Ref r = repo.exactRef(name);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 48aa0bb..0d68f4a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -31,6 +31,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.junit.Assert.fail;
@@ -50,6 +51,7 @@
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
@@ -98,6 +100,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.EnumSet;
@@ -707,7 +710,8 @@
   @Test
   public void lookUpByPreferredEmail() throws Exception {
     // create an inconsistent account that has a preferred email without external ID
-    String prefEmail = "foo.preferred@example.com";
+    String prefix = "foo.preferred";
+    String prefEmail = prefix + "@example.com";
     TestAccount foo = accountCreator.create(name("foo"));
     accountsUpdate.create().update(db, foo.id, a -> a.setPreferredEmail(prefEmail));
 
@@ -715,6 +719,14 @@
     ImmutableSet<Account.Id> accountsByPrefEmail = emails.getAccountFor(prefEmail);
     assertThat(accountsByPrefEmail).hasSize(1);
     assertThat(Iterables.getOnlyElement(accountsByPrefEmail)).isEqualTo(foo.id);
+
+    // look up by email prefix doesn't find the account
+    accountsByPrefEmail = emails.getAccountFor(prefix);
+    assertThat(accountsByPrefEmail).isEmpty();
+
+    // look up by other case doesn't find the account
+    accountsByPrefEmail = emails.getAccountFor(prefEmail.toUpperCase(Locale.US));
+    assertThat(accountsByPrefEmail).isEmpty();
   }
 
   @Test
@@ -1269,6 +1281,59 @@
     assertThat(accountQueryProvider.get().byDefault(name)).hasSize(2);
   }
 
+  @Test
+  @GerritConfig(name = "user.readAccountsFromGit", value = "true")
+  public void checkMetaId() throws Exception {
+    // metaId is set when account is loaded
+    assertThat(accounts.get(db, admin.getId()).getMetaId()).isEqualTo(getMetaId(admin.getId()));
+
+    // metaId is set when account is created
+    AccountsUpdate au = accountsUpdate.create();
+    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account account = au.insert(db, accountId, a -> {});
+    assertThat(account.getMetaId()).isEqualTo(getMetaId(accountId));
+
+    // metaId is set when account is updated
+    Account updatedAccount = au.update(db, accountId, a -> a.setFullName("foo"));
+    assertThat(account.getMetaId()).isNotEqualTo(updatedAccount.getMetaId());
+    assertThat(updatedAccount.getMetaId()).isEqualTo(getMetaId(accountId));
+
+    // metaId is set when account is replaced
+    Account newAccount = new Account(accountId, TimeUtil.nowTs());
+    au.replace(db, newAccount);
+    assertThat(updatedAccount.getMetaId()).isNotEqualTo(newAccount.getMetaId());
+    assertThat(newAccount.getMetaId()).isEqualTo(getMetaId(accountId));
+  }
+
+  private String getMetaId(Account.Id accountId) throws IOException {
+    try (Repository repo = repoManager.openRepository(allUsers);
+        RevWalk rw = new RevWalk(repo);
+        ObjectReader or = repo.newObjectReader()) {
+      Ref ref = repo.exactRef(RefNames.refsUsers(accountId));
+      return ref != null ? ref.getObjectId().name() : null;
+    }
+  }
+
+  @Test
+  public void groups() throws Exception {
+    assertGroups(
+        admin.username, ImmutableList.of("Anonymous Users", "Registered Users", "Administrators"));
+
+    //TODO: update when test user is fixed to be included in "Anonymous Users" and
+    //      "Registered Users" groups
+    assertGroups(user.username, ImmutableList.of());
+
+    String group = createGroup("group");
+    String newUser = createAccount("user1", group);
+    assertGroups(newUser, ImmutableList.of(group));
+  }
+
+  private void assertGroups(String user, List<String> expected) throws Exception {
+    List<String> actual =
+        gApi.accounts().id(user).getGroups().stream().map(g -> g.name).collect(toList());
+    assertThat(actual).containsExactlyElementsIn(expected);
+  }
+
   private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
     int seq = 1;
     for (SshKeyInfo key : sshKeys) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 4c209e2..69315a2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -1797,7 +1797,7 @@
 
     setApiUser(user);
     exception.expect(AuthException.class);
-    exception.expectMessage("delete reviewer not permitted");
+    exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).remove();
   }
 
@@ -2117,10 +2117,19 @@
   @Test
   public void submitted() throws Exception {
     PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
-    assertThat(gApi.changes().id(r.getChangeId()).info().submitted).isNull();
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
-    assertThat(gApi.changes().id(r.getChangeId()).info().submitted).isNotNull();
+    String id = r.getChangeId();
+
+    ChangeInfo c = gApi.changes().id(r.getChangeId()).info();
+    assertThat(c.submitted).isNull();
+    assertThat(c.submitter).isNull();
+
+    gApi.changes().id(id).current().review(ReviewInput.approve());
+    gApi.changes().id(id).current().submit();
+
+    c = gApi.changes().id(r.getChangeId()).info();
+    assertThat(c.submitted).isNotNull();
+    assertThat(c.submitter).isNotNull();
+    assertThat(c.submitter._accountId).isEqualTo(atrScope.get().getUser().getAccountId().get());
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 2cef672..1b5e544a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -64,6 +64,12 @@
   @Inject @ServerInitiated private Provider<GroupsUpdate> groupsUpdateProvider;
 
   @Test
+  public void systemGroupCanBeRetrievedFromIndex() throws Exception {
+    List<GroupInfo> groupInfos = gApi.groups().query("name:Administrators").get();
+    assertThat(groupInfos).isNotEmpty();
+  }
+
+  @Test
   public void addToNonExistingGroup_NotFound() throws Exception {
     exception.expect(ResourceNotFoundException.class);
     gApi.groups().id("non-existing").addMembers("admin");
@@ -681,12 +687,6 @@
     return groupCache.get(new AccountGroup.NameKey(name));
   }
 
-  private String createAccount(String name, String group) throws Exception {
-    name = name(name);
-    accountCreator.create(name, group);
-    return name;
-  }
-
   private void setCreatedOnToNull(AccountGroup.UUID groupUuid) throws Exception {
     groupsUpdateProvider.get().updateGroup(db, groupUuid, group -> group.setCreatedOn(null));
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 9e4cc67..44e5eb0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -62,6 +62,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.receive.ReceiveConstants;
@@ -83,8 +84,11 @@
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
@@ -183,6 +187,69 @@
   }
 
   @Test
+  public void pushInitialCommitForRefsMetaConfigBranch() throws Exception {
+    // delete refs/meta/config
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      RefUpdate u = repo.updateRef(RefNames.REFS_CONFIG);
+      u.setForceUpdate(true);
+      u.setExpectedOldObjectId(repo.resolve(RefNames.REFS_CONFIG));
+      assertThat(u.delete(rw)).isEqualTo(Result.FORCED);
+    }
+
+    RevCommit c =
+        testRepo
+            .commit()
+            .message("Initial commit")
+            .author(admin.getIdent())
+            .committer(admin.getIdent())
+            .insertChangeId()
+            .create();
+    String id = GitUtil.getChangeId(testRepo, c).get();
+    testRepo.reset(c);
+
+    String r = "refs/for/" + RefNames.REFS_CONFIG;
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    ChangeInfo change = gApi.changes().id(id).info();
+    assertThat(change.branch).isEqualTo(RefNames.REFS_CONFIG);
+    assertThat(change.status).isEqualTo(ChangeStatus.NEW);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.resolve(RefNames.REFS_CONFIG)).isNull();
+    }
+
+    gApi.changes().id(change.id).current().review(ReviewInput.approve());
+    gApi.changes().id(change.id).current().submit();
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.resolve(RefNames.REFS_CONFIG)).isEqualTo(c);
+    }
+  }
+
+  @Test
+  public void pushInitialCommitForNormalNonExistingBranchFails() throws Exception {
+    RevCommit c =
+        testRepo
+            .commit()
+            .message("Initial commit")
+            .author(admin.getIdent())
+            .committer(admin.getIdent())
+            .insertChangeId()
+            .create();
+    testRepo.reset(c);
+
+    String r = "refs/for/foo";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushRejected(pr, r, "branch foo not found");
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.resolve("foo")).isNull();
+    }
+  }
+
+  @Test
   public void output() throws Exception {
     String url = canonicalWebUrl.get();
     ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
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 d8b82e3..c89ad5e 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -25,7 +26,9 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.StreamSupport;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -141,6 +144,31 @@
     return pushChangeTo(repo, "refs/heads/" + branch, "some change", "");
   }
 
+  protected ObjectId pushChangesTo(TestRepository<?> repo, String branch, int numChanges)
+      throws Exception {
+    for (int i = 0; i < numChanges; i++) {
+      repo.branch("HEAD")
+          .commit()
+          .insertChangeId()
+          .message("Message " + i)
+          .add(name("file"), "content" + i)
+          .create();
+    }
+    String remoteBranch = "refs/heads/" + branch;
+    Iterable<PushResult> res =
+        repo.git()
+            .push()
+            .setRemote("origin")
+            .setRefSpecs(new RefSpec("HEAD:" + remoteBranch))
+            .call();
+    List<Status> status =
+        StreamSupport.stream(res.spliterator(), false)
+            .map(r -> r.getRemoteUpdate(remoteBranch).getStatus())
+            .collect(toList());
+    assertThat(status).containsExactly(Status.OK);
+    return Iterables.getLast(res).getRemoteUpdate(remoteBranch).getNewObjectId();
+  }
+
   protected void allowSubmoduleSubscription(
       String submodule, String subBranch, String superproject, String superBranch, boolean match)
       throws Exception {
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 02fdd09..a5b381a 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.GerritConfig;
@@ -499,4 +500,42 @@
 
     expectToHaveSubmoduleState(superRepo, "master", "nested/subscribed-to-project", subHEAD);
   }
+
+  @Test
+  @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "SUBJECT_ONLY")
+  @GerritConfig(name = "submodule.maxCommitMessages", value = "1")
+  public void submoduleSubjectCommitMessageCountLimit() throws Exception {
+    testSubmoduleSubjectCommitMessageAndExpectTruncation();
+  }
+
+  @Test
+  @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "SUBJECT_ONLY")
+  @GerritConfig(name = "submodule.maxCombinedCommitMessageSize", value = "175")
+  public void submoduleSubjectCommitMessageSizeLimit() throws Exception {
+    assume().that(isSubmitWholeTopicEnabled()).isFalse();
+    testSubmoduleSubjectCommitMessageAndExpectTruncation();
+  }
+
+  private void testSubmoduleSubjectCommitMessageAndExpectTruncation() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowMatchingSubmoduleSubscription(
+        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    // The first update doesn't include the rev log, so we ignore it
+    pushChangeTo(subRepo, "master");
+
+    // Next, we push two commits at once. Since maxCommitMessages=1, we expect to see only the first
+    // message plus ellipsis to mark truncation.
+    ObjectId subHEAD = pushChangesTo(subRepo, "master", 2);
+    RevCommit subCommitMsg = subRepo.getRevWalk().parseCommit(subHEAD);
+    expectToHaveCommitMessage(
+        superRepo,
+        "master",
+        String.format(
+            "Update git submodules\n\n* Update %s from branch 'master'\n  - %s\n\n[...]",
+            name("subscribed-to-project"), subCommitMsg.getShortMessage()));
+  }
 }
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 43d02a6..c0f31a0 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
@@ -401,6 +401,7 @@
     assertThat(out.workInProgress).isEqualTo(in.workInProgress);
     assertThat(out.revisions).hasSize(1);
     assertThat(out.submitted).isNull();
+    assertThat(out.submitter).isNull();
     Boolean draft = Iterables.getOnlyElement(out.revisions.values()).draft;
     assertThat(booleanToDraftStatus(draft)).isEqualTo(in.status);
     return out;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
index b880152..a10062c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
@@ -17,23 +17,39 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.reviewdb.client.Project;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.Before;
 import org.junit.Test;
 
 public class PrivateByDefaultIT extends AbstractDaemonTest {
+  private Project.NameKey project1;
+  private Project.NameKey project2;
+
+  @Before
+  public void setUp() throws Exception {
+    project1 = createProject("project-1");
+    project2 = createProject("project-2", project1);
+    setPrivateByDefault(project1, InheritableBoolean.FALSE);
+  }
+
   @Test
-  @GerritConfig(name = "change.privateByDefault", value = "true")
   public void createChangeWithPrivateByDefaultEnabled() throws Exception {
-    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
+    setPrivateByDefault(project2, InheritableBoolean.TRUE);
+    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
     assertThat(gApi.changes().create(input).get().isPrivate).isEqualTo(true);
   }
 
   @Test
-  @GerritConfig(name = "change.privateByDefault", value = "true")
   public void createChangeBypassPrivateByDefaultEnabled() throws Exception {
-    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
+    setPrivateByDefault(project2, InheritableBoolean.TRUE);
+    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
     input.isPrivate = false;
     assertThat(gApi.changes().create(input).get().isPrivate).isNull();
   }
@@ -41,25 +57,62 @@
   @Test
   public void createChangeWithPrivateByDefaultDisabled() throws Exception {
     ChangeInfo info =
-        gApi.changes().create(new ChangeInput(project.get(), "master", "empty change")).get();
+        gApi.changes().create(new ChangeInput(project2.get(), "master", "empty change")).get();
     assertThat(info.isPrivate).isNull();
   }
 
   @Test
-  @GerritConfig(name = "change.privateByDefault", value = "true")
-  public void pushWithPrivateByDefaultEnabled() throws Exception {
-    assertThat(createChange().getChange().change().isPrivate()).isEqualTo(true);
+  public void createChangeWithPrivateByDefaultInherited() throws Exception {
+    setPrivateByDefault(project1, InheritableBoolean.TRUE);
+    ChangeInfo info =
+        gApi.changes().create(new ChangeInput(project2.get(), "master", "empty change")).get();
+    assertThat(info.isPrivate).isTrue();
   }
 
   @Test
-  @GerritConfig(name = "change.privateByDefault", value = "true")
+  public void pushWithPrivateByDefaultEnabled() throws Exception {
+    setPrivateByDefault(project2, InheritableBoolean.TRUE);
+    assertThat(createChange(project2).getChange().change().isPrivate()).isEqualTo(true);
+  }
+
+  @Test
   public void pushBypassPrivateByDefaultEnabled() throws Exception {
-    assertThat(createChange("refs/for/master%remove-private").getChange().change().isPrivate())
+    setPrivateByDefault(project2, InheritableBoolean.TRUE);
+    assertThat(
+            createChange(project2, "refs/for/master%remove-private")
+                .getChange()
+                .change()
+                .isPrivate())
         .isEqualTo(false);
   }
 
   @Test
   public void pushWithPrivateByDefaultDisabled() throws Exception {
-    assertThat(createChange().getChange().change().isPrivate()).isEqualTo(false);
+    assertThat(createChange(project2).getChange().change().isPrivate()).isEqualTo(false);
+  }
+
+  @Test
+  public void pushBypassPrivateByDefaultInherited() throws Exception {
+    setPrivateByDefault(project1, InheritableBoolean.TRUE);
+    assertThat(createChange(project2).getChange().change().isPrivate()).isEqualTo(true);
+  }
+
+  private void setPrivateByDefault(Project.NameKey proj, InheritableBoolean value)
+      throws Exception {
+    ConfigInput input = new ConfigInput();
+    input.privateByDefault = value;
+    gApi.projects().name(proj.get()).config(input);
+  }
+
+  private PushOneCommit.Result createChange(Project.NameKey proj) throws Exception {
+    return createChange(proj, "refs/for/master");
+  }
+
+  private PushOneCommit.Result createChange(Project.NameKey proj, String ref) throws Exception {
+    TestRepository<InMemoryRepository> testRepo = cloneProject(proj);
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result result = push.to(ref);
+    result.assertOkStatus();
+    return result;
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 33ff5a4..e5816dd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -25,19 +25,15 @@
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.group.CreateGroup;
-import com.google.gerrit.server.group.GroupsCollection;
 import com.google.inject.Inject;
 import java.util.Arrays;
 import java.util.List;
@@ -48,8 +44,6 @@
 public class SuggestReviewersIT extends AbstractDaemonTest {
   @Inject private CreateGroup.Factory createGroupFactory;
 
-  @Inject private GroupsCollection groups;
-
   private AccountGroup group1;
   private AccountGroup group2;
   private AccountGroup group3;
@@ -432,8 +426,7 @@
 
   private AccountGroup group(String name) throws Exception {
     GroupInfo group = createGroupFactory.create(name(name)).apply(TopLevelResource.INSTANCE, null);
-    GroupDescription.Basic d = groups.parseInternal(Url.decode(group.id));
-    return GroupDescriptions.toAccountGroup(d);
+    return groupCache.get(new AccountGroup.UUID(group.id));
   }
 
   private TestAccount user(String name, String fullName, String emailName, AccountGroup... groups)
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
index 62a8544..ef3cfd0 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import java.sql.Timestamp;
 
 /** Group methods exposed by the GroupBackend. */
 public class GroupDescription {
@@ -42,10 +43,18 @@
     String getUrl();
   }
 
-  /** The extended information exposed by internal groups backed by an AccountGroup. */
+  /** The extended information exposed by internal groups. */
   public interface Internal extends Basic {
-    /** @return the backing AccountGroup. */
-    AccountGroup getAccountGroup();
+
+    AccountGroup.Id getId();
+
+    String getDescription();
+
+    AccountGroup.UUID getOwnerGroupUUID();
+
+    boolean isVisibleToAll();
+
+    Timestamp getCreatedOn();
   }
 
   private GroupDescription() {}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
index 0c06868..b7a06c5 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
@@ -17,18 +17,11 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import java.sql.Timestamp;
 
 /** Utility class for building GroupDescription objects. */
 public class GroupDescriptions {
 
-  @Nullable
-  public static AccountGroup toAccountGroup(GroupDescription.Basic group) {
-    if (group instanceof GroupDescription.Internal) {
-      return ((GroupDescription.Internal) group).getAccountGroup();
-    }
-    return null;
-  }
-
   public static GroupDescription.Internal forAccountGroup(AccountGroup group) {
     return new GroupDescription.Internal() {
       @Override
@@ -42,21 +35,40 @@
       }
 
       @Override
-      public AccountGroup getAccountGroup() {
-        return group;
-      }
-
-      @Override
       @Nullable
       public String getEmailAddress() {
         return null;
       }
 
       @Override
-      @Nullable
       public String getUrl() {
         return "#" + PageLinks.toGroup(getGroupUUID());
       }
+
+      @Override
+      public AccountGroup.Id getId() {
+        return group.getId();
+      }
+
+      @Override
+      public String getDescription() {
+        return group.getDescription();
+      }
+
+      @Override
+      public AccountGroup.UUID getOwnerGroupUUID() {
+        return group.getOwnerGroupUUID();
+      }
+
+      @Override
+      public boolean isVisibleToAll() {
+        return group.isVisibleToAll();
+      }
+
+      @Override
+      public Timestamp getCreatedOn() {
+        return group.getCreatedOn();
+      }
     };
   }
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java
index 5de0aad..2b5bf1b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java
@@ -46,8 +46,7 @@
     url = a.getUrl();
 
     if (a instanceof GroupDescription.Internal) {
-      AccountGroup group = ((GroupDescription.Internal) a).getAccountGroup();
-      description = group.getDescription();
+      description = ((GroupDescription.Internal) a).getDescription();
     }
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index b88097c..912ad64 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.EmailInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.SshKeyInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -69,6 +70,8 @@
 
   List<ChangeInfo> getStarredChanges() throws RestApiException;
 
+  List<GroupInfo> getGroups() throws RestApiException;
+
   List<EmailInfo> getEmails() throws RestApiException;
 
   void addEmail(EmailInput input) throws RestApiException;
@@ -197,6 +200,11 @@
     }
 
     @Override
+    public List<GroupInfo> getGroups() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public List<EmailInfo> getEmails() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
index 23c1f8e..443e2064 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -31,6 +31,7 @@
   public InheritedBooleanInfo enableSignedPush;
   public InheritedBooleanInfo requireSignedPush;
   public InheritedBooleanInfo rejectImplicitMerges;
+  public InheritedBooleanInfo privateByDefault;
   public InheritedBooleanInfo enableReviewerByEmail;
   public InheritedBooleanInfo matchAuthorToCommitterDate;
   public MaxObjectSizeLimitInfo maxObjectSizeLimit;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
index 65e056b..0c1cec4 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
@@ -29,6 +29,7 @@
   public InheritableBoolean enableSignedPush;
   public InheritableBoolean requireSignedPush;
   public InheritableBoolean rejectImplicitMerges;
+  public InheritableBoolean privateByDefault;
   public InheritableBoolean enableReviewerByEmail;
   public InheritableBoolean matchAuthorToCommitterDate;
   public String maxObjectSizeLimit;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java
index 3114cb9..e5bc194 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java
@@ -15,7 +15,23 @@
 package com.google.gerrit.extensions.client;
 
 public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
+  ACTIVE(true, true),
+  READ_ONLY(true, false),
+  HIDDEN(false, false);
+
+  private final boolean permitsRead;
+  private final boolean permitsWrite;
+
+  ProjectState(boolean permitsRead, boolean permitsWrite) {
+    this.permitsRead = permitsRead;
+    this.permitsWrite = permitsWrite;
+  }
+
+  public boolean permitsRead() {
+    return permitsRead;
+  }
+
+  public boolean permitsWrite() {
+    return permitsWrite;
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index a4f85e5..0ae45dd5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -35,6 +35,7 @@
   public Timestamp created;
   public Timestamp updated;
   public Timestamp submitted;
+  public AccountInfo submitter;
   public Boolean starred;
   public Boolean muted;
   public Collection<String> stars;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index 9149230..8db0ef7 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -134,6 +134,8 @@
 
   private native String submittedRaw() /*-{ return this.submitted; }-*/;
 
+  public final native AccountInfo submitter() /*-{ return this.submitter; }-*/;
+
   public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
 
   public final native boolean muted() /*-{ return this.muted ? true : false; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 8f5f415..2629cec 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -75,6 +75,8 @@
 
   String rejectImplicitMerges();
 
+  String privateByDefault();
+
   String enableReviewerByEmail();
 
   String matchAuthorToCommitterDate();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 60cadfb..54f5c8b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -29,6 +29,7 @@
 requireSignedPush = Require signed push
 requireChangeID = Require <code>Change-Id</code> in commit message
 rejectImplicitMerges = Reject implicit merges when changes are pushed for review
+privateByDefault = Set all new changes private by default
 headingMaxObjectSizeLimit = Maximum Git object size limit
 headingGroupOptions = Group Options
 isVisibleToAll = Make group visible to all registered users.
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 0ef8893..432d3a9 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
@@ -86,6 +86,7 @@
   private ListBox enableSignedPush;
   private ListBox requireSignedPush;
   private ListBox rejectImplicitMerges;
+  private ListBox privateByDefault;
   private ListBox enableReviewerByEmail;
   private ListBox matchAuthorToCommitterDate;
   private NpTextBox maxObjectSizeLimit;
@@ -192,6 +193,7 @@
     signedOffBy.setEnabled(isOwner);
     requireChangeID.setEnabled(isOwner);
     rejectImplicitMerges.setEnabled(isOwner);
+    privateByDefault.setEnabled(isOwner);
     maxObjectSizeLimit.setEnabled(isOwner);
     enableReviewerByEmail.setEnabled(isOwner);
     matchAuthorToCommitterDate.setEnabled(isOwner);
@@ -268,6 +270,10 @@
     saveEnabler.listenTo(rejectImplicitMerges);
     grid.addHtml(AdminConstants.I.rejectImplicitMerges(), rejectImplicitMerges);
 
+    privateByDefault = newInheritedBooleanBox();
+    saveEnabler.listenTo(privateByDefault);
+    grid.addHtml(AdminConstants.I.privateByDefault(), privateByDefault);
+
     enableReviewerByEmail = newInheritedBooleanBox();
     saveEnabler.listenTo(enableReviewerByEmail);
     grid.addHtml(AdminConstants.I.enableReviewerByEmail(), enableReviewerByEmail);
@@ -407,6 +413,7 @@
       setBool(requireSignedPush, result.requireSignedPush());
     }
     setBool(rejectImplicitMerges, result.rejectImplicitMerges());
+    setBool(privateByDefault, result.privateByDefault());
     setBool(enableReviewerByEmail, result.enableReviewerByEmail());
     setBool(matchAuthorToCommitterDate, result.matchAuthorToCommitterDate());
     setSubmitType(result.submitType());
@@ -679,6 +686,7 @@
         esp,
         rsp,
         getBool(rejectImplicitMerges),
+        getBool(privateByDefault),
         getBool(enableReviewerByEmail),
         getBool(matchAuthorToCommitterDate),
         maxObjectSizeLimit.getText().trim(),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index c76ec84..b8effdf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -57,6 +57,9 @@
   public final native InheritedBooleanInfo rejectImplicitMerges()
       /*-{ return this.reject_implicit_merges; }-*/ ;
 
+  public final native InheritedBooleanInfo privateByDefault()
+      /*-{ return this.private_by_default; }-*/ ;
+
   public final native InheritedBooleanInfo enableReviewerByEmail()
       /*-{ return this.enable_reviewer_by_email; }-*/ ;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index 73cc995..acee478 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -147,6 +147,7 @@
       InheritableBoolean enableSignedPush,
       InheritableBoolean requireSignedPush,
       InheritableBoolean rejectImplicitMerges,
+      InheritableBoolean privateByDefault,
       InheritableBoolean enableReviewerByEmail,
       InheritableBoolean matchAuthorToCommitterDate,
       String maxObjectSizeLimit,
@@ -168,6 +169,7 @@
       in.setRequireSignedPush(requireSignedPush);
     }
     in.setRejectImplicitMerges(rejectImplicitMerges);
+    in.setPrivateByDefault(privateByDefault);
     in.setMaxObjectSizeLimit(maxObjectSizeLimit);
     in.setSubmitType(submitType);
     in.setState(state);
@@ -298,6 +300,12 @@
       setRequireSignedPushRaw(v.name());
     }
 
+    final void setPrivateByDefault(InheritableBoolean v) {
+      setPrivateByDefault(v.name());
+    }
+
+    private native void setPrivateByDefault(String v) /*-{ if(v)this.private_by_default=v; }-*/;
+
     final void setEnableReviewerByEmail(InheritableBoolean v) {
       setEnableReviewerByEmailRaw(v.name());
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 4f7a5ba..01aec6e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -223,8 +223,6 @@
               .get()
               .byPreferredEmail(email)
               .stream()
-              // the index query also matches prefixes, filter those out
-              .filter(a -> email.equalsIgnoreCase(a.getAccount().getPreferredEmail()))
               .map(AccountState::getAccount)
               .findFirst();
       return match.isPresent() ? auth(match.get()) : null;
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 a4eea96..1a263f3 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
@@ -24,7 +24,10 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
-import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -50,22 +53,25 @@
 public class CatServlet extends HttpServlet {
   private final Provider<ReviewDb> requestDb;
   private final Provider<CurrentUser> userProvider;
-  private final ChangeControl.GenericFactory changeControl;
   private final ChangeEditUtil changeEditUtil;
   private final PatchSetUtil psUtil;
+  private final ChangeNotes.Factory changeNotesFactory;
+  private final PermissionBackend permissionBackend;
 
   @Inject
   CatServlet(
       Provider<ReviewDb> sf,
-      ChangeControl.GenericFactory ccf,
       Provider<CurrentUser> usrprv,
       ChangeEditUtil ceu,
-      PatchSetUtil psu) {
+      PatchSetUtil psu,
+      ChangeNotes.Factory cnf,
+      PermissionBackend pb) {
     requestDb = sf;
-    changeControl = ccf;
     userProvider = usrprv;
     changeEditUtil = ceu;
     psUtil = psu;
+    changeNotesFactory = cnf;
+    permissionBackend = pb;
   }
 
   @Override
@@ -119,34 +125,33 @@
     final Change.Id changeId = patchKey.getParentKey().getParentKey();
     String revision;
     try {
-      final ReviewDb db = requestDb.get();
-      final ChangeControl control = changeControl.validateFor(db, changeId, userProvider.get());
+      ChangeNotes notes = changeNotesFactory.createChecked(changeId);
+      permissionBackend
+          .user(userProvider)
+          .change(notes)
+          .database(requestDb)
+          .check(ChangePermission.READ);
       if (patchKey.getParentKey().get() == 0) {
         // change edit
-        try {
-          Optional<ChangeEdit> edit = changeEditUtil.byChange(control.getChange());
-          if (edit.isPresent()) {
-            revision = ObjectId.toString(edit.get().getEditCommit());
-          } else {
-            rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
-            return;
-          }
-        } catch (AuthException e) {
+        Optional<ChangeEdit> edit = changeEditUtil.byChange(notes.getChange());
+        if (edit.isPresent()) {
+          revision = ObjectId.toString(edit.get().getEditCommit());
+        } else {
           rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
           return;
         }
       } else {
-        PatchSet patchSet = psUtil.get(db, control.getNotes(), patchKey.getParentKey());
+        PatchSet patchSet = psUtil.get(requestDb.get(), notes, patchKey.getParentKey());
         if (patchSet == null) {
           rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
           return;
         }
         revision = patchSet.getRevision().get();
       }
-    } catch (NoSuchChangeException e) {
+    } catch (NoSuchChangeException | AuthException e) {
       rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
       return;
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       getServletContext().log("Cannot query database", e);
       rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
       return;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java
index 5fd5a08..5aa2ae4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
 import com.google.inject.Injector;
@@ -47,15 +48,17 @@
 
   @Override
   public int run() throws Exception {
+    Injector dbInjector = createDbInjector(DataSourceProvider.Context.SINGLE_USER);
     SitePaths sitePaths = new SitePaths(getSitePath());
+    ThreadSettingsConfig threadSettingsConfig = dbInjector.getInstance(ThreadSettingsConfig.class);
     Config fakeCfg = new Config();
     if (!Strings.isNullOrEmpty(sourceUrl)) {
       fakeCfg.setString("accountPatchReviewDb", null, "url", sourceUrl);
     }
     JdbcAccountPatchReviewStore sourceJdbcAccountPatchReviewStore =
-        JdbcAccountPatchReviewStore.createAccountPatchReviewStore(fakeCfg, sitePaths);
+        JdbcAccountPatchReviewStore.createAccountPatchReviewStore(
+            fakeCfg, sitePaths, threadSettingsConfig);
 
-    Injector dbInjector = createDbInjector(DataSourceProvider.Context.SINGLE_USER);
     Config cfg = dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
     String targetUrl = cfg.getString("accountPatchReviewDb", null, "url");
     if (targetUrl == null) {
@@ -64,7 +67,8 @@
     }
     System.out.println("target Url: " + targetUrl);
     JdbcAccountPatchReviewStore targetJdbcAccountPatchReviewStore =
-        JdbcAccountPatchReviewStore.createAccountPatchReviewStore(cfg, sitePaths);
+        JdbcAccountPatchReviewStore.createAccountPatchReviewStore(
+            cfg, sitePaths, threadSettingsConfig);
     targetJdbcAccountPatchReviewStore.createTableIfNotExists();
 
     if (!isTargetTableEmpty(targetJdbcAccountPatchReviewStore)) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
index eb71e58..d81d58c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
@@ -111,6 +111,7 @@
           throw new IOException(
               String.format("Failed to update ref %s: %s", refName, result.name()));
         }
+        account.setMetaId(id.name());
       }
     }
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
index e6d7e58..e4a1cd5 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -19,6 +19,7 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
@@ -29,6 +30,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gwtorm.server.SchemaFactory;
@@ -44,8 +46,9 @@
 import org.apache.commons.validator.routines.EmailValidator;
 
 public class InitAdminUser implements InitStep {
-  private final ConsoleUI ui;
   private final InitFlags flags;
+  private final ConsoleUI ui;
+  private final AllUsersNameOnInitProvider allUsers;
   private final AccountsOnInit accounts;
   private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
   private final ExternalIdsOnInit externalIds;
@@ -58,6 +61,7 @@
   InitAdminUser(
       InitFlags flags,
       ConsoleUI ui,
+      AllUsersNameOnInitProvider allUsers,
       AccountsOnInit accounts,
       VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory,
       ExternalIdsOnInit externalIds,
@@ -65,6 +69,7 @@
       GroupsOnInit groupsOnInit) {
     this.flags = flags;
     this.ui = ui;
+    this.allUsers = allUsers;
     this.accounts = accounts;
     this.authorizedKeysFactory = authorizedKeysFactory;
     this.externalIds = externalIds;
@@ -128,7 +133,11 @@
 
           AccountState as =
               new AccountState(
-                  a, Collections.singleton(adminGroup.getGroupUUID()), extIds, new HashMap<>());
+                  new AllUsersName(allUsers.get()),
+                  a,
+                  Collections.singleton(adminGroup.getGroupUUID()),
+                  extIds,
+                  new HashMap<>());
           for (AccountIndex accountIndex : indexCollection.getWriteIndexes()) {
             accountIndex.replace(as);
           }
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
index bc8a523..268a32a 100644
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
@@ -13,15 +13,15 @@
 # limitations under the License.
 
 [library "mysqlDriver"]
-  name = MySQL Connector/J 5.1.42
-  url = https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.42/mysql-connector-java-5.1.42.jar
-  sha1 = 80a448a3ec2178b649bb2e3cb3610fab06e11669
+  name = MySQL Connector/J 5.1.43
+  url = https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.43/mysql-connector-java-5.1.43.jar
+  sha1 = dee9103eec0d877f3a21c82d4d9e9f4fbd2d6e0a
   remove = mysql-connector-java-.*[.]jar
 
 [library "mariadbDriver"]
-  name = MariaDB Connector/J 2.0.2
-  url = https://repo1.maven.org/maven2/org/mariadb/jdbc/mariadb-java-client/2.0.2/mariadb-java-client-2.0.2.jar
-  sha1 = 36ab24223b0e915e6d81c98856d73c47628a31c1
+  name = MariaDB Connector/J 2.1.0
+  url = https://repo1.maven.org/maven2/org/mariadb/jdbc/mariadb-java-client/2.1.0/mariadb-java-client-2.1.0.jar
+  sha1 = 639be502c0d191e1cc21e4e86d388486358fddf8
   remove = mariadb-java-client-.*[.]jar
 
 [library "oracleDriver"]
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD
index 51b1486..fe9ce19 100644
--- a/gerrit-plugin-api/BUILD
+++ b/gerrit-plugin-api/BUILD
@@ -1,9 +1,3 @@
-SRCS = [
-    "gerrit-server/src/main/java/",
-    "gerrit-httpd/src/main/java/",
-    "gerrit-sshd/src/main/java/",
-]
-
 PLUGIN_API = [
     "//gerrit-httpd:httpd",
     "//gerrit-pgm:init-api",
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
deleted file mode 100644
index f9dc7e4..0000000
--- a/gerrit-plugin-api/pom.xml
+++ /dev/null
@@ -1,86 +0,0 @@
-<project>
-  <modelVersion>4.0.0</modelVersion>
-  <groupId>com.google.gerrit</groupId>
-  <artifactId>gerrit-plugin-api</artifactId>
-  <version>2.15-SNAPSHOT</version>
-  <packaging>jar</packaging>
-  <name>Gerrit Code Review - Plugin API</name>
-  <description>API for Gerrit Plugins</description>
-  <url>https://www.gerritcodereview.com/</url>
-
-  <licenses>
-    <license>
-      <name>The Apache Software License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>
-    </license>
-  </licenses>
-
-  <scm>
-    <url>https://gerrit.googlesource.com/gerrit</url>
-    <connection>https://gerrit.googlesource.com/gerrit</connection>
-  </scm>
-
-  <developers>
-    <developer>
-      <name>Alice Kober-Sotzek</name>
-    </developer>
-    <developer>
-      <name>Andrew Bonventre</name>
-    </developer>
-    <developer>
-      <name>Becky Siegel</name>
-    </developer>
-    <developer>
-      <name>Dave Borowitz</name>
-    </developer>
-    <developer>
-      <name>David Ostrovsky</name>
-    </developer>
-    <developer>
-      <name>David Pursehouse</name>
-    </developer>
-    <developer>
-      <name>Edwin Kempin</name>
-    </developer>
-    <developer>
-      <name>Hugo Arès</name>
-    </developer>
-    <developer>
-      <name>Kasper Nilsson</name>
-    </developer>
-    <developer>
-      <name>Logan Hanks</name>
-    </developer>
-    <developer>
-      <name>Martin Fick</name>
-    </developer>
-    <developer>
-      <name>Saša Živkov</name>
-    </developer>
-    <developer>
-      <name>Shawn Pearce</name>
-    </developer>
-    <developer>
-      <name>Viktar Donich</name>
-    </developer>
-    <developer>
-      <name>Wyatt Allen</name>
-    </developer>
-  </developers>
-
-  <mailingLists>
-    <mailingList>
-      <name>Repo and Gerrit Discussion</name>
-      <post>repo-discuss@googlegroups.com</post>
-      <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe>
-      <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe>
-      <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive>
-    </mailingList>
-  </mailingLists>
-
-  <issueManagement>
-    <url>https://bugs.chromium.org/p/gerrit/issues/list</url>
-    <system>Gerrit Issue Tracker</system>
-  </issueManagement>
-</project>
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
deleted file mode 100644
index daabb46..0000000
--- a/gerrit-plugin-gwtui/pom.xml
+++ /dev/null
@@ -1,86 +0,0 @@
-<project>
-  <modelVersion>4.0.0</modelVersion>
-  <groupId>com.google.gerrit</groupId>
-  <artifactId>gerrit-plugin-gwtui</artifactId>
-  <version>2.15-SNAPSHOT</version>
-  <packaging>jar</packaging>
-  <name>Gerrit Code Review - Plugin GWT UI</name>
-  <description>Common Classes for Gerrit GWT UI Plugins</description>
-  <url>https://www.gerritcodereview.com/</url>
-
-  <licenses>
-    <license>
-      <name>The Apache Software License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>
-    </license>
-  </licenses>
-
-  <scm>
-    <url>https://gerrit.googlesource.com/gerrit</url>
-    <connection>https://gerrit.googlesource.com/gerrit</connection>
-  </scm>
-
-  <developers>
-    <developer>
-      <name>Alice Kober-Sotzek</name>
-    </developer>
-    <developer>
-      <name>Andrew Bonventre</name>
-    </developer>
-    <developer>
-      <name>Becky Siegel</name>
-    </developer>
-    <developer>
-      <name>Dave Borowitz</name>
-    </developer>
-    <developer>
-      <name>David Ostrovsky</name>
-    </developer>
-    <developer>
-      <name>David Pursehouse</name>
-    </developer>
-    <developer>
-      <name>Edwin Kempin</name>
-    </developer>
-    <developer>
-      <name>Hugo Arès</name>
-    </developer>
-    <developer>
-      <name>Kasper Nilsson</name>
-    </developer>
-    <developer>
-      <name>Logan Hanks</name>
-    </developer>
-    <developer>
-      <name>Martin Fick</name>
-    </developer>
-    <developer>
-      <name>Saša Živkov</name>
-    </developer>
-    <developer>
-      <name>Shawn Pearce</name>
-    </developer>
-    <developer>
-      <name>Viktar Donich</name>
-    </developer>
-    <developer>
-      <name>Wyatt Allen</name>
-    </developer>
-  </developers>
-
-  <mailingLists>
-    <mailingList>
-      <name>Repo and Gerrit Discussion</name>
-      <post>repo-discuss@googlegroups.com</post>
-      <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe>
-      <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe>
-      <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive>
-    </mailingList>
-  </mailingLists>
-
-  <issueManagement>
-    <url>https://bugs.chromium.org/p/gerrit/issues/list</url>
-    <system>Gerrit Issue Tracker</system>
-  </issueManagement>
-</project>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
index ad94f13..b7506e5 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
@@ -183,6 +183,12 @@
   /** <i>stored in git, used for caching</i> the user's preferences. */
   private GeneralPreferencesInfo generalPreferences;
 
+  /**
+   * ID of the user branch from which the account was read, {@code null} if the account was read
+   * from ReviewDb.
+   */
+  private String metaId;
+
   protected Account() {}
 
   /**
@@ -282,6 +288,14 @@
     generalPreferences = p;
   }
 
+  public String getMetaId() {
+    return metaId;
+  }
+
+  public void setMetaId(String metaId) {
+    this.metaId = metaId;
+  }
+
   public boolean isActive() {
     return !inactive;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index 96d037f..e756ce5 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -102,6 +102,7 @@
   protected InheritableBoolean requireSignedPush;
 
   protected InheritableBoolean rejectImplicitMerges;
+  protected InheritableBoolean privateByDefault;
 
   protected InheritableBoolean enableReviewerByEmail;
 
@@ -120,6 +121,7 @@
     createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT;
     enableSignedPush = InheritableBoolean.INHERIT;
     requireSignedPush = InheritableBoolean.INHERIT;
+    privateByDefault = InheritableBoolean.INHERIT;
     enableReviewerByEmail = InheritableBoolean.INHERIT;
     matchAuthorToCommitterDate = InheritableBoolean.INHERIT;
   }
@@ -164,6 +166,14 @@
     return rejectImplicitMerges;
   }
 
+  public InheritableBoolean getPrivateByDefault() {
+    return privateByDefault;
+  }
+
+  public void setPrivateByDefault(InheritableBoolean privateByDefault) {
+    this.privateByDefault = privateByDefault;
+  }
+
   public InheritableBoolean getEnableReviewerByEmail() {
     return enableReviewerByEmail;
   }
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java
index c86d804..7044547 100644
--- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java
@@ -37,6 +37,17 @@
   }
 
   @Test
+  public void changeRefs() throws Exception {
+    String changeMetaRef = RefNames.changeMetaRef(changeId);
+    assertThat(changeMetaRef).isEqualTo("refs/changes/73/67473/meta");
+    assertThat(RefNames.isNoteDbMetaRef(changeMetaRef)).isTrue();
+
+    String robotCommentsRef = RefNames.robotCommentsRef(changeId);
+    assertThat(robotCommentsRef).isEqualTo("refs/changes/73/67473/robot-comments");
+    assertThat(RefNames.isNoteDbMetaRef(robotCommentsRef)).isTrue();
+  }
+
+  @Test
   public void refsUsers() throws Exception {
     assertThat(RefNames.refsUsers(accountId)).isEqualTo("refs/users/23/1011123");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
index 7688f1d..1a327db 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import java.io.IOException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -37,7 +38,8 @@
     try (Repository repo = repoManager.openRepository(branch.getParentKey())) {
       boolean exists = repo.getRefDatabase().exactRef(branch.get()) != null;
       if (!exists) {
-        exists = repo.getFullBranch().equals(branch.get());
+        exists =
+            repo.getFullBranch().equals(branch.get()) || RefNames.REFS_CONFIG.equals(branch.get());
       }
       return exists;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 6cb4444..16901ed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.group.Groups;
 import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
@@ -76,15 +77,18 @@
     };
   }
 
+  private final AllUsersName allUsersName;
   private final LoadingCache<Account.Id, Optional<AccountState>> byId;
   private final LoadingCache<String, Optional<Account.Id>> byName;
   private final Provider<AccountIndexer> indexer;
 
   @Inject
   AccountCacheImpl(
+      AllUsersName allUsersName,
       @Named(BYID_NAME) LoadingCache<Account.Id, Optional<AccountState>> byId,
       @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername,
       Provider<AccountIndexer> indexer) {
+    this.allUsersName = allUsersName;
     this.byId = byId;
     this.byName = byUsername;
     this.indexer = indexer;
@@ -142,16 +146,21 @@
     }
   }
 
-  private static AccountState missing(Account.Id accountId) {
+  private AccountState missing(Account.Id accountId) {
     Account account = new Account(accountId, TimeUtil.nowTs());
     account.setActive(false);
     Set<AccountGroup.UUID> anon = ImmutableSet.of();
     return new AccountState(
-        account, anon, Collections.emptySet(), new HashMap<ProjectWatchKey, Set<NotifyType>>());
+        allUsersName,
+        account,
+        anon,
+        Collections.emptySet(),
+        new HashMap<ProjectWatchKey, Set<NotifyType>>());
   }
 
   static class ByIdLoader extends CacheLoader<Account.Id, Optional<AccountState>> {
     private final SchemaFactory<ReviewDb> schema;
+    private final AllUsersName allUsersName;
     private final Accounts accounts;
     private final GroupCache groupCache;
     private final Groups groups;
@@ -163,6 +172,7 @@
     @Inject
     ByIdLoader(
         SchemaFactory<ReviewDb> sf,
+        AllUsersName allUsersName,
         Accounts accounts,
         GroupCache groupCache,
         Groups groups,
@@ -170,8 +180,9 @@
         @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername,
         Provider<WatchConfig.Accessor> watchConfig,
         ExternalIds externalIds) {
-      this.accounts = accounts;
       this.schema = sf;
+      this.allUsersName = allUsersName;
+      this.accounts = accounts;
       this.groupCache = groupCache;
       this.groups = groups;
       this.loader = loader;
@@ -219,6 +230,7 @@
 
       return Optional.of(
           new AccountState(
+              allUsersName,
               account,
               internalGroups,
               externalIds.byAccount(who),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java
index cd477c0..f44aa0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.VersionedMetaData;
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
@@ -34,6 +35,7 @@
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevSort;
 
 /**
@@ -142,6 +144,7 @@
       Config cfg = readConfig(ACCOUNT_CONFIG);
 
       account = parse(cfg);
+      account.setMetaId(revision.name());
     }
 
     isLoaded = true;
@@ -165,6 +168,13 @@
   }
 
   @Override
+  public RevCommit commit(MetaDataUpdate update) throws IOException {
+    RevCommit c = super.commit(update);
+    account.setMetaId(c.name());
+    return c;
+  }
+
+  @Override
   protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
     checkLoaded();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index f247d86..7d91415 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -329,8 +329,7 @@
       // setting the given user name has failed, but the realm does not
       // allow the user to manually set a user name,
       // this means we would end with an account without user name
-      // (without 'username:<USERNAME>' entry in
-      // account_external_ids table),
+      // (without 'username:<USERNAME>' external ID),
       // such an account cannot be used for uploading changes,
       // this is why the best we can do here is to fail early and cleanup
       // the database
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
index 1eaf34f..dd523a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.account.WatchConfig.NotifyType;
 import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.config.AllUsersName;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Map;
@@ -43,6 +44,7 @@
   public static final Function<AccountState, Account.Id> ACCOUNT_ID_FUNCTION =
       a -> a.getAccount().getId();
 
+  private final AllUsersName allUsersName;
   private final Account account;
   private final Set<AccountGroup.UUID> internalGroups;
   private final Collection<ExternalId> externalIds;
@@ -50,10 +52,12 @@
   private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
 
   public AccountState(
+      AllUsersName allUsersName,
       Account account,
       Set<AccountGroup.UUID> actualGroups,
       Collection<ExternalId> externalIds,
       Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
+    this.allUsersName = allUsersName;
     this.account = account;
     this.internalGroups = actualGroups;
     this.externalIds = externalIds;
@@ -61,6 +65,10 @@
     this.account.setUserName(getUserName(externalIds));
   }
 
+  public AllUsersName getAllUsersNameForIndexing() {
+    return allUsersName;
+  }
+
   /** Get the cached account metadata. */
   public Account getAccount() {
     return account;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
index f9fdcfc..f6f7918 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -265,6 +265,7 @@
     if (account != null) {
       consumers.stream().forEach(c -> c.accept(account));
       commit(accountConfig);
+      return account;
     } else if (reviewDbAccount != null) {
       // user branch doesn't exist yet
       accountConfig.setAccount(reviewDbAccount);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index 1812ef4..70cbb6d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -18,7 +18,7 @@
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -211,8 +211,8 @@
     Set<AccountGroup.UUID> groupUuids = new HashSet<>();
     if (groups != null) {
       for (String g : groups) {
-        AccountGroup group = GroupDescriptions.toAccountGroup(groupsCollection.parseInternal(g));
-        groupUuids.add(group.getGroupUUID());
+        GroupDescription.Internal internalGroup = groupsCollection.parseInternal(g);
+        groupUuids.add(internalGroup.getGroupUUID());
       }
     }
     return groupUuids;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
index db707a8..3e97265 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
@@ -27,7 +27,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.List;
 
 /** Class to access accounts by email. */
 @Singleton
@@ -61,15 +60,9 @@
    * @see #getAccountsFor(String...)
    */
   public ImmutableSet<Account.Id> getAccountFor(String email) throws IOException, OrmException {
-    List<AccountState> byPreferredEmail = queryProvider.get().byPreferredEmail(email);
     return Streams.concat(
             externalIds.byEmail(email).stream().map(e -> e.accountId()),
-            byPreferredEmail
-                .stream()
-                // the index query also matches prefixes and emails with other case,
-                // filter those out
-                .filter(a -> email.equals(a.getAccount().getPreferredEmail()))
-                .map(a -> a.getAccount().getId()))
+            queryProvider.get().byPreferredEmail(email).stream().map(a -> a.getAccount().getId()))
         .collect(toImmutableSet());
   }
 
@@ -91,9 +84,6 @@
         .byPreferredEmail(emails)
         .entries()
         .stream()
-        // the index query also matches prefixes and emails with other case,
-        // filter those out
-        .filter(e -> e.getKey().equals(e.getValue().getAccount().getPreferredEmail()))
         .forEach(e -> builder.put(e.getKey(), e.getValue().getAccount().getId()));
     return builder.build();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index 56f8d63..5af4898 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -142,12 +142,15 @@
   }
 
   public boolean isOwner() {
-    AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
-    if (accountGroup == null) {
-      isOwner = false;
-    } else if (isOwner == null) {
-      AccountGroup.UUID ownerUUID = accountGroup.getOwnerGroupUUID();
+    if (isOwner != null) {
+      return isOwner;
+    }
+
+    if (group instanceof GroupDescription.Internal) {
+      AccountGroup.UUID ownerUUID = ((GroupDescription.Internal) group).getOwnerGroupUUID();
       isOwner = getUser().getEffectiveGroups().contains(ownerUUID) || canAdministrateServer();
+    } else {
+      isOwner = false;
     }
     return isOwner;
   }
@@ -189,7 +192,9 @@
   }
 
   private boolean canSeeMembers() {
-    AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
-    return (accountGroup != null && accountGroup.isVisibleToAll()) || isOwner();
+    if (group instanceof GroupDescription.Internal) {
+      return ((GroupDescription.Internal) group).isVisibleToAll() || isOwner();
+    }
+    return false;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
index a42362c..9764d80 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -85,6 +85,6 @@
   @Override
   public boolean isVisibleToAll(AccountGroup.UUID uuid) {
     GroupDescription.Internal g = get(uuid);
-    return g != null && g.getAccountGroup().isVisibleToAll();
+    return g != null && g.isVisibleToAll();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
index 687f1fe..1033641 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.account.externalids;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.inject.AbstractModule;
 import com.google.inject.Module;
 import java.io.IOException;
 import java.util.Collection;
-import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 
 public class DisabledExternalIdCache implements ExternalIdCache {
@@ -49,21 +49,6 @@
       Collection<ExternalId> toAdd) {}
 
   @Override
-  public void onReplaceByKeys(
-      ObjectId oldNotesRev,
-      ObjectId newNotesRev,
-      Account.Id accountId,
-      Collection<ExternalId.Key> toRemove,
-      Collection<ExternalId> toAdd) {}
-
-  @Override
-  public void onReplaceByKeys(
-      ObjectId oldNotesRev,
-      ObjectId newNotesRev,
-      Collection<ExternalId.Key> toRemove,
-      Collection<ExternalId> toAdd) {}
-
-  @Override
   public void onReplace(
       ObjectId oldNotesRev,
       ObjectId newNotesRev,
@@ -74,18 +59,7 @@
   public void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) {}
 
   @Override
-  public void onRemoveByKeys(
-      ObjectId oldNotesRev,
-      ObjectId newNotesRev,
-      Account.Id accountId,
-      Collection<ExternalId.Key> extIdKeys) {}
-
-  @Override
-  public void onRemoveByKeys(
-      ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId.Key> extIdKeys) {}
-
-  @Override
-  public Set<ExternalId> byAccount(Account.Id accountId) {
+  public ImmutableSet<ExternalId> byAccount(Account.Id accountId) {
     throw new UnsupportedOperationException();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 3d42ced..ad119ca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -15,9 +15,11 @@
 package com.google.gerrit.server.account.externalids;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.hash.Hashing;
@@ -26,9 +28,11 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.HashedPassword;
 import java.io.Serializable;
+import java.util.Objects;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 
 @AutoValue
@@ -122,7 +126,7 @@
   }
 
   public static ExternalId create(String scheme, String id, Account.Id accountId) {
-    return new AutoValue_ExternalId(Key.create(scheme, id), accountId, null, null);
+    return create(Key.create(scheme, id), accountId, null, null);
   }
 
   public static ExternalId create(
@@ -140,8 +144,8 @@
 
   public static ExternalId create(
       Key key, Account.Id accountId, @Nullable String email, @Nullable String hashedPassword) {
-    return new AutoValue_ExternalId(
-        key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword));
+    return create(
+        key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), null);
   }
 
   public static ExternalId createWithPassword(
@@ -162,13 +166,29 @@
   }
 
   public static ExternalId createWithEmail(Key key, Account.Id accountId, @Nullable String email) {
-    return new AutoValue_ExternalId(key, accountId, Strings.emptyToNull(email), null);
+    return create(key, accountId, Strings.emptyToNull(email), null);
   }
 
   public static ExternalId createEmail(Account.Id accountId, String email) {
     return createWithEmail(SCHEME_MAILTO, email, accountId, checkNotNull(email));
   }
 
+  static ExternalId create(ExternalId extId, @Nullable ObjectId blobId) {
+    return new AutoValue_ExternalId(
+        extId.key(), extId.accountId(), extId.email(), extId.password(), blobId);
+  }
+
+  @VisibleForTesting
+  public static ExternalId create(
+      Key key,
+      Account.Id accountId,
+      @Nullable String email,
+      @Nullable String hashedPassword,
+      @Nullable ObjectId blobId) {
+    return new AutoValue_ExternalId(
+        key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), blobId);
+  }
+
   /**
    * Parses an external ID from a byte array that contain the external ID as an Git config file
    * text.
@@ -183,7 +203,10 @@
    *   password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
    * </pre>
    */
-  public static ExternalId parse(String noteId, byte[] raw) throws ConfigInvalidException {
+  public static ExternalId parse(String noteId, byte[] raw, ObjectId blobId)
+      throws ConfigInvalidException {
+    checkNotNull(blobId);
+
     Config externalIdConfig = new Config();
     try {
       externalIdConfig.fromText(new String(raw, UTF_8));
@@ -218,11 +241,12 @@
         externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, PASSWORD_KEY);
     int accountId = readAccountId(noteId, externalIdConfig, externalIdKeyStr);
 
-    return new AutoValue_ExternalId(
+    return create(
         externalIdKey,
         new Account.Id(accountId),
         Strings.emptyToNull(email),
-        Strings.emptyToNull(password));
+        Strings.emptyToNull(password),
+        blobId);
   }
 
   private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr)
@@ -270,10 +294,51 @@
 
   public abstract @Nullable String password();
 
+  /**
+   * ID of the note blob in the external IDs branch that stores this external ID. {@code null} if
+   * the external ID was created in code and is not yet stored in Git.
+   */
+  public abstract @Nullable ObjectId blobId();
+
+  public void checkThatBlobIdIsSet() {
+    checkState(blobId() != null, "No blob ID set for external ID %s", key().get());
+  }
+
   public boolean isScheme(String scheme) {
     return key().isScheme(scheme);
   }
 
+  public byte[] toByteArray() {
+    checkState(blobId() != null, "Missing blobId in external ID %s", key().get());
+    byte[] b = new byte[2 * Constants.OBJECT_ID_STRING_LENGTH + 1];
+    key().sha1().copyTo(b, 0);
+    b[Constants.OBJECT_ID_STRING_LENGTH] = ':';
+    blobId().copyTo(b, Constants.OBJECT_ID_STRING_LENGTH + 1);
+    return b;
+  }
+
+  /**
+   * For checking if two external IDs are equals the blobId is excluded and external IDs that have
+   * different blob IDs but identical other fields are considered equal. This way an external ID
+   * that was loaded from Git can be equal with an external ID that was created from code.
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof ExternalId)) {
+      return false;
+    }
+    ExternalId o = (ExternalId) obj;
+    return Objects.equals(key(), o.key())
+        && Objects.equals(accountId(), o.accountId())
+        && Objects.equals(email(), o.email())
+        && Objects.equals(password(), o.password());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(key(), accountId(), email(), password());
+  }
+
   /**
    * Exports this external ID as Git config file text.
    *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
index b344448..d928e15 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
@@ -20,7 +20,6 @@
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 
 /**
@@ -44,21 +43,6 @@
       Collection<ExternalId> toAdd)
       throws IOException;
 
-  void onReplaceByKeys(
-      ObjectId oldNotesRev,
-      ObjectId newNotesRev,
-      Account.Id accountId,
-      Collection<ExternalId.Key> toRemove,
-      Collection<ExternalId> toAdd)
-      throws IOException;
-
-  void onReplaceByKeys(
-      ObjectId oldNotesRev,
-      ObjectId newNotesRev,
-      Collection<ExternalId.Key> toRemove,
-      Collection<ExternalId> toAdd)
-      throws IOException;
-
   void onReplace(
       ObjectId oldNotesRev,
       ObjectId newNotesRev,
@@ -69,18 +53,7 @@
   void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId)
       throws IOException;
 
-  void onRemoveByKeys(
-      ObjectId oldNotesRev,
-      ObjectId newNotesRev,
-      Account.Id accountId,
-      Collection<ExternalId.Key> extIdKeys)
-      throws IOException;
-
-  void onRemoveByKeys(
-      ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId.Key> extIdKeys)
-      throws IOException;
-
-  Set<ExternalId> byAccount(Account.Id accountId) throws IOException;
+  ImmutableSet<ExternalId> byAccount(Account.Id accountId) throws IOException;
 
   ImmutableSetMultimap<Account.Id, ExternalId> allByAccount() throws IOException;
 
@@ -102,12 +75,6 @@
     onRemove(oldNotesRev, newNotesRev, Collections.singleton(extId));
   }
 
-  default void onRemoveByKey(
-      ObjectId oldNotesRev, ObjectId newNotesRev, Account.Id accountId, ExternalId.Key extIdKey)
-      throws IOException {
-    onRemoveByKeys(oldNotesRev, newNotesRev, accountId, Collections.singleton(extIdKey));
-  }
-
   default void onUpdate(ObjectId oldNotesRev, ObjectId newNotesRev, ExternalId updatedExtId)
       throws IOException {
     onUpdate(oldNotesRev, newNotesRev, Collections.singleton(updatedExtId));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
index 756c360..311e70f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -23,6 +23,7 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
@@ -33,7 +34,6 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Collection;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
@@ -84,6 +84,7 @@
         newNotesRev,
         m -> {
           for (ExternalId extId : extIds) {
+            extId.checkThatBlobIdIsSet();
             m.put(extId.accountId(), extId);
           }
         });
@@ -103,23 +104,6 @@
   }
 
   @Override
-  public void onRemoveByKeys(
-      ObjectId oldNotesRev,
-      ObjectId newNotesRev,
-      Account.Id accountId,
-      Collection<ExternalId.Key> extIdKeys)
-      throws IOException {
-    updateCache(oldNotesRev, newNotesRev, m -> removeKeys(m.get(accountId), extIdKeys));
-  }
-
-  @Override
-  public void onRemoveByKeys(
-      ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId.Key> extIdKeys)
-      throws IOException {
-    updateCache(oldNotesRev, newNotesRev, m -> removeKeys(m.values(), extIdKeys));
-  }
-
-  @Override
   public void onUpdate(
       ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> updatedExtIds)
       throws IOException {
@@ -129,6 +113,7 @@
         m -> {
           removeKeys(m.values(), updatedExtIds.stream().map(e -> e.key()).collect(toSet()));
           for (ExternalId updatedExtId : updatedExtIds) {
+            updatedExtId.checkThatBlobIdIsSet();
             m.put(updatedExtId.accountId(), updatedExtId);
           }
         });
@@ -152,45 +137,7 @@
             m.remove(extId.accountId(), extId);
           }
           for (ExternalId extId : toAdd) {
-            m.put(extId.accountId(), extId);
-          }
-        });
-  }
-
-  @Override
-  public void onReplaceByKeys(
-      ObjectId oldNotesRev,
-      ObjectId newNotesRev,
-      Account.Id accountId,
-      Collection<ExternalId.Key> toRemove,
-      Collection<ExternalId> toAdd)
-      throws IOException {
-    ExternalIdsUpdate.checkSameAccount(toAdd, accountId);
-
-    updateCache(
-        oldNotesRev,
-        newNotesRev,
-        m -> {
-          removeKeys(m.get(accountId), toRemove);
-          for (ExternalId extId : toAdd) {
-            m.put(extId.accountId(), extId);
-          }
-        });
-  }
-
-  @Override
-  public void onReplaceByKeys(
-      ObjectId oldNotesRev,
-      ObjectId newNotesRev,
-      Collection<ExternalId.Key> toRemove,
-      Collection<ExternalId> toAdd)
-      throws IOException {
-    updateCache(
-        oldNotesRev,
-        newNotesRev,
-        m -> {
-          removeKeys(m.values(), toRemove);
-          for (ExternalId extId : toAdd) {
+            extId.checkThatBlobIdIsSet();
             m.put(extId.accountId(), extId);
           }
         });
@@ -211,13 +158,14 @@
             m.remove(extId.accountId(), extId);
           }
           for (ExternalId extId : toAdd) {
+            extId.checkThatBlobIdIsSet();
             m.put(extId.accountId(), extId);
           }
         });
   }
 
   @Override
-  public Set<ExternalId> byAccount(Account.Id accountId) throws IOException {
+  public ImmutableSet<ExternalId> byAccount(Account.Id accountId) throws IOException {
     return get().byAccount().get(accountId);
   }
 
@@ -289,6 +237,7 @@
       Multimap<Account.Id, ExternalId> extIdsByAccount =
           MultimapBuilder.hashKeys().arrayListValues().build();
       for (ExternalId extId : externalIdReader.all(notesRev)) {
+        extId.checkThatBlobIdIsSet();
         extIdsByAccount.put(extId.accountId(), extId);
       }
       return AllExternalIds.create(extIdsByAccount);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
index ead2c1c..bf78b13 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
@@ -138,7 +138,7 @@
         byte[] raw =
             rw.getObjectReader().open(note.getData(), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
         try {
-          extIds.add(ExternalId.parse(note.getName(), raw));
+          extIds.add(ExternalId.parse(note.getName(), raw, note.getData()));
         } catch (Exception e) {
           log.error(String.format("Ignoring invalid external ID note %s", note.getName()), e);
         }
@@ -186,9 +186,9 @@
       return null;
     }
 
-    byte[] raw =
-        rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
-    return ExternalId.parse(noteId.name(), raw);
+    ObjectId noteData = noteMap.get(noteId);
+    byte[] raw = rw.getObjectReader().open(noteData, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
+    return ExternalId.parse(noteId.name(), raw, noteData);
   }
 
   private void checkReadEnabled() throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
index f74210f..5dbde8e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
@@ -84,7 +84,7 @@
                 .open(note.getData(), OBJ_BLOB)
                 .getCachedBytes(ExternalIdReader.MAX_NOTE_SZ);
         try {
-          ExternalId extId = ExternalId.parse(note.getName(), raw);
+          ExternalId extId = ExternalId.parse(note.getName(), raw, note.getData());
           problems.addAll(validateExternalId(extId));
 
           if (extId.email() != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
index 46f27f2..e4434fb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
@@ -32,6 +32,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
 import com.google.common.util.concurrent.Runnables;
@@ -59,6 +60,7 @@
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
@@ -324,12 +326,15 @@
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
+              UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
               for (ExternalId extId : extIds) {
-                insert(o.rw(), o.ins(), o.noteMap(), extId);
+                ExternalId insertedExtId = insert(o.rw(), o.ins(), o.noteMap(), extId);
+                updatedExtIds.onUpdate(insertedExtId);
               }
+              return updatedExtIds;
             });
-    externalIdCache.onCreate(u.oldRev(), u.newRev(), extIds);
-    evictAccounts(extIds);
+    externalIdCache.onCreate(u.oldRev(), u.newRev(), u.updatedExtIds().getUpdated());
+    evictAccounts(u);
   }
 
   /**
@@ -351,12 +356,15 @@
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
+              UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
               for (ExternalId extId : extIds) {
-                upsert(o.rw(), o.ins(), o.noteMap(), extId);
+                ExternalId updatedExtId = upsert(o.rw(), o.ins(), o.noteMap(), extId);
+                updatedExtIds.onUpdate(updatedExtId);
               }
+              return updatedExtIds;
             });
-    externalIdCache.onUpdate(u.oldRev(), u.newRev(), extIds);
-    evictAccounts(extIds);
+    externalIdCache.onUpdate(u.oldRev(), u.newRev(), u.updatedExtIds().getUpdated());
+    evictAccounts(u);
   }
 
   /**
@@ -381,12 +389,15 @@
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
+              UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
               for (ExternalId extId : extIds) {
-                remove(o.rw(), o.noteMap(), extId);
+                ExternalId removedExtId = remove(o.rw(), o.noteMap(), extId);
+                updatedExtIds.onRemove(removedExtId);
               }
+              return updatedExtIds;
             });
-    externalIdCache.onRemove(u.oldRev(), u.newRev(), extIds);
-    evictAccounts(extIds);
+    externalIdCache.onRemove(u.oldRev(), u.newRev(), u.updatedExtIds().getRemoved());
+    evictAccounts(u);
   }
 
   /**
@@ -411,11 +422,14 @@
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
+              UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
               for (ExternalId.Key extIdKey : extIdKeys) {
-                remove(o.rw(), o.noteMap(), extIdKey, accountId);
+                ExternalId removedExtId = remove(o.rw(), o.noteMap(), extIdKey, accountId);
+                updatedExtIds.onRemove(removedExtId);
               }
+              return updatedExtIds;
             });
-    externalIdCache.onRemoveByKeys(u.oldRev(), u.newRev(), accountId, extIdKeys);
+    externalIdCache.onRemove(u.oldRev(), u.newRev(), u.updatedExtIds().getRemoved());
     evictAccount(accountId);
   }
 
@@ -426,19 +440,18 @@
    */
   public void deleteByKeys(Collection<ExternalId.Key> extIdKeys)
       throws IOException, ConfigInvalidException, OrmException {
-    Set<ExternalId> deletedExtIds = new HashSet<>();
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
+              UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
               for (ExternalId.Key extIdKey : extIdKeys) {
                 ExternalId extId = remove(o.rw(), o.noteMap(), extIdKey, null);
-                if (extId != null) {
-                  deletedExtIds.add(extId);
-                }
+                updatedExtIds.onRemove(extId);
               }
+              return updatedExtIds;
             });
-    externalIdCache.onRemoveByKeys(u.oldRev(), u.newRev(), extIdKeys);
-    evictAccounts(deletedExtIds);
+    externalIdCache.onRemove(u.oldRev(), u.newRev(), u.updatedExtIds().getRemoved());
+    evictAccounts(u);
   }
 
   /** Deletes all external IDs of the specified account. */
@@ -466,15 +479,24 @@
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
+              UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
               for (ExternalId.Key extIdKey : toDelete) {
-                remove(o.rw(), o.noteMap(), extIdKey, accountId);
+                ExternalId removedExtId = remove(o.rw(), o.noteMap(), extIdKey, accountId);
+                updatedExtIds.onRemove(removedExtId);
               }
 
               for (ExternalId extId : toAdd) {
-                insert(o.rw(), o.ins(), o.noteMap(), extId);
+                ExternalId insertedExtId = insert(o.rw(), o.ins(), o.noteMap(), extId);
+                updatedExtIds.onUpdate(insertedExtId);
               }
+              return updatedExtIds;
             });
-    externalIdCache.onReplaceByKeys(u.oldRev(), u.newRev(), accountId, toDelete, toAdd);
+    externalIdCache.onReplace(
+        u.oldRev(),
+        u.newRev(),
+        accountId,
+        u.updatedExtIds().getRemoved(),
+        u.updatedExtIds().getUpdated());
     evictAccount(accountId);
   }
 
@@ -490,23 +512,24 @@
    */
   public void replaceByKeys(Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
       throws IOException, ConfigInvalidException, OrmException {
-    Set<ExternalId> deletedExtIds = new HashSet<>();
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
+              UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
               for (ExternalId.Key extIdKey : toDelete) {
-                ExternalId extId = remove(o.rw(), o.noteMap(), extIdKey, null);
-                if (extId != null) {
-                  deletedExtIds.add(extId);
-                }
+                ExternalId removedExtId = remove(o.rw(), o.noteMap(), extIdKey, null);
+                updatedExtIds.onRemove(removedExtId);
               }
 
               for (ExternalId extId : toAdd) {
-                insert(o.rw(), o.ins(), o.noteMap(), extId);
+                ExternalId insertedExtId = insert(o.rw(), o.ins(), o.noteMap(), extId);
+                updatedExtIds.onUpdate(insertedExtId);
               }
+              return updatedExtIds;
             });
-    externalIdCache.onReplaceByKeys(u.oldRev(), u.newRev(), toDelete, toAdd);
-    evictAccounts(Streams.concat(deletedExtIds.stream(), toAdd.stream()).collect(toSet()));
+    externalIdCache.onReplace(
+        u.oldRev(), u.newRev(), u.updatedExtIds().getRemoved(), u.updatedExtIds().getUpdated());
+    evictAccounts(u);
   }
 
   /**
@@ -579,13 +602,13 @@
    *
    * <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}.
    */
-  public static void insert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
+  public static ExternalId insert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
       throws OrmDuplicateKeyException, ConfigInvalidException, IOException {
     if (noteMap.contains(extId.key().sha1())) {
       throw new OrmDuplicateKeyException(
           String.format("external id %s already exists", extId.key().get()));
     }
-    upsert(rw, ins, noteMap, extId);
+    return upsert(rw, ins, noteMap, extId);
   }
 
   /**
@@ -593,7 +616,7 @@
    *
    * <p>If the external ID already exists it is overwritten.
    */
-  public static void upsert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
+  public static ExternalId upsert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
       throws IOException, ConfigInvalidException {
     ObjectId noteId = extId.key().sha1();
     Config c = new Config();
@@ -609,8 +632,9 @@
     }
     extId.writeToConfig(c);
     byte[] raw = c.toText().getBytes(UTF_8);
-    ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
-    noteMap.set(noteId, dataBlob);
+    ObjectId noteData = ins.insert(OBJ_BLOB, raw);
+    noteMap.set(noteId, noteData);
+    return ExternalId.create(extId, noteData);
   }
 
   /**
@@ -619,22 +643,23 @@
    * @throws IllegalStateException is thrown if there is an existing external ID that has the same
    *     key, but otherwise doesn't match the specified external ID.
    */
-  public static void remove(RevWalk rw, NoteMap noteMap, ExternalId extId)
+  public static ExternalId remove(RevWalk rw, NoteMap noteMap, ExternalId extId)
       throws IOException, ConfigInvalidException {
     ObjectId noteId = extId.key().sha1();
     if (!noteMap.contains(noteId)) {
-      return;
+      return null;
     }
 
-    byte[] raw =
-        rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
-    ExternalId actualExtId = ExternalId.parse(noteId.name(), raw);
+    ObjectId noteData = noteMap.get(noteId);
+    byte[] raw = rw.getObjectReader().open(noteData, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
+    ExternalId actualExtId = ExternalId.parse(noteId.name(), raw, noteData);
     checkState(
         extId.equals(actualExtId),
         "external id %s should be removed, but it's not matching the actual external id %s",
         extId.toString(),
         actualExtId.toString());
     noteMap.remove(noteId);
+    return actualExtId;
   }
 
   /**
@@ -653,9 +678,9 @@
       return null;
     }
 
-    byte[] raw =
-        rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
-    ExternalId extId = ExternalId.parse(noteId.name(), raw);
+    ObjectId noteData = noteMap.get(noteId);
+    byte[] raw = rw.getObjectReader().open(noteData, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
+    ExternalId extId = ExternalId.parse(noteId.name(), raw, noteData);
     if (expectedAccountId != null) {
       checkState(
           expectedAccountId.equals(extId.accountId()),
@@ -669,7 +694,7 @@
     return extId;
   }
 
-  private RefsMetaExternalIdsUpdate updateNoteMap(MyConsumer<OpenRepo> update)
+  private RefsMetaExternalIdsUpdate updateNoteMap(ExternalIdUpdater updater)
       throws IOException, ConfigInvalidException, OrmException {
     try {
       return retryer.call(
@@ -682,9 +707,10 @@
 
               try (RevWalk rw = new RevWalk(repo)) {
                 NoteMap noteMap = readNoteMap(rw, rev);
-                update.accept(OpenRepo.create(repo, rw, ins, noteMap));
+                UpdatedExternalIds updatedExtIds =
+                    updater.update(OpenRepo.create(repo, rw, ins, noteMap));
 
-                return commit(repo, rw, ins, rev, noteMap);
+                return commit(repo, rw, ins, rev, noteMap, updatedExtIds);
               }
             }
           });
@@ -699,11 +725,16 @@
   }
 
   private RefsMetaExternalIdsUpdate commit(
-      Repository repo, RevWalk rw, ObjectInserter ins, ObjectId rev, NoteMap noteMap)
+      Repository repo,
+      RevWalk rw,
+      ObjectInserter ins,
+      ObjectId rev,
+      NoteMap noteMap,
+      UpdatedExternalIds updatedExtIds)
       throws IOException {
     ObjectId newRev = commit(repo, rw, ins, rev, noteMap, COMMIT_MSG, committerIdent, authorIdent);
     updateCount.increment();
-    return RefsMetaExternalIdsUpdate.create(rev, newRev);
+    return RefsMetaExternalIdsUpdate.create(rev, newRev, updatedExtIds);
   }
 
   /** Commits updates to the external IDs. */
@@ -775,19 +806,18 @@
     }
   }
 
-  private void evictAccounts(Collection<ExternalId> extIds) throws IOException {
-    if (accountCache == null) {
-      return;
-    }
-
-    for (Account.Id id : extIds.stream().map(ExternalId::accountId).collect(toSet())) {
-      accountCache.evict(id);
+  private void evictAccounts(RefsMetaExternalIdsUpdate u) throws IOException {
+    if (accountCache != null) {
+      for (Account.Id id : u.updatedExtIds().all().map(ExternalId::accountId).collect(toSet())) {
+        accountCache.evict(id);
+      }
     }
   }
 
   @FunctionalInterface
-  private static interface MyConsumer<T> {
-    void accept(T t) throws IOException, ConfigInvalidException, OrmException;
+  private static interface ExternalIdUpdater {
+    UpdatedExternalIds update(OpenRepo openRepo)
+        throws IOException, ConfigInvalidException, OrmException;
   }
 
   @AutoValue
@@ -808,12 +838,45 @@
   @VisibleForTesting
   @AutoValue
   public abstract static class RefsMetaExternalIdsUpdate {
-    static RefsMetaExternalIdsUpdate create(ObjectId oldRev, ObjectId newRev) {
-      return new AutoValue_ExternalIdsUpdate_RefsMetaExternalIdsUpdate(oldRev, newRev);
+    static RefsMetaExternalIdsUpdate create(
+        ObjectId oldRev, ObjectId newRev, UpdatedExternalIds updatedExtIds) {
+      return new AutoValue_ExternalIdsUpdate_RefsMetaExternalIdsUpdate(
+          oldRev, newRev, updatedExtIds);
     }
 
     abstract ObjectId oldRev();
 
     abstract ObjectId newRev();
+
+    abstract UpdatedExternalIds updatedExtIds();
+  }
+
+  public static class UpdatedExternalIds {
+    private Set<ExternalId> updated = new HashSet<>();
+    private Set<ExternalId> removed = new HashSet<>();
+
+    public void onUpdate(ExternalId extId) {
+      if (extId != null) {
+        updated.add(extId);
+      }
+    }
+
+    public void onRemove(ExternalId extId) {
+      if (extId != null) {
+        removed.add(extId);
+      }
+    }
+
+    public Set<ExternalId> getUpdated() {
+      return ImmutableSet.copyOf(updated);
+    }
+
+    public Set<ExternalId> getRemoved() {
+      return ImmutableSet.copyOf(removed);
+    }
+
+    public Stream<ExternalId> all() {
+      return Streams.concat(removed.stream(), updated.stream());
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 64760a65..f8539d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.EmailInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.SshKeyInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.Response;
@@ -54,6 +55,7 @@
 import com.google.gerrit.server.account.GetEditPreferences;
 import com.google.gerrit.server.account.GetEmails;
 import com.google.gerrit.server.account.GetExternalIds;
+import com.google.gerrit.server.account.GetGroups;
 import com.google.gerrit.server.account.GetPreferences;
 import com.google.gerrit.server.account.GetSshKeys;
 import com.google.gerrit.server.account.GetWatchedProjects;
@@ -70,6 +72,7 @@
 import com.google.gerrit.server.account.Stars;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.List;
@@ -116,6 +119,7 @@
   private final GetExternalIds getExternalIds;
   private final DeleteExternalIds deleteExternalIds;
   private final PutStatus putStatus;
+  private final GetGroups getGroups;
 
   @Inject
   AccountApiImpl(
@@ -153,6 +157,7 @@
       GetExternalIds getExternalIds,
       DeleteExternalIds deleteExternalIds,
       PutStatus putStatus,
+      GetGroups getGroups,
       @Assisted AccountResource account) {
     this.account = account;
     this.accountLoaderFactory = ailf;
@@ -189,6 +194,7 @@
     this.getExternalIds = getExternalIds;
     this.deleteExternalIds = deleteExternalIds;
     this.putStatus = putStatus;
+    this.getGroups = getGroups;
   }
 
   @Override
@@ -363,6 +369,15 @@
   }
 
   @Override
+  public List<GroupInfo> getGroups() throws RestApiException {
+    try {
+      return getGroups.apply(account);
+    } catch (OrmException e) {
+      throw asRestApiException("Cannot get groups", e);
+    }
+  }
+
+  @Override
   public List<EmailInfo> getEmails() {
     return getEmails.apply(account);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
index a1deb89..9401c88 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
 import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
@@ -132,6 +133,7 @@
     copy.starred = changeInfo.starred;
     copy.stars = changeInfo.stars;
     copy.submitted = changeInfo.submitted;
+    copy.submitter = changeInfo.submitter;
     copy.id = changeInfo.id;
     return copy;
   }
@@ -168,7 +170,8 @@
     // The followup action is a client-side only operation that does not
     // have a server side handler. It must be manually registered into the
     // resulting action map.
-    if (ctl.getChange().getStatus().isOpen()) {
+    Status status = ctl.getChange().getStatus();
+    if (status.isOpen() || status.equals(Status.MERGED)) {
       UiAction.Description descr = new UiAction.Description();
       PrivateInternals_UiActionDescription.setId(descr, "followup");
       PrivateInternals_UiActionDescription.setMethod(descr, "POST");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index d400999..ac7248b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -57,9 +57,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
@@ -94,7 +92,7 @@
   private static final Logger log = LoggerFactory.getLogger(ChangeInserter.class);
 
   private final PermissionBackend permissionBackend;
-  private final ProjectControl.GenericFactory projectControlFactory;
+  private final ProjectCache projectCache;
   private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
@@ -144,7 +142,7 @@
   @Inject
   ChangeInserter(
       PermissionBackend permissionBackend,
-      ProjectControl.GenericFactory projectControlFactory,
+      ProjectCache projectCache,
       IdentifiedUser.GenericFactory userFactory,
       ChangeControl.GenericFactory changeControlFactory,
       PatchSetInfoFactory patchSetInfoFactory,
@@ -161,7 +159,7 @@
       @Assisted ObjectId commitId,
       @Assisted String refName) {
     this.permissionBackend = permissionBackend;
-    this.projectControlFactory = projectControlFactory;
+    this.projectCache = projectCache;
     this.userFactory = userFactory;
     this.changeControlFactory = changeControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
@@ -567,24 +565,25 @@
     PermissionBackend.ForRef perm =
         permissionBackend.user(ctx.getUser()).project(ctx.getProject()).ref(refName);
     try {
-      RefControl refControl =
-          projectControlFactory.controlFor(ctx.getProject(), ctx.getUser()).controlForRef(refName);
       try (CommitReceivedEvent event =
           new CommitReceivedEvent(
               cmd,
-              refControl.getProjectControl().getProject(),
+              projectCache.checkedGet(ctx.getProject()).getProject(),
               change.getDest().get(),
               ctx.getRevWalk().getObjectReader(),
               commitId,
               ctx.getIdentifiedUser())) {
         commitValidatorsFactory
-            .forGerritCommits(perm, refControl, new NoSshInfo(), ctx.getRevWalk())
+            .forGerritCommits(
+                perm,
+                new Branch.NameKey(ctx.getProject(), refName),
+                ctx.getIdentifiedUser(),
+                new NoSshInfo(),
+                ctx.getRevWalk())
             .validate(event);
       }
     } catch (CommitValidationException e) {
       throw new ResourceConflictException(e.getFullMessage());
-    } catch (NoSuchProjectException e) {
-      throw new ResourceConflictException(e.getMessage());
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 4e6bb5b..3ceeb24 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -116,7 +116,9 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.RemoveReviewerControl;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
@@ -212,6 +214,7 @@
   private final ChangeKindCache changeKindCache;
   private final ChangeIndexCollection indexes;
   private final ApprovalsUtil approvalsUtil;
+  private final RemoveReviewerControl removeReviewerControl;
 
   private boolean lazyLoad = true;
   private AccountLoader accountLoader;
@@ -243,6 +246,7 @@
       ChangeKindCache changeKindCache,
       ChangeIndexCollection indexes,
       ApprovalsUtil approvalsUtil,
+      RemoveReviewerControl removeReviewerControl,
       @Assisted Iterable<ListChangesOption> options) {
     this.db = db;
     this.userProvider = user;
@@ -267,6 +271,7 @@
     this.changeKindCache = changeKindCache;
     this.indexes = indexes;
     this.approvalsUtil = approvalsUtil;
+    this.removeReviewerControl = removeReviewerControl;
     this.options = Sets.immutableEnumSet(options);
   }
 
@@ -556,7 +561,7 @@
       out.removableReviewers = removableReviewers(ctl, out);
     }
 
-    out.submitted = getSubmittedOn(cd);
+    setSubmitter(cd, out);
     out.plugins =
         pluginDefinedAttributesFactory != null ? pluginDefinedAttributesFactory.create(cd) : null;
     out.revertOf = cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null;
@@ -852,9 +857,13 @@
     return Ints.tryParse(value);
   }
 
-  private Timestamp getSubmittedOn(ChangeData cd) throws OrmException {
+  private void setSubmitter(ChangeData cd, ChangeInfo out) throws OrmException {
     Optional<PatchSetApproval> s = cd.getSubmitApproval();
-    return s.isPresent() ? s.get().getGranted() : null;
+    if (!s.isPresent()) {
+      return;
+    }
+    out.submitted = s.get().getGranted();
+    out.submitter = accountLoader.get(s.get().getAccountId());
   }
 
   private Map<String, LabelWithStatus> labelsForSubmittedChange(
@@ -1096,7 +1105,8 @@
     return result;
   }
 
-  private Collection<AccountInfo> removableReviewers(ChangeControl ctl, ChangeInfo out) {
+  private Collection<AccountInfo> removableReviewers(ChangeControl ctl, ChangeInfo out)
+      throws PermissionBackendException, NoSuchChangeException {
     // Although this is called removableReviewers, this method also determines
     // which CCs are removable.
     //
@@ -1116,7 +1126,9 @@
       }
       for (ApprovalInfo ai : label.all) {
         Account.Id id = new Account.Id(ai._accountId);
-        if (ctl.canRemoveReviewer(id, MoreObjects.firstNonNull(ai.value, 0))) {
+
+        if (removeReviewerControl.testRemoveReviewer(
+            ctl.getNotes(), ctl.getUser(), id, MoreObjects.firstNonNull(ai.value, 0))) {
           removable.add(id);
         } else {
           fixed.add(id);
@@ -1133,7 +1145,7 @@
       for (AccountInfo ai : ccs) {
         if (ai._accountId != null) {
           Account.Id id = new Account.Id(ai._accountId);
-          if (ctl.canRemoveReviewer(id, 0)) {
+          if (removeReviewerControl.testRemoveReviewer(ctl.getNotes(), ctl.getUser(), id, 0)) {
             removable.add(id);
           }
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index 930cb8b..f3c5f0a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
@@ -32,6 +33,7 @@
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -70,7 +72,7 @@
   public ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource rsrc, CherryPickInput input)
       throws OrmException, IOException, UpdateException, RestApiException,
-          PermissionBackendException, ConfigInvalidException {
+          PermissionBackendException, ConfigInvalidException, NoSuchProjectException {
     input.parent = input.parent == null ? 1 : input.parent;
     if (input.message == null || input.message.trim().isEmpty()) {
       throw new BadRequestException("message must be non-empty");
@@ -93,7 +95,7 @@
               rsrc.getChange(),
               rsrc.getPatchSet(),
               input,
-              rsrc.getControl().getProjectControl().controlForRef(refName));
+              new Branch.NameKey(rsrc.getProject(), refName));
       return json.noOptions().format(rsrc.getProject(), cherryPickedChangeId);
     } catch (InvalidChangeOperationException e) {
       throw new BadRequestException(e.getMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index deb379d..6e555e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -51,8 +50,9 @@
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -94,6 +94,7 @@
   private final PatchSetInserter.Factory patchSetInserterFactory;
   private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeNotes.Factory changeNotesFactory;
+  private final ProjectControl.GenericFactory projectControlFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil changeMessagesUtil;
   private final PatchSetUtil psUtil;
@@ -111,6 +112,7 @@
       PatchSetInserter.Factory patchSetInserterFactory,
       MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory changeNotesFactory,
+      ProjectControl.GenericFactory projectControlFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil changeMessagesUtil,
       PatchSetUtil psUtil,
@@ -125,6 +127,7 @@
     this.patchSetInserterFactory = patchSetInserterFactory;
     this.mergeUtilFactory = mergeUtilFactory;
     this.changeNotesFactory = changeNotesFactory;
+    this.projectControlFactory = projectControlFactory;
     this.approvalsUtil = approvalsUtil;
     this.changeMessagesUtil = changeMessagesUtil;
     this.psUtil = psUtil;
@@ -136,9 +139,9 @@
       Change change,
       PatchSet patch,
       CherryPickInput input,
-      RefControl refControl)
+      Branch.NameKey dest)
       throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
-          UpdateException, RestApiException, ConfigInvalidException {
+          UpdateException, RestApiException, ConfigInvalidException, NoSuchProjectException {
     return cherryPick(
         batchUpdateFactory,
         change,
@@ -146,7 +149,7 @@
         change.getProject(),
         ObjectId.fromString(patch.getRevision().get()),
         input,
-        refControl);
+        dest);
   }
 
   public Change.Id cherryPick(
@@ -156,9 +159,9 @@
       Project.NameKey project,
       ObjectId sourceCommit,
       CherryPickInput input,
-      RefControl destRefControl)
+      Branch.NameKey dest)
       throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
-          UpdateException, RestApiException, ConfigInvalidException {
+          UpdateException, RestApiException, ConfigInvalidException, NoSuchProjectException {
 
     IdentifiedUser identifiedUser = user.get();
     try (Repository git = gitManager.openRepository(project);
@@ -168,11 +171,10 @@
         ObjectInserter oi = git.newObjectInserter();
         ObjectReader reader = oi.newReader();
         CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader)) {
-      String destRefName = destRefControl.getRefName();
-      Ref destRef = git.getRefDatabase().exactRef(destRefName);
+      Ref destRef = git.getRefDatabase().exactRef(dest.get());
       if (destRef == null) {
         throw new InvalidChangeOperationException(
-            String.format("Branch %s does not exist.", destRefName));
+            String.format("Branch %s does not exist.", dest.get()));
       }
 
       RevCommit baseCommit = getBaseCommit(destRef, project.get(), revWalk, input.base);
@@ -200,8 +202,10 @@
       String commitMessage = ChangeIdUtil.insertId(input.message, computedChangeId).trim() + '\n';
 
       CodeReviewCommit cherryPickCommit;
+      ProjectControl projectControl =
+          projectControlFactory.controlFor(dest.getParentKey(), identifiedUser);
       try {
-        ProjectState projectState = destRefControl.getProjectControl().getProjectState();
+        ProjectState projectState = projectControl.getProjectState();
         cherryPickCommit =
             mergeUtilFactory
                 .create(projectState)
@@ -242,8 +246,7 @@
           if (destChanges.size() == 1) {
             // The change key exists on the destination branch. The cherry pick
             // will be added as a new patch set.
-            ChangeControl destCtl =
-                destRefControl.getProjectControl().controlFor(destChanges.get(0).notes());
+            ChangeControl destCtl = projectControl.controlFor(destChanges.get(0).notes());
             result = insertPatchSet(bu, git, destCtl, cherryPickCommit, input);
           } else {
             // Change key not found on destination branch. We can create a new
@@ -254,16 +257,13 @@
             }
             result =
                 createNewChange(
-                    bu, cherryPickCommit, destRefName, newTopic, sourceChange, sourceCommit, input);
+                    bu, cherryPickCommit, dest.get(), newTopic, sourceChange, sourceCommit, input);
 
             if (sourceChange != null && sourcePatchId != null) {
               bu.addOp(
                   sourceChange.getId(),
                   new AddMessageToSourceChangeOp(
-                      changeMessagesUtil,
-                      sourcePatchId,
-                      RefNames.shortName(destRefName),
-                      cherryPickCommit));
+                      changeMessagesUtil, sourcePatchId, dest.getShortName(), cherryPickCommit));
             }
           }
           bu.execute();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
index c7ad77c..5d5a6ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+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.RefNames;
@@ -30,6 +31,7 @@
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.CommitResource;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -68,7 +70,7 @@
   public ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, CommitResource rsrc, CherryPickInput input)
       throws OrmException, IOException, UpdateException, RestApiException,
-          PermissionBackendException, ConfigInvalidException {
+          PermissionBackendException, ConfigInvalidException, NoSuchProjectException {
     RevCommit commit = rsrc.getCommit();
     String message = Strings.nullToEmpty(input.message).trim();
     input.message = message.isEmpty() ? commit.getFullMessage() : message;
@@ -97,7 +99,7 @@
               projectName,
               commit,
               input,
-              rsrc.getProject().controlForRef(refName));
+              new Branch.NameKey(rsrc.getProject().getProject().getNameKey(), refName));
       return json.noOptions().format(projectName, cherryPickedChangeId);
     } catch (InvalidChangeOperationException e) {
       throw new BadRequestException(e.getMessage());
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 2a0a412..c9827f1 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
@@ -109,7 +109,6 @@
   private final ChangeFinder changeFinder;
   private final PatchSetUtil psUtil;
   private final boolean allowDrafts;
-  private final boolean privateByDefault;
   private final MergeUtil.Factory mergeUtilFactory;
   private final SubmitType submitType;
   private final NotifyUtil notifyUtil;
@@ -150,7 +149,6 @@
     this.changeFinder = changeFinder;
     this.psUtil = psUtil;
     this.allowDrafts = config.getBoolean("change", "allowDrafts", true);
-    this.privateByDefault = config.getBoolean("change", "privateByDefault", false);
     this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
     this.mergeUtilFactory = mergeUtilFactory;
     this.notifyUtil = notifyUtil;
@@ -261,6 +259,7 @@
         c = newCommit(oi, rw, author, mergeTip, commitMessage);
       }
 
+      boolean privateByDefault = rsrc.getProjectState().isPrivateByDefault();
       Change.Id changeId = new Change.Id(seq.nextChangeId());
       ChangeInserter ins = changeInserterFactory.create(changeId, c, refName);
       ins.setMessage(String.format("Uploaded patch set %s.", ins.getPatchSetId().get()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index df4b435..933705a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -38,6 +38,8 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.RemoveReviewerControl;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.BatchUpdateReviewDb;
 import com.google.gerrit.server.update.ChangeContext;
@@ -70,6 +72,7 @@
   private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
   private final NotesMigration migration;
   private final NotifyUtil notifyUtil;
+  private final RemoveReviewerControl removeReviewerControl;
 
   private final Account reviewer;
   private final DeleteReviewerInput input;
@@ -91,6 +94,7 @@
       DeleteReviewerSender.Factory deleteReviewerSenderFactory,
       NotesMigration migration,
       NotifyUtil notifyUtil,
+      RemoveReviewerControl removeReviewerControl,
       @Assisted Account reviewerAccount,
       @Assisted DeleteReviewerInput input) {
     this.approvalsUtil = approvalsUtil;
@@ -102,6 +106,7 @@
     this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
     this.migration = migration;
     this.notifyUtil = notifyUtil;
+    this.removeReviewerControl = removeReviewerControl;
 
     this.reviewer = reviewerAccount;
     this.input = input;
@@ -109,7 +114,7 @@
 
   @Override
   public boolean updateChange(ChangeContext ctx)
-      throws AuthException, ResourceNotFoundException, OrmException {
+      throws AuthException, ResourceNotFoundException, OrmException, PermissionBackendException {
     Account.Id reviewerId = reviewer.getId();
     if (!approvalsUtil.getReviewers(ctx.getDb(), ctx.getNotes()).all().contains(reviewerId)) {
       throw new ResourceNotFoundException();
@@ -130,21 +135,18 @@
     List<PatchSetApproval> del = new ArrayList<>();
     boolean votesRemoved = false;
     for (PatchSetApproval a : approvals(ctx, reviewerId)) {
-      if (ctx.getControl().canRemoveReviewer(a)) {
-        del.add(a);
-        if (a.getPatchSetId().equals(currPs.getId()) && a.getValue() != 0) {
-          oldApprovals.put(a.getLabel(), a.getValue());
-          removedVotesMsg
-              .append("* ")
-              .append(a.getLabel())
-              .append(formatLabelValue(a.getValue()))
-              .append(" by ")
-              .append(userFactory.create(a.getAccountId()).getNameEmail())
-              .append("\n");
-          votesRemoved = true;
-        }
-      } else {
-        throw new AuthException("delete reviewer not permitted");
+      removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), a);
+      del.add(a);
+      if (a.getPatchSetId().equals(currPs.getId()) && a.getValue() != 0) {
+        oldApprovals.put(a.getLabel(), a.getValue());
+        removedVotesMsg
+            .append("* ")
+            .append(a.getLabel())
+            .append(formatLabelValue(a.getValue()))
+            .append(" by ")
+            .append(userFactory.create(a.getAccountId()).getNameEmail())
+            .append("\n");
+        votesRemoved = true;
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
index c31f72d..7029101 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -40,7 +40,9 @@
 import com.google.gerrit.server.extensions.events.VoteDeleted;
 import com.google.gerrit.server.mail.send.DeleteVoteSender;
 import com.google.gerrit.server.mail.send.ReplyToChangeSender;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.RemoveReviewerControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -72,6 +74,7 @@
   private final VoteDeleted voteDeleted;
   private final DeleteVoteSender.Factory deleteVoteSenderFactory;
   private final NotifyUtil notifyUtil;
+  private final RemoveReviewerControl removeReviewerControl;
 
   @Inject
   DeleteVote(
@@ -83,7 +86,8 @@
       IdentifiedUser.GenericFactory userFactory,
       VoteDeleted voteDeleted,
       DeleteVoteSender.Factory deleteVoteSenderFactory,
-      NotifyUtil notifyUtil) {
+      NotifyUtil notifyUtil,
+      RemoveReviewerControl removeReviewerControl) {
     super(retryHelper);
     this.db = db;
     this.approvalsUtil = approvalsUtil;
@@ -93,6 +97,7 @@
     this.voteDeleted = voteDeleted;
     this.deleteVoteSenderFactory = deleteVoteSenderFactory;
     this.notifyUtil = notifyUtil;
+    this.removeReviewerControl = removeReviewerControl;
   }
 
   @Override
@@ -143,7 +148,8 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, AuthException, ResourceNotFoundException, IOException {
+        throws OrmException, AuthException, ResourceNotFoundException, IOException,
+            PermissionBackendException {
       ChangeControl ctl = ctx.getControl();
       change = ctl.getChange();
       PatchSet.Id psId = change.currentPatchSetId();
@@ -166,8 +172,12 @@
           // Populate map for non-matching labels, needed by VoteDeleted.
           newApprovals.put(a.getLabel(), a.getValue());
           continue;
-        } else if (!ctl.canRemoveReviewer(a)) {
-          throw new AuthException("delete vote not permitted");
+        } else {
+          try {
+            removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), a);
+          } catch (AuthException e) {
+            throw new AuthException("delete vote not permitted", e);
+          }
         }
         // Set the approval to 0 if vote is being removed.
         newApprovals.put(a.getLabel(), (short) 0);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 8f2c3a8..5e26305 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -338,7 +339,13 @@
             commitId,
             ctx.getIdentifiedUser())) {
       commitValidatorsFactory
-          .forGerritCommits(perm, origCtl.getRefControl(), new NoSshInfo(), ctx.getRevWalk())
+          .forGerritCommits(
+              perm,
+              new Branch.NameKey(
+                  origCtl.getProject().getNameKey(), origCtl.getRefControl().getRefName()),
+              ctx.getIdentifiedUser(),
+              new NoSshInfo(),
+              ctx.getRevWalk())
           .validate(event);
     } catch (CommitValidationException e) {
       throw new ResourceConflictException(e.getFullMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
index 6d20864..e7303e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.Maps;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -135,7 +136,8 @@
         update = or.repo.updateRef(name.get());
         if (update.getOldObjectId() != null) {
           oldTip = or.rw.parseCommit(update.getOldObjectId());
-        } else if (Objects.equals(or.repo.getFullBranch(), name.get())) {
+        } else if (Objects.equals(or.repo.getFullBranch(), name.get())
+            || Objects.equals(RefNames.REFS_CONFIG, name.get())) {
           oldTip = null;
           update.setExpectedOldObjectId(ObjectId.zeroId());
         } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 91379fd..5ee9c45 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -125,6 +125,9 @@
   private static final String KEY_REQUIRE_SIGNED_PUSH = "requireSignedPush";
   private static final String KEY_REJECT_IMPLICIT_MERGES = "rejectImplicitMerges";
 
+  private static final String CHANGE = "change";
+  private static final String KEY_PRIVATE_BY_DEFAULT = "privateByDefault";
+
   private static final String SUBMIT = "submit";
   private static final String KEY_ACTION = "action";
   private static final String KEY_MERGE_CONTENT = "mergeContent";
@@ -535,6 +538,10 @@
     p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
     p.setRejectImplicitMerges(
         getEnum(rc, RECEIVE, null, KEY_REJECT_IMPLICIT_MERGES, InheritableBoolean.INHERIT));
+
+    p.setPrivateByDefault(
+        getEnum(rc, CHANGE, null, KEY_PRIVATE_BY_DEFAULT, InheritableBoolean.INHERIT));
+
     p.setEnableReviewerByEmail(
         getEnum(rc, REVIEWER, null, KEY_ENABLE_REVIEWER_BY_EMAIL, InheritableBoolean.INHERIT));
 
@@ -1106,6 +1113,15 @@
         KEY_REJECT_IMPLICIT_MERGES,
         p.getRejectImplicitMerges(),
         InheritableBoolean.INHERIT);
+
+    set(
+        rc,
+        CHANGE,
+        null,
+        KEY_PRIVATE_BY_DEFAULT,
+        p.getPrivateByDefault(),
+        InheritableBoolean.INHERIT);
+
     set(
         rc,
         REVIEWER,
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 eb9c024..8ec62aa 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
@@ -45,10 +45,12 @@
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import org.apache.commons.lang.StringUtils;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -135,6 +137,8 @@
   private final BatchUpdate.Factory batchUpdateFactory;
   private final VerboseSuperprojectUpdate verboseSuperProject;
   private final boolean enableSuperProjectSubscriptions;
+  private final long maxCombinedCommitMessageSize;
+  private final long maxCommitMessages;
   private final MergeOpRepoManager orm;
   private final Map<Branch.NameKey, GitModules> branchGitModules;
 
@@ -170,6 +174,9 @@
         cfg.getEnum("submodule", null, "verboseSuperprojectUpdate", VerboseSuperprojectUpdate.TRUE);
     this.enableSuperProjectSubscriptions =
         cfg.getBoolean("submodule", "enableSuperProjectSubscriptions", true);
+    this.maxCombinedCommitMessageSize =
+        cfg.getLong("submodule", "maxCombinedCommitMessageSize", 256 << 10);
+    this.maxCommitMessages = cfg.getLong("submodule", "maxCommitMessages", 1000);
     this.orm = orm;
     this.updatedBranches = updatedBranches;
     this.targets = MultimapBuilder.hashKeys().hashSetValues().build();
@@ -547,8 +554,11 @@
       RevCommit newCommit,
       RevCommit oldCommit)
       throws SubmoduleException {
-    msgbuf.append("* Update " + s.getPath());
-    msgbuf.append(" from branch '" + s.getSubmodule().getShortName() + "'");
+    msgbuf.append("* Update ");
+    msgbuf.append(s.getPath());
+    msgbuf.append(" from branch '");
+    msgbuf.append(s.getSubmodule().getShortName());
+    msgbuf.append("'");
 
     // newly created submodule gitlink, do not append whole history
     if (oldCommit == null) {
@@ -559,13 +569,27 @@
       subOr.rw.resetRetain(subOr.canMergeFlag);
       subOr.rw.markStart(newCommit);
       subOr.rw.markUninteresting(oldCommit);
-      for (RevCommit c : subOr.rw) {
+      int numMessages = 0;
+      for (Iterator<RevCommit> iter = subOr.rw.iterator(); iter.hasNext(); ) {
+        RevCommit c = iter.next();
         subOr.rw.parseBody(c);
-        if (verboseSuperProject == VerboseSuperprojectUpdate.SUBJECT_ONLY) {
-          msgbuf.append("\n  - " + c.getShortMessage());
-        } else if (verboseSuperProject == VerboseSuperprojectUpdate.TRUE) {
-          msgbuf.append("\n  - " + c.getFullMessage().replace("\n", "\n    "));
+
+        String message =
+            verboseSuperProject == VerboseSuperprojectUpdate.SUBJECT_ONLY
+                ? c.getShortMessage()
+                : StringUtils.replace(c.getFullMessage(), "\n", "\n    ");
+
+        String bullet = "\n  - ";
+        String ellipsis = "\n\n[...]";
+        int newSize = msgbuf.length() + bullet.length() + message.length();
+        if (++numMessages > maxCommitMessages
+            || newSize > maxCombinedCommitMessageSize
+            || iter.hasNext() && (newSize + ellipsis.length()) > maxCombinedCommitMessageSize) {
+          msgbuf.append(ellipsis);
+          break;
         }
+        msgbuf.append(bullet);
+        msgbuf.append(message);
       }
     } catch (IOException e) {
       throw new SubmoduleException(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 7f909d4..d4e085c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -128,11 +128,12 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.CreateRefControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.ssh.SshInfo;
@@ -320,6 +321,7 @@
   private final String canonicalWebUrl;
   private final SubmoduleOp.Factory subOpFactory;
   private final TagCache tagCache;
+  private final CreateRefControl createRefControl;
 
   // Assisted injected fields.
   private final AllRefsWatcher allRefsWatcher;
@@ -402,6 +404,7 @@
       SshInfo sshInfo,
       SubmoduleOp.Factory subOpFactory,
       TagCache tagCache,
+      CreateRefControl createRefControl,
       @Assisted ProjectControl projectControl,
       @Assisted ReceivePack rp,
       @Assisted AllRefsWatcher allRefsWatcher,
@@ -440,6 +443,7 @@
     this.sshInfo = sshInfo;
     this.subOpFactory = subOpFactory;
     this.tagCache = tagCache;
+    this.createRefControl = createRefControl;
 
     // Assisted injected fields.
     this.allRefsWatcher = allRefsWatcher;
@@ -524,7 +528,7 @@
 
     try {
       parseCommands(commands);
-    } catch (PermissionBackendException err) {
+    } catch (PermissionBackendException | NoSuchProjectException | IOException err) {
       for (ReceiveCommand cmd : actualCommands) {
         if (cmd.getResult() == NOT_ATTEMPTED) {
           cmd.setResult(REJECTED_OTHER_REASON, "internal server error");
@@ -772,7 +776,7 @@
   }
 
   private void parseCommands(Collection<ReceiveCommand> commands)
-      throws PermissionBackendException {
+      throws PermissionBackendException, NoSuchProjectException, IOException {
     List<String> optionList = rp.getPushOptions();
     if (optionList != null) {
       for (String option : optionList) {
@@ -977,7 +981,8 @@
     }
   }
 
-  private void parseCreate(ReceiveCommand cmd) throws PermissionBackendException {
+  private void parseCreate(ReceiveCommand cmd)
+      throws PermissionBackendException, NoSuchProjectException, IOException {
     RevObject obj;
     try {
       obj = rp.getRevWalk().parseAny(cmd.getNewId());
@@ -994,8 +999,8 @@
       return;
     }
 
-    RefControl ctl = projectControl.controlForRef(cmd.getRefName());
-    String rejectReason = ctl.canCreate(rp.getRepository(), obj);
+    Branch.NameKey branch = new Branch.NameKey(project.getName(), cmd.getRefName());
+    String rejectReason = createRefControl.canCreateRef(rp.getRepository(), obj, user, branch);
     if (rejectReason != null) {
       reject(cmd, "prohibited by Gerrit: " + rejectReason);
       return;
@@ -1006,7 +1011,7 @@
       return;
     }
 
-    validateNewCommits(ctl, cmd);
+    validateNewCommits(new Branch.NameKey(project.getNameKey(), cmd.getRefName()), cmd);
     actualCommands.add(cmd);
   }
 
@@ -1026,7 +1031,7 @@
       if (!validRefOperation(cmd)) {
         return;
       }
-      validateNewCommits(projectControl.controlForRef(cmd.getRefName()), cmd);
+      validateNewCommits(new Branch.NameKey(project.getNameKey(), cmd.getRefName()), cmd);
       actualCommands.add(cmd);
     } else {
       if (RefNames.REFS_CONFIG.equals(cmd.getRefName())) {
@@ -1097,9 +1102,8 @@
     }
     logDebug("Rewinding {}", cmd);
 
-    RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (newObject != null) {
-      validateNewCommits(ctl, cmd);
+      validateNewCommits(new Branch.NameKey(project.getNameKey(), cmd.getRefName()), cmd);
       if (cmd.getResult() != NOT_ATTEMPTED) {
         return;
       }
@@ -1131,7 +1135,6 @@
     final NotesMigration notesMigration;
     private final boolean defaultPublishComments;
     Branch.NameKey dest;
-    RefControl ctl;
     PermissionBackend.ForRef perm;
     Set<Account.Id> reviewer = Sets.newLinkedHashSet();
     Set<Account.Id> cc = Sets.newLinkedHashSet();
@@ -1430,7 +1433,9 @@
       logDebug("Handling {}", RefNames.REFS_USERS_SELF);
       ref = RefNames.refsUsers(user.getAccountId());
     }
-    if (!rp.getAdvertisedRefs().containsKey(ref) && !ref.equals(readHEAD(repo))) {
+    if (!rp.getAdvertisedRefs().containsKey(ref)
+        && !ref.equals(readHEAD(repo))
+        && !ref.equals(RefNames.REFS_CONFIG)) {
       logDebug("Ref {} not found", ref);
       if (ref.startsWith(Constants.R_HEADS)) {
         String n = ref.substring(Constants.R_HEADS.length());
@@ -1442,11 +1447,9 @@
     }
 
     magicBranch.dest = new Branch.NameKey(project.getNameKey(), ref);
-    magicBranch.ctl = projectControl.controlForRef(ref);
     magicBranch.perm = permissions.ref(ref);
-    if (projectControl.getProject().getState()
-        != com.google.gerrit.extensions.client.ProjectState.ACTIVE) {
-      reject(cmd, "project is read only");
+    if (!projectControl.getProject().getState().permitsWrite()) {
+      reject(cmd, "project state does not permit write");
       return;
     }
 
@@ -1582,7 +1585,7 @@
     // commits and the target branch head.
     //
     try {
-      Ref targetRef = rp.getAdvertisedRefs().get(magicBranch.ctl.getRefName());
+      Ref targetRef = rp.getAdvertisedRefs().get(magicBranch.dest.get());
       if (targetRef == null || targetRef.getObjectId() == null) {
         // The destination branch does not yet exist. Assume the
         // history being sent for review will start it and thus
@@ -1790,7 +1793,7 @@
           logDebug("Creating new change for {} even though it is already tracked", name);
         }
 
-        if (!validCommit(rp.getRevWalk(), magicBranch.perm, magicBranch.ctl, magicBranch.cmd, c)) {
+        if (!validCommit(rp.getRevWalk(), magicBranch.perm, magicBranch.dest, magicBranch.cmd, c)) {
           // Not a change the user can propose? Abort as early as possible.
           newChanges = Collections.emptyList();
           logDebug("Aborting early due to invalid commit");
@@ -1984,7 +1987,7 @@
         rw.markUninteresting(c);
       }
     } else {
-      markHeadsAsUninteresting(rw, magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
+      markHeadsAsUninteresting(rw, magicBranch.dest != null ? magicBranch.dest.get() : null);
     }
     return start;
   }
@@ -1994,11 +1997,11 @@
     for (RevCommit c : magicBranch.baseCommit) {
       rp.getRevWalk().markUninteresting(c);
     }
-    Ref targetRef = allRefs().get(magicBranch.ctl.getRefName());
+    Ref targetRef = allRefs().get(magicBranch.dest.get());
     if (targetRef != null) {
       logDebug(
           "Marking target ref {} ({}) uninteresting",
-          magicBranch.ctl.getRefName(),
+          magicBranch.dest.get(),
           targetRef.getObjectId().name());
       rp.getRevWalk().markUninteresting(rp.getRevWalk().parseCommit(targetRef.getObjectId()));
     }
@@ -2006,7 +2009,7 @@
 
   private void rejectImplicitMerges(Set<RevCommit> mergedParents) throws IOException {
     if (!mergedParents.isEmpty()) {
-      Ref targetRef = allRefs().get(magicBranch.ctl.getRefName());
+      Ref targetRef = allRefs().get(magicBranch.dest.get());
       if (targetRef != null) {
         RevWalk rw = rp.getRevWalk();
         RevCommit tip = rw.parseCommit(targetRef.getObjectId());
@@ -2093,14 +2096,14 @@
     }
 
     private void setChangeId(int id) {
+      boolean privateByDefault = projectCache.get(project.getNameKey()).isPrivateByDefault();
+
       changeId = new Change.Id(id);
       ins =
           changeInserterFactory
               .create(changeId, commit, refName)
               .setTopic(magicBranch.topic)
-              .setPrivate(
-                  magicBranch.isPrivate
-                      || (receiveConfig.privateByDefault && !magicBranch.removePrivate))
+              .setPrivate(magicBranch.isPrivate || (privateByDefault && !magicBranch.removePrivate))
               .setWorkInProgress(magicBranch.workInProgress)
               // Changes already validated in validateNewCommits.
               .setValidate(false);
@@ -2372,8 +2375,7 @@
       }
 
       PermissionBackend.ForRef perm = permissions.ref(change.getDest().get());
-      RefControl refctl = projectControl.controlForRef(change.getDest());
-      if (!validCommit(rp.getRevWalk(), perm, refctl, inputCommand, newCommit)) {
+      if (!validCommit(rp.getRevWalk(), perm, change.getDest(), inputCommand, newCommit)) {
         return false;
       }
       rp.getRevWalk().parseBody(priorCommit);
@@ -2677,9 +2679,9 @@
     return true;
   }
 
-  private void validateNewCommits(RefControl ctl, ReceiveCommand cmd)
+  private void validateNewCommits(Branch.NameKey branch, ReceiveCommand cmd)
       throws PermissionBackendException {
-    PermissionBackend.ForRef perm = permissions.ref(ctl.getRefName());
+    PermissionBackend.ForRef perm = permissions.ref(branch.get());
     if (!RefNames.REFS_CONFIG.equals(cmd.getRefName())
         && !(MagicBranch.isMagicBranch(cmd.getRefName())
             || NEW_PATCHSET_PATTERN.matcher(cmd.getRefName()).matches())
@@ -2713,7 +2715,7 @@
         i++;
         if (existing.keySet().contains(c)) {
           continue;
-        } else if (!validCommit(walk, perm, ctl, cmd, c)) {
+        } else if (!validCommit(walk, perm, branch, cmd, c)) {
           break;
         }
 
@@ -2749,7 +2751,11 @@
   }
 
   private boolean validCommit(
-      RevWalk rw, PermissionBackend.ForRef perm, RefControl ctl, ReceiveCommand cmd, ObjectId id)
+      RevWalk rw,
+      PermissionBackend.ForRef perm,
+      Branch.NameKey branch,
+      ReceiveCommand cmd,
+      ObjectId id)
       throws IOException {
 
     if (validCommits.contains(id)) {
@@ -2760,15 +2766,16 @@
     rw.parseBody(c);
 
     try (CommitReceivedEvent receiveEvent =
-        new CommitReceivedEvent(cmd, project, ctl.getRefName(), rw.getObjectReader(), c, user)) {
+        new CommitReceivedEvent(cmd, project, branch.get(), rw.getObjectReader(), c, user)) {
       boolean isMerged =
           magicBranch != null
               && cmd.getRefName().equals(magicBranch.cmd.getRefName())
               && magicBranch.merged;
       CommitValidators validators =
           isMerged
-              ? commitValidatorsFactory.forMergedCommits(perm, ctl)
-              : commitValidatorsFactory.forReceiveCommits(perm, ctl, sshInfo, repo, rw);
+              ? commitValidatorsFactory.forMergedCommits(perm, user.asIdentifiedUser())
+              : commitValidatorsFactory.forReceiveCommits(
+                  perm, branch, user.asIdentifiedUser(), sshInfo, repo, rw);
       messages.addAll(validators.validate(receiveEvent));
     } catch (CommitValidationException e) {
       logDebug("Commit validation failed on {}", c.name());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
index 39b6d8b..7be6dcc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
@@ -28,7 +28,6 @@
   final boolean checkMagicRefs;
   final boolean checkReferencedObjectsAreReachable;
   final boolean allowDrafts;
-  final boolean privateByDefault;
   private final int systemMaxBatchChanges;
   private final AccountLimits.Factory limitsFactory;
 
@@ -38,7 +37,6 @@
     checkReferencedObjectsAreReachable =
         config.getBoolean("receive", null, "checkReferencedObjectsAreReachable", true);
     allowDrafts = config.getBoolean("change", null, "allowDrafts", true);
-    privateByDefault = config.getBoolean("change", null, "privateByDefault", false);
     systemMaxBatchChanges = config.getInt("receive", "maxBatchChanges", 0);
     this.limitsFactory = limitsFactory;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 17303f8..41381e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -44,9 +45,8 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Inject;
@@ -89,6 +89,7 @@
     private final AllUsersName allUsers;
     private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
     private final String installCommitMsgHookCommand;
+    private final ProjectCache projectCache;
 
     @Inject
     Factory(
@@ -97,7 +98,8 @@
         @GerritServerConfig Config cfg,
         DynamicSet<CommitValidationListener> pluginValidators,
         AllUsersName allUsers,
-        ExternalIdsConsistencyChecker externalIdsConsistencyChecker) {
+        ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
+        ProjectCache projectCache) {
       this.gerritIdent = gerritIdent;
       this.canonicalWebUrl = canonicalWebUrl;
       this.pluginValidators = pluginValidators;
@@ -105,26 +107,29 @@
       this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
       this.installCommitMsgHookCommand =
           cfg != null ? cfg.getString("gerrit", null, "installCommitMsgHookCommand") : null;
+      this.projectCache = projectCache;
     }
 
     public CommitValidators forReceiveCommits(
         PermissionBackend.ForRef perm,
-        RefControl refctl,
+        Branch.NameKey branch,
+        IdentifiedUser user,
         SshInfo sshInfo,
         Repository repo,
         RevWalk rw)
         throws IOException {
       NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(repo, rw);
-      IdentifiedUser user = refctl.getUser().asIdentifiedUser();
+      ProjectState projectState = projectCache.checkedGet(branch.getParentKey());
       return new CommitValidators(
           ImmutableList.of(
               new UploadMergesPermissionValidator(perm),
               new AmendedGerritMergeCommitValidationListener(perm, gerritIdent),
               new AuthorUploaderValidator(user, perm, canonicalWebUrl),
               new CommitterUploaderValidator(user, perm, canonicalWebUrl),
-              new SignedOffByValidator(user, perm, refctl.getProjectControl().getProjectState()),
-              new ChangeIdValidator(refctl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
-              new ConfigValidator(refctl, rw, allUsers),
+              new SignedOffByValidator(user, perm, projectState),
+              new ChangeIdValidator(
+                  projectState, user, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
+              new ConfigValidator(branch, user, rw, allUsers),
               new BannedCommitsValidator(rejectCommits),
               new PluginCommitValidationListener(pluginValidators),
               new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
@@ -132,23 +137,31 @@
     }
 
     public CommitValidators forGerritCommits(
-        PermissionBackend.ForRef perm, RefControl refctl, SshInfo sshInfo, RevWalk rw) {
-      IdentifiedUser user = refctl.getUser().asIdentifiedUser();
+        PermissionBackend.ForRef perm,
+        Branch.NameKey branch,
+        IdentifiedUser user,
+        SshInfo sshInfo,
+        RevWalk rw)
+        throws IOException {
       return new CommitValidators(
           ImmutableList.of(
               new UploadMergesPermissionValidator(perm),
               new AmendedGerritMergeCommitValidationListener(perm, gerritIdent),
               new AuthorUploaderValidator(user, perm, canonicalWebUrl),
-              new SignedOffByValidator(user, perm, refctl.getProjectControl().getProjectState()),
-              new ChangeIdValidator(refctl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
-              new ConfigValidator(refctl, rw, allUsers),
+              new SignedOffByValidator(user, perm, projectCache.checkedGet(branch.getParentKey())),
+              new ChangeIdValidator(
+                  projectCache.checkedGet(branch.getParentKey()),
+                  user,
+                  canonicalWebUrl,
+                  installCommitMsgHookCommand,
+                  sshInfo),
+              new ConfigValidator(branch, user, rw, allUsers),
               new PluginCommitValidationListener(pluginValidators),
               new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
               new AccountValidator(allUsers)));
     }
 
-    public CommitValidators forMergedCommits(PermissionBackend.ForRef perm, RefControl refControl) {
-      IdentifiedUser user = refControl.getUser().asIdentifiedUser();
+    public CommitValidators forMergedCommits(PermissionBackend.ForRef perm, IdentifiedUser user) {
       // Generally only include validators that are based on permissions of the
       // user creating a change for a merged commit; generally exclude
       // validators that would require amending the change in order to correct.
@@ -208,22 +221,23 @@
             + " line format in commit message footer";
     private static final Pattern CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
 
-    private final ProjectControl projectControl;
+    private final ProjectState projectState;
     private final String canonicalWebUrl;
     private final String installCommitMsgHookCommand;
     private final SshInfo sshInfo;
     private final IdentifiedUser user;
 
     public ChangeIdValidator(
-        RefControl refControl,
+        ProjectState projectState,
+        IdentifiedUser user,
         String canonicalWebUrl,
         String installCommitMsgHookCommand,
         SshInfo sshInfo) {
-      this.projectControl = refControl.getProjectControl();
+      this.projectState = projectState;
       this.canonicalWebUrl = canonicalWebUrl;
       this.installCommitMsgHookCommand = installCommitMsgHookCommand;
       this.sshInfo = sshInfo;
-      this.user = projectControl.getUser().asIdentifiedUser();
+      this.user = user;
     }
 
     @Override
@@ -238,7 +252,7 @@
       String sha1 = commit.abbreviate(SHA1_LENGTH).name();
 
       if (idList.isEmpty()) {
-        if (projectControl.getProjectState().isRequireChangeID()) {
+        if (projectState.isRequireChangeID()) {
           String shortMsg = commit.getShortMessage();
           if (shortMsg.startsWith(CHANGE_ID_PREFIX)
               && CHANGE_ID
@@ -342,12 +356,15 @@
 
   /** If this is the special project configuration branch, validate the config. */
   public static class ConfigValidator implements CommitValidationListener {
-    private final RefControl refControl;
+    private final Branch.NameKey branch;
+    private final IdentifiedUser user;
     private final RevWalk rw;
     private final AllUsersName allUsers;
 
-    public ConfigValidator(RefControl refControl, RevWalk rw, AllUsersName allUsers) {
-      this.refControl = refControl;
+    public ConfigValidator(
+        Branch.NameKey branch, IdentifiedUser user, RevWalk rw, AllUsersName allUsers) {
+      this.branch = branch;
+      this.user = user;
       this.rw = rw;
       this.allUsers = allUsers;
     }
@@ -355,9 +372,7 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
         throws CommitValidationException {
-      IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser();
-
-      if (REFS_CONFIG.equals(refControl.getRefName())) {
+      if (REFS_CONFIG.equals(branch.get())) {
         List<CommitValidationMessage> messages = new ArrayList<>();
 
         try {
@@ -373,7 +388,7 @@
         } catch (ConfigInvalidException | IOException e) {
           log.error(
               "User "
-                  + currentUser.getUserName()
+                  + user.getUserName()
                   + " tried to push an invalid project configuration "
                   + receiveEvent.command.getNewId().name()
                   + " for project "
@@ -383,10 +398,9 @@
         }
       }
 
-      if (allUsers.equals(refControl.getProjectControl().getProject().getNameKey())
-          && RefNames.isRefsUsers(refControl.getRefName())) {
+      if (allUsers.equals(branch.getParentKey()) && RefNames.isRefsUsers(branch.get())) {
         List<CommitValidationMessage> messages = new ArrayList<>();
-        Account.Id accountId = Account.Id.fromRef(refControl.getRefName());
+        Account.Id accountId = Account.Id.fromRef(branch.get());
         if (accountId != null) {
           try {
             WatchConfig wc = new WatchConfig(accountId);
@@ -401,7 +415,7 @@
           } catch (IOException | ConfigInvalidException e) {
             log.error(
                 "User "
-                    + currentUser.getUserName()
+                    + user.getUserName()
                     + " tried to push an invalid watch configuration "
                     + receiveEvent.command.getNewId().name()
                     + " for account "
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
index 109baa8..27692c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
@@ -87,10 +87,8 @@
   public List<GroupInfo> apply(GroupResource resource, Input input)
       throws MethodNotAllowedException, AuthException, UnprocessableEntityException, OrmException,
           ResourceNotFoundException {
-    AccountGroup group = resource.toAccountGroup();
-    if (group == null) {
-      throw new MethodNotAllowedException();
-    }
+    GroupDescription.Internal group =
+        resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
     input = Input.init(input);
 
     GroupControl control = resource.getControl();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
index 15bcc18..96024d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -111,10 +112,8 @@
   public List<AccountInfo> apply(GroupResource resource, Input input)
       throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
           IOException, ConfigInvalidException, ResourceNotFoundException {
-    AccountGroup internalGroup = resource.toAccountGroup();
-    if (internalGroup == null) {
-      throw new MethodNotAllowedException();
-    }
+    GroupDescription.Internal internalGroup =
+        resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
     input = Input.init(input);
 
     GroupControl control = resource.getControl();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
index 19974c9..804d3e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
@@ -166,8 +166,8 @@
 
   private AccountGroup.Id owner(GroupInput input) throws UnprocessableEntityException {
     if (input.ownerId != null) {
-      GroupDescription.Basic d = groups.parseInternal(Url.decode(input.ownerId));
-      return GroupDescriptions.toAccountGroup(d).getId();
+      GroupDescription.Internal d = groups.parseInternal(Url.decode(input.ownerId));
+      return d.getId();
     }
     return null;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
index ebdb12d..64618c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
@@ -54,10 +54,8 @@
   public Response<?> apply(GroupResource resource, Input input)
       throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
           ResourceNotFoundException {
-    AccountGroup internalGroup = resource.toAccountGroup();
-    if (internalGroup == null) {
-      throw new MethodNotAllowedException();
-    }
+    GroupDescription.Internal internalGroup =
+        resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
     input = Input.init(input);
 
     final GroupControl control = resource.getControl();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
index babff37..1069e1c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -56,10 +57,8 @@
   public Response<?> apply(GroupResource resource, Input input)
       throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
           IOException, ConfigInvalidException, ResourceNotFoundException {
-    AccountGroup internalGroup = resource.toAccountGroup();
-    if (internalGroup == null) {
-      throw new MethodNotAllowedException();
-    }
+    GroupDescription.Internal internalGroup =
+        resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
     input = Input.init(input);
 
     final GroupControl control = resource.getControl();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java
index e2a467a..4ea7041 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java
@@ -64,10 +64,9 @@
   @Override
   public List<? extends GroupAuditEventInfo> apply(GroupResource rsrc)
       throws AuthException, MethodNotAllowedException, OrmException {
-    AccountGroup group = rsrc.toAccountGroup();
-    if (group == null) {
-      throw new MethodNotAllowedException();
-    } else if (!rsrc.getControl().isOwner()) {
+    GroupDescription.Internal group =
+        rsrc.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
+    if (!rsrc.getControl().isOwner()) {
       throw new AuthException("Not group owner");
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java
index 6900b83..0610843 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java
@@ -15,19 +15,17 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.inject.Singleton;
 
 @Singleton
 public class GetDescription implements RestReadView<GroupResource> {
   @Override
   public String apply(GroupResource resource) throws MethodNotAllowedException {
-    AccountGroup group = resource.toAccountGroup();
-    if (group == null) {
-      throw new MethodNotAllowedException();
-    }
+    GroupDescription.Internal group =
+        resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
     return Strings.nullToEmpty(group.getDescription());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
index 464be18..03d0788 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -40,10 +40,8 @@
   @Override
   public GroupInfo apply(GroupResource resource)
       throws MethodNotAllowedException, ResourceNotFoundException, OrmException {
-    AccountGroup group = resource.toAccountGroup();
-    if (group == null) {
-      throw new MethodNotAllowedException();
-    }
+    GroupDescription.Internal group =
+        resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
     try {
       GroupControl c = controlFactory.validateFor(group.getOwnerGroupUUID());
       return json.format(c.getGroup());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
index 0be167d..639ee55 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
@@ -19,7 +19,6 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
@@ -37,8 +36,7 @@
 public class GroupJson {
   public static GroupOptionsInfo createOptions(GroupDescription.Basic group) {
     GroupOptionsInfo options = new GroupOptionsInfo();
-    AccountGroup ag = GroupDescriptions.toAccountGroup(group);
-    if (ag != null && ag.isVisibleToAll()) {
+    if (isInternalGroup(group) && ((GroupDescription.Internal) group).isVisibleToAll()) {
       options.visibleToAll = true;
     }
     return options;
@@ -96,25 +94,30 @@
     info.url = Strings.emptyToNull(group.getUrl());
     info.options = createOptions(group);
 
-    AccountGroup g = GroupDescriptions.toAccountGroup(group);
-    if (g != null) {
-      info.description = Strings.emptyToNull(g.getDescription());
-      info.groupId = g.getId().get();
-      if (g.getOwnerGroupUUID() != null) {
-        info.ownerId = Url.encode(g.getOwnerGroupUUID().get());
-        GroupDescription.Basic o = groupBackend.get(g.getOwnerGroupUUID());
+    if (isInternalGroup(group)) {
+      GroupDescription.Internal internalGroup = (GroupDescription.Internal) group;
+      info.description = Strings.emptyToNull(internalGroup.getDescription());
+      info.groupId = internalGroup.getId().get();
+      AccountGroup.UUID ownerGroupUUID = internalGroup.getOwnerGroupUUID();
+      if (ownerGroupUUID != null) {
+        info.ownerId = Url.encode(ownerGroupUUID.get());
+        GroupDescription.Basic o = groupBackend.get(ownerGroupUUID);
         if (o != null) {
           info.owner = o.getName();
         }
       }
-      info.createdOn = g.getCreatedOn();
+      info.createdOn = internalGroup.getCreatedOn();
     }
 
     return info;
   }
 
+  private static boolean isInternalGroup(GroupDescription.Basic group) {
+    return group instanceof GroupDescription.Internal;
+  }
+
   private GroupInfo initMembersAndIncludes(GroupResource rsrc, GroupInfo info) throws OrmException {
-    if (rsrc.toAccountGroup() == null) {
+    if (!rsrc.isInternalGroup()) {
       return info;
     }
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
index 54fc787..44e770f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
@@ -15,12 +15,11 @@
 package com.google.gerrit.server.group;
 
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.inject.TypeLiteral;
+import java.util.Optional;
 
 public class GroupResource implements RestResource {
   public static final TypeLiteral<RestView<GroupResource>> GROUP_KIND =
@@ -44,12 +43,17 @@
     return getGroup().getName();
   }
 
-  public AccountGroup.UUID getGroupUUID() {
-    return getGroup().getGroupUUID();
+  public boolean isInternalGroup() {
+    GroupDescription.Basic group = getGroup();
+    return group instanceof GroupDescription.Internal;
   }
 
-  public AccountGroup toAccountGroup() {
-    return GroupDescriptions.toAccountGroup(getGroup());
+  public Optional<GroupDescription.Internal> asInternalGroup() {
+    GroupDescription.Basic group = getGroup();
+    if (group instanceof GroupDescription.Internal) {
+      return Optional.of((GroupDescription.Internal) group);
+    }
+    return Optional.empty();
   }
 
   public GroupControl getControl() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
index 397bf08..c8ab8f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
@@ -16,7 +16,6 @@
 
 import com.google.common.collect.ListMultimap;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -142,12 +141,13 @@
    * @throws UnprocessableEntityException thrown if the group ID cannot be resolved, if the group is
    *     not visible to the calling user or if it's an external group
    */
-  public GroupDescription.Basic parseInternal(String id) throws UnprocessableEntityException {
+  public GroupDescription.Internal parseInternal(String id) throws UnprocessableEntityException {
     GroupDescription.Basic group = parse(id);
-    if (GroupDescriptions.toAccountGroup(group) == null) {
-      throw new UnprocessableEntityException(String.format("External Group Not Allowed: %s", id));
+    if (group instanceof GroupDescription.Internal) {
+      return (GroupDescription.Internal) group;
     }
-    return group;
+
+    throw new UnprocessableEntityException(String.format("External Group Not Allowed: %s", id));
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
index 1fecc38..d48acee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.group.AddIncludedGroups.PutIncludedGroup;
 import com.google.gwtorm.server.OrmException;
@@ -67,10 +66,8 @@
   @Override
   public IncludedGroupResource parse(GroupResource resource, IdString id)
       throws MethodNotAllowedException, AuthException, ResourceNotFoundException, OrmException {
-    AccountGroup parent = resource.toAccountGroup();
-    if (parent == null) {
-      throw new MethodNotAllowedException();
-    }
+    GroupDescription.Internal parent =
+        resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
 
     GroupDescription.Basic member =
         groupsCollection.parse(TopLevelResource.INSTANCE, id).getGroup();
@@ -80,7 +77,7 @@
     throw new ResourceNotFoundException(id);
   }
 
-  private boolean isMember(AccountGroup parent, GroupDescription.Basic member)
+  private boolean isMember(GroupDescription.Internal parent, GroupDescription.Basic member)
       throws OrmException, ResourceNotFoundException {
     try {
       return groups.isIncluded(dbProvider.get(), parent.getGroupUUID(), member.getGroupUUID());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/Index.java
index b7b98b2..5d076d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/Index.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/Index.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.group;
 
-import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -44,14 +43,15 @@
       throw new AuthException("not allowed to index group");
     }
 
-    AccountGroup group = GroupDescriptions.toAccountGroup(rsrc.getGroup());
-    if (group == null) {
+    AccountGroup.UUID groupUuid = rsrc.getGroup().getGroupUUID();
+    if (!rsrc.isInternalGroup()) {
       throw new UnprocessableEntityException(
-          String.format("External Group Not Allowed: %s", rsrc.getGroupUUID().get()));
+          String.format("External Group Not Allowed: %s", groupUuid.get()));
     }
 
+    AccountGroup accountGroup = groupCache.get(groupUuid);
     // evicting the group from the cache, reindexes the group
-    groupCache.evict(group);
+    groupCache.evict(accountGroup);
     return Response.none();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
index 9004a8a..33d9b57 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Strings.nullToEmpty;
 
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -51,14 +52,13 @@
 
   @Override
   public List<GroupInfo> apply(GroupResource rsrc) throws MethodNotAllowedException, OrmException {
-    if (rsrc.toAccountGroup() == null) {
-      throw new MethodNotAllowedException();
-    }
+    GroupDescription.Internal group =
+        rsrc.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
 
     boolean ownerOfParent = rsrc.getControl().isOwner();
     List<GroupInfo> included = new ArrayList<>();
     Collection<AccountGroup.UUID> includedGroupUuids =
-        groupIncludeCache.subgroupsOf(rsrc.toAccountGroup().getGroupUUID());
+        groupIncludeCache.subgroupsOf(group.getGroupUUID());
     for (AccountGroup.UUID includedGroupUuid : includedGroupUuids) {
       try {
         GroupControl i = controlFactory.controlFor(includedGroupUuid);
@@ -69,7 +69,7 @@
         log.warn(
             String.format(
                 "Group %s no longer available, included into %s",
-                includedGroupUuid, rsrc.getGroup().getName()));
+                includedGroupUuid, group.getName()));
         continue;
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
index 3d383a0..0f8aa40 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -61,11 +62,9 @@
   @Override
   public List<AccountInfo> apply(GroupResource resource)
       throws MethodNotAllowedException, OrmException {
-    if (resource.toAccountGroup() == null) {
-      throw new MethodNotAllowedException();
-    }
-
-    return apply(resource.getGroupUUID());
+    GroupDescription.Internal group =
+        resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
+    return apply(group.getGroupUUID());
   }
 
   public List<AccountInfo> apply(AccountGroup group) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
index 66f5b26..4c0d458 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsCreate;
@@ -71,10 +72,8 @@
   public MemberResource parse(GroupResource parent, IdString id)
       throws MethodNotAllowedException, AuthException, ResourceNotFoundException, OrmException,
           IOException, ConfigInvalidException {
-    AccountGroup group = parent.toAccountGroup();
-    if (group == null) {
-      throw new MethodNotAllowedException();
-    }
+    GroupDescription.Internal group =
+        parent.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
 
     IdentifiedUser user = accounts.parse(TopLevelResource.INSTANCE, id).getUser();
     if (parent.getControl().canSeeMember(user.getAccountId()) && isMember(group, user)) {
@@ -83,7 +82,7 @@
     throw new ResourceNotFoundException(id);
   }
 
-  private boolean isMember(AccountGroup group, IdentifiedUser user)
+  private boolean isMember(GroupDescription.Internal group, IdentifiedUser user)
       throws OrmException, ResourceNotFoundException {
     AccountGroup.UUID groupUuid = group.getGroupUUID();
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java
index f0709b4..3d6feea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -56,16 +57,15 @@
       input = new Input(); // Delete would set description to null.
     }
 
-    AccountGroup accountGroup = resource.toAccountGroup();
-    if (accountGroup == null) {
-      throw new MethodNotAllowedException();
-    } else if (!resource.getControl().isOwner()) {
+    GroupDescription.Internal internalGroup =
+        resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
+    if (!resource.getControl().isOwner()) {
       throw new AuthException("Not group owner");
     }
 
     String newDescription = Strings.emptyToNull(input.description);
-    if (!Objects.equals(accountGroup.getDescription(), newDescription)) {
-      AccountGroup.UUID groupUuid = resource.getGroupUUID();
+    if (!Objects.equals(internalGroup.getDescription(), newDescription)) {
+      AccountGroup.UUID groupUuid = internalGroup.getGroupUUID();
       try {
         groupsUpdateProvider
             .get()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
index 9ef8d8727..75a7eb5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -52,9 +53,9 @@
   public String apply(GroupResource rsrc, Input input)
       throws MethodNotAllowedException, AuthException, BadRequestException,
           ResourceConflictException, ResourceNotFoundException, OrmException, IOException {
-    if (rsrc.toAccountGroup() == null) {
-      throw new MethodNotAllowedException();
-    } else if (!rsrc.getControl().isOwner()) {
+    GroupDescription.Internal internalGroup =
+        rsrc.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
+    if (!rsrc.getControl().isOwner()) {
       throw new AuthException("Not group owner");
     } else if (input == null || Strings.isNullOrEmpty(input.name)) {
       throw new BadRequestException("name is required");
@@ -64,15 +65,15 @@
       throw new BadRequestException("name is required");
     }
 
-    if (rsrc.toAccountGroup().getName().equals(newName)) {
+    if (internalGroup.getName().equals(newName)) {
       return newName;
     }
 
-    renameGroup(rsrc.toAccountGroup(), newName);
+    renameGroup(internalGroup, newName);
     return newName;
   }
 
-  private void renameGroup(AccountGroup group, String newName)
+  private void renameGroup(GroupDescription.Internal group, String newName)
       throws ResourceConflictException, ResourceNotFoundException, OrmException, IOException {
     AccountGroup.UUID groupUuid = group.getGroupUUID();
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java
index 69ce64b..1ea018f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -44,10 +45,9 @@
   public GroupOptionsInfo apply(GroupResource resource, GroupOptionsInfo input)
       throws MethodNotAllowedException, AuthException, BadRequestException,
           ResourceNotFoundException, OrmException, IOException {
-    AccountGroup accountGroup = resource.toAccountGroup();
-    if (accountGroup == null) {
-      throw new MethodNotAllowedException();
-    } else if (!resource.getControl().isOwner()) {
+    GroupDescription.Internal internalGroup =
+        resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
+    if (!resource.getControl().isOwner()) {
       throw new AuthException("Not group owner");
     }
 
@@ -58,8 +58,8 @@
       input.visibleToAll = false;
     }
 
-    if (accountGroup.isVisibleToAll() != input.visibleToAll) {
-      AccountGroup.UUID groupUuid = accountGroup.getGroupUUID();
+    if (internalGroup.isVisibleToAll() != input.visibleToAll) {
+      AccountGroup.UUID groupUuid = internalGroup.getGroupUUID();
       try {
         groupsUpdateProvider
             .get()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
index 6dd0809..20e1dbe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
@@ -61,10 +61,9 @@
   public GroupInfo apply(GroupResource resource, Input input)
       throws ResourceNotFoundException, MethodNotAllowedException, AuthException,
           BadRequestException, UnprocessableEntityException, OrmException, IOException {
-    AccountGroup accountGroup = resource.toAccountGroup();
-    if (accountGroup == null) {
-      throw new MethodNotAllowedException();
-    } else if (!resource.getControl().isOwner()) {
+    GroupDescription.Internal internalGroup =
+        resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
+    if (!resource.getControl().isOwner()) {
       throw new AuthException("Not group owner");
     }
 
@@ -73,8 +72,8 @@
     }
 
     GroupDescription.Basic owner = groupsCollection.parse(input.owner);
-    if (!accountGroup.getOwnerGroupUUID().equals(owner.getGroupUUID())) {
-      AccountGroup.UUID groupUuid = resource.getGroupUUID();
+    if (!internalGroup.getOwnerGroupUUID().equals(owner.getGroupUUID())) {
+      AccountGroup.UUID groupUuid = internalGroup.getGroupUUID();
       try {
         groupsUpdateProvider
             .get()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/RefState.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/RefState.java
new file mode 100644
index 0000000..b55afb6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/RefState.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2017 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.index;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Project;
+import java.io.IOException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+@AutoValue
+public abstract class RefState {
+  public static RefState create(String ref, String sha) {
+    return new AutoValue_RefState(ref, ObjectId.fromString(sha));
+  }
+
+  public static RefState create(String ref, @Nullable ObjectId id) {
+    return new AutoValue_RefState(ref, firstNonNull(id, ObjectId.zeroId()));
+  }
+
+  public static RefState of(Ref ref) {
+    return new AutoValue_RefState(ref.getName(), ref.getObjectId());
+  }
+
+  public byte[] toByteArray(Project.NameKey project) {
+    byte[] a = (project.toString() + ':' + ref() + ':').getBytes(UTF_8);
+    byte[] b = new byte[a.length + Constants.OBJECT_ID_STRING_LENGTH];
+    System.arraycopy(a, 0, b, 0, a.length);
+    id().copyTo(b, a.length);
+    return b;
+  }
+
+  public static void check(boolean condition, String str) {
+    checkArgument(condition, "invalid RefState: %s", str);
+  }
+
+  public abstract String ref();
+
+  public abstract ObjectId id();
+
+  public boolean match(Repository repo) throws IOException {
+    Ref ref = repo.exactRef(ref());
+    ObjectId expected = ref != null ? ref.getObjectId() : ObjectId.zeroId();
+    return id().equals(expected);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
index b7c5e77..5e12c12 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
@@ -17,20 +17,26 @@
 import static com.google.gerrit.index.FieldDef.exact;
 import static com.google.gerrit.index.FieldDef.integer;
 import static com.google.gerrit.index.FieldDef.prefix;
+import static com.google.gerrit.index.FieldDef.storedOnly;
 import static com.google.gerrit.index.FieldDef.timestamp;
+import static java.util.stream.Collectors.toSet;
 
 import com.google.common.base.Predicates;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.SchemaUtil;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.index.RefState;
 import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.Locale;
 import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
 
 /** Secondary index schemas for accounts. */
 public class AccountField {
@@ -84,6 +90,9 @@
                 return preferredEmail != null ? preferredEmail.toLowerCase() : null;
               });
 
+  public static final FieldDef<AccountState, String> PREFERRED_EMAIL_EXACT =
+      exact("preferredemail_exact").build(a -> a.getAccount().getPreferredEmail());
+
   public static final FieldDef<AccountState, Timestamp> REGISTERED =
       timestamp("registered").build(a -> a.getAccount().getRegisteredOn());
 
@@ -98,5 +107,43 @@
                       .transform(k -> k.project().get())
                       .toSet());
 
+  /**
+   * All values of all refs that were used in the course of indexing this document, except the
+   * refs/meta/external-ids notes branch which is handled specially (see {@link
+   * #EXTERNAL_ID_STATE}).
+   *
+   * <p>Emitted as UTF-8 encoded strings of the form {@code project:ref/name:[hex sha]}.
+   */
+  public static final FieldDef<AccountState, Iterable<byte[]>> REF_STATE =
+      storedOnly("ref_state")
+          .buildRepeatable(
+              a -> {
+                if (a.getAccount().getMetaId() == null) {
+                  return ImmutableList.of();
+                }
+
+                return ImmutableList.of(
+                    RefState.create(
+                            RefNames.refsUsers(a.getAccount().getId()),
+                            ObjectId.fromString(a.getAccount().getMetaId()))
+                        .toByteArray(a.getAllUsersNameForIndexing()));
+              });
+
+  /**
+   * All note values of all external IDs that were used in the course of indexing this document.
+   *
+   * <p>Emitted as UTF-8 encoded strings of the form {@code [hex sha of external ID]:[hex sha of
+   * note blob]}, or with other words {@code [note ID]:[note data ID]}.
+   */
+  public static final FieldDef<AccountState, Iterable<byte[]>> EXTERNAL_ID_STATE =
+      storedOnly("external_id_state")
+          .buildRepeatable(
+              a ->
+                  a.getExternalIds()
+                      .stream()
+                      .filter(e -> e.blobId() != null)
+                      .map(e -> e.toByteArray())
+                      .collect(toSet()));
+
   private AccountField() {}
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
index 67b507d..2a14f9b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
@@ -18,13 +18,11 @@
 import com.google.gerrit.index.IndexCollection;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountState;
-import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
 public class AccountIndexCollection
     extends IndexCollection<Account.Id, AccountState, AccountIndex> {
-  @Inject
   @VisibleForTesting
   public AccountIndexCollection() {}
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
index 8f9b443..dcdf9e3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
@@ -34,7 +34,13 @@
           AccountField.USERNAME,
           AccountField.WATCHED_PROJECT);
 
-  static final Schema<AccountState> V5 = schema(V4, AccountField.PREFERRED_EMAIL);
+  @Deprecated static final Schema<AccountState> V5 = schema(V4, AccountField.PREFERRED_EMAIL);
+
+  @Deprecated
+  static final Schema<AccountState> V6 =
+      schema(V5, AccountField.REF_STATE, AccountField.EXTERNAL_ID_STATE);
+
+  static final Schema<AccountState> V7 = schema(V6, AccountField.PREFERRED_EMAIL_EXACT);
 
   public static final String NAME = "accounts";
   public static final AccountSchemaDefinitions INSTANCE = new AccountSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index cc8f9be..a1c7f14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -50,7 +50,7 @@
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.index.change.StalenessChecker.RefState;
+import com.google.gerrit.server.index.RefState;
 import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.ChangeNotes;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java
index 5ce361f..a353a2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java
@@ -18,12 +18,10 @@
 import com.google.gerrit.index.IndexCollection;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
 public class ChangeIndexCollection extends IndexCollection<Change.Id, ChangeData, ChangeIndex> {
-  @Inject
   @VisibleForTesting
   public ChangeIndexCollection() {}
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
index df92379..e804702 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.index.change;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -35,6 +34,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.RefState;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -47,8 +47,6 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
-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.slf4j.Logger;
@@ -221,43 +219,6 @@
     }
   }
 
-  @AutoValue
-  public abstract static class RefState {
-    static RefState create(String ref, String sha) {
-      return new AutoValue_StalenessChecker_RefState(ref, ObjectId.fromString(sha));
-    }
-
-    static RefState create(String ref, @Nullable ObjectId id) {
-      return new AutoValue_StalenessChecker_RefState(ref, firstNonNull(id, ObjectId.zeroId()));
-    }
-
-    static RefState of(Ref ref) {
-      return new AutoValue_StalenessChecker_RefState(ref.getName(), ref.getObjectId());
-    }
-
-    byte[] toByteArray(Project.NameKey project) {
-      byte[] a = (project.toString() + ':' + ref() + ':').getBytes(UTF_8);
-      byte[] b = new byte[a.length + Constants.OBJECT_ID_STRING_LENGTH];
-      System.arraycopy(a, 0, b, 0, a.length);
-      id().copyTo(b, a.length);
-      return b;
-    }
-
-    private static void check(boolean condition, String str) {
-      checkArgument(condition, "invalid RefState: %s", str);
-    }
-
-    abstract String ref();
-
-    abstract ObjectId id();
-
-    private boolean match(Repository repo) throws IOException {
-      Ref ref = repo.exactRef(ref());
-      ObjectId expected = ref != null ? ref.getObjectId() : ObjectId.zeroId();
-      return id().equals(expected);
-    }
-  }
-
   /**
    * Pattern for matching refs.
    *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
index 5c49ee5..5ce65a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
@@ -17,13 +17,11 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.index.IndexCollection;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
 public class GroupIndexCollection
     extends IndexCollection<AccountGroup.UUID, AccountGroup, GroupIndex> {
-  @Inject
   @VisibleForTesting
   public GroupIndexCollection() {}
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index 278cd86..d1434bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -16,7 +16,6 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.index.query.Predicate;
@@ -175,12 +174,12 @@
         continue;
       }
 
-      AccountGroup ig = GroupDescriptions.toAccountGroup(group);
-      if (ig == null) {
+      if (!(group instanceof GroupDescription.Internal)) {
         // Non-internal groups cannot be expanded by the server.
         continue;
       }
 
+      GroupDescription.Internal ig = (GroupDescription.Internal) group;
       try {
         args.groups.getMembers(db, ig.getGroupUUID()).forEach(matching.accounts::add);
       } catch (NoSuchGroupException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
index b43dc16..16ede58 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
@@ -105,8 +105,6 @@
     this.patches = patches;
   }
 
-  protected PatchList() {}
-
   /** Old side tree or commit; null only if this is a combined diff. */
   @Nullable
   public ObjectId getOldId() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index be5a7aa..b985723 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -19,7 +19,6 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.Cache;
-import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.UncheckedExecutionException;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
@@ -31,7 +30,6 @@
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
-import java.util.List;
 import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.lib.Config;
@@ -198,15 +196,11 @@
     private static final long serialVersionUID = 1L;
 
     @VisibleForTesting
-    public LargeObjectTombstone() {}
-
-    /**
-     * Return an empty list to prevent {@link NullPointerException}s inside of {@link
-     * PatchListWeigher}.
-     */
-    @Override
-    public List<PatchListEntry> getPatches() {
-      return ImmutableList.of();
+    public LargeObjectTombstone() {
+      // Initialize super class with valid values. We don't care about the inner state, but need to
+      // pass valid values that don't break (de)serialization.
+      super(
+          null, ObjectId.zeroId(), false, ComparisonType.againstAutoMerge(), new PatchListEntry[0]);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 9baf01e..78bd167 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -95,20 +95,6 @@
         throw new NoSuchChangeException(notes.getChangeId(), e);
       }
     }
-
-    public ChangeControl validateFor(ReviewDb db, Change.Id changeId, CurrentUser user)
-        throws OrmException {
-      return validateFor(db, notesFactory.createChecked(changeId), user);
-    }
-
-    public ChangeControl validateFor(ReviewDb db, ChangeNotes notes, CurrentUser user)
-        throws OrmException {
-      ChangeControl c = controlFor(notes, user);
-      if (!c.isVisible(db)) {
-        throw new NoSuchChangeException(c.getId());
-      }
-      return c;
-    }
   }
 
   @Singleton
@@ -244,6 +230,7 @@
 
   /** Can this user publish this draft change or any draft patch set of this change? */
   public boolean canPublish(ReviewDb db) throws OrmException {
+    // TODO(hiesel) These don't need to be migrated, just remove after support for drafts is removed
     return (isOwner() || getRefControl().canPublishDrafts()) && isVisible(db);
   }
 
@@ -350,7 +337,7 @@
   }
 
   /** Is this user the owner of the change? */
-  private boolean isOwner() {
+  boolean isOwner() {
     if (getUser().isIdentifiedUser()) {
       Account.Id id = getUser().asIdentifiedUser().getAccountId();
       return id.equals(getChange().getOwner());
@@ -377,40 +364,6 @@
     return false;
   }
 
-  /** @return true if the user is allowed to remove this reviewer. */
-  public boolean canRemoveReviewer(PatchSetApproval approval) {
-    return canRemoveReviewer(approval.getAccountId(), approval.getValue());
-  }
-
-  public boolean canRemoveReviewer(Account.Id reviewer, int value) {
-    if (getChange().getStatus().isOpen()) {
-      // A user can always remove themselves.
-      //
-      if (getUser().isIdentifiedUser()) {
-        if (getUser().getAccountId().equals(reviewer)) {
-          return true; // can remove self
-        }
-      }
-
-      // The change owner may remove any zero or positive score.
-      //
-      if (isOwner() && 0 <= value) {
-        return true;
-      }
-
-      // Users with the remove reviewer permission, the branch owner, project
-      // owner and site admin can remove anyone
-      if (getRefControl().canRemoveReviewer() // has removal permissions
-          || getRefControl().isOwner() // branch owner
-          || getProjectControl().isOwner() // project owner
-          || getProjectControl().isAdmin()) {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
   /** Can this user edit the topic name? */
   private boolean canEditTopicName() {
     if (getChange().getStatus().isOpen()) {
@@ -459,6 +412,7 @@
   }
 
   public boolean isDraftVisible(ReviewDb db, ChangeData cd) throws OrmException {
+    // TODO(hiesel) These don't need to be migrated, just remove after support for drafts is removed
     return isOwner()
         || isReviewer(db, cd)
         || getRefControl().canViewDrafts()
@@ -571,7 +525,7 @@
           case SUBMIT:
             return getRefControl().canSubmit(isOwner());
 
-          case REMOVE_REVIEWER: // TODO Honor specific removal filters?
+          case REMOVE_REVIEWER:
           case SUBMIT_AS:
             return getRefControl().canPerform(perm.permissionName().get());
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
index 87bea15..34c3287 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
@@ -58,6 +58,7 @@
     InheritedBooleanInfo enableSignedPush = new InheritedBooleanInfo();
     InheritedBooleanInfo requireSignedPush = new InheritedBooleanInfo();
     InheritedBooleanInfo rejectImplicitMerges = new InheritedBooleanInfo();
+    InheritedBooleanInfo privateByDefault = new InheritedBooleanInfo();
     InheritedBooleanInfo enableReviewerByEmail = new InheritedBooleanInfo();
     InheritedBooleanInfo matchAuthorToCommitterDate = new InheritedBooleanInfo();
 
@@ -75,6 +76,7 @@
     enableSignedPush.configuredValue = p.getEnableSignedPush();
     requireSignedPush.configuredValue = p.getRequireSignedPush();
     rejectImplicitMerges.configuredValue = p.getRejectImplicitMerges();
+    privateByDefault.configuredValue = p.getPrivateByDefault();
     enableReviewerByEmail.configuredValue = p.getEnableReviewerByEmail();
     matchAuthorToCommitterDate.configuredValue = p.getMatchAuthorToCommitterDate();
 
@@ -88,6 +90,7 @@
           parentState.isCreateNewChangeForAllNotInTarget();
       enableSignedPush.inheritedValue = projectState.isEnableSignedPush();
       requireSignedPush.inheritedValue = projectState.isRequireSignedPush();
+      privateByDefault.inheritedValue = projectState.isPrivateByDefault();
       rejectImplicitMerges.inheritedValue = projectState.isRejectImplicitMerges();
       enableReviewerByEmail.inheritedValue = projectState.isEnableReviewerByEmail();
       matchAuthorToCommitterDate.inheritedValue = projectState.isMatchAuthorToCommitterDate();
@@ -105,6 +108,7 @@
       this.enableSignedPush = enableSignedPush;
       this.requireSignedPush = requireSignedPush;
     }
+    this.privateByDefault = privateByDefault;
 
     MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
     maxObjectSizeLimit.value =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
index 4e2e327..77fb86b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Inject;
@@ -55,6 +56,7 @@
   private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated referenceUpdated;
   private final RefValidationHelper refCreationValidator;
+  private final CreateRefControl createRefControl;
   private String ref;
 
   @Inject
@@ -64,18 +66,21 @@
       GitRepositoryManager repoManager,
       GitReferenceUpdated referenceUpdated,
       RefValidationHelper.Factory refHelperFactory,
+      CreateRefControl createRefControl,
       @Assisted String ref) {
     this.identifiedUser = identifiedUser;
     this.permissionBackend = permissionBackend;
     this.repoManager = repoManager;
     this.referenceUpdated = referenceUpdated;
     this.refCreationValidator = refHelperFactory.create(ReceiveCommand.Type.CREATE);
+    this.createRefControl = createRefControl;
     this.ref = ref;
   }
 
   @Override
   public BranchInfo apply(ProjectResource rsrc, BranchInput input)
-      throws BadRequestException, AuthException, ResourceConflictException, IOException {
+      throws BadRequestException, AuthException, ResourceConflictException, IOException,
+          PermissionBackendException, NoSuchProjectException {
     if (input == null) {
       input = new BranchInput();
     }
@@ -100,7 +105,6 @@
     }
 
     final Branch.NameKey name = new Branch.NameKey(rsrc.getNameKey(), ref);
-    final RefControl refControl = rsrc.getControl().controlForRef(name);
     try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
       ObjectId revid = RefUtil.parseBaseRevision(repo, rsrc.getNameKey(), input.revision);
       RevWalk rw = RefUtil.verifyConnected(repo, revid);
@@ -117,7 +121,7 @@
         }
       }
 
-      String rejectReason = refControl.canCreate(repo, object);
+      String rejectReason = createRefControl.canCreateRef(repo, object, identifiedUser.get(), name);
       if (rejectReason != null) {
         throw new AuthException("Cannot create \"" + ref + "\": " + rejectReason);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateRefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateRefControl.java
new file mode 100644
index 0000000..aa48a73
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateRefControl.java
@@ -0,0 +1,177 @@
+// Copyright (C) 2017 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.project;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
+import java.io.IOException;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Manages access control for creating Git references (aka branches, tags). */
+@Singleton
+public class CreateRefControl {
+  private static final Logger log = LoggerFactory.getLogger(CreateRefControl.class);
+
+  private final PermissionBackend permissionBackend;
+  private final ProjectCache projectCache;
+
+  @Inject
+  CreateRefControl(PermissionBackend permissionBackend, ProjectCache projectCache) {
+    this.permissionBackend = permissionBackend;
+    this.projectCache = projectCache;
+  }
+
+  /**
+   * Determines whether the user can create a new Git ref.
+   *
+   * @param repo repository on which user want to create
+   * @param object the object the user will start the reference with
+   * @param user the current identified user
+   * @param branch the branch the new {@link RevObject} should be created on
+   * @return {@code null} if the user specified can create a new Git ref, or a String describing why
+   *     the creation is not allowed.
+   * @throws PermissionBackendException on failure of permission checks
+   */
+  @Nullable
+  public String canCreateRef(
+      Repository repo, RevObject object, IdentifiedUser user, Branch.NameKey branch)
+      throws PermissionBackendException, NoSuchProjectException, IOException {
+    ProjectState ps = projectCache.checkedGet(branch.getParentKey());
+    if (ps == null) {
+      throw new NoSuchProjectException(branch.getParentKey());
+    }
+    if (!ps.getProject().getState().permitsWrite()) {
+      return "project state does not permit write";
+    }
+
+    PermissionBackend.ForRef perm = permissionBackend.user(user).ref(branch);
+    if (object instanceof RevCommit) {
+      if (!testAuditLogged(perm, RefPermission.CREATE)) {
+        return user.getAccountId() + " lacks permission: " + Permission.CREATE;
+      }
+      return canCreateCommit(repo, (RevCommit) object, ps, user, perm);
+    } else if (object instanceof RevTag) {
+      final RevTag tag = (RevTag) object;
+      try (RevWalk rw = new RevWalk(repo)) {
+        rw.parseBody(tag);
+      } catch (IOException e) {
+        String msg =
+            String.format("RevWalk(%s) for pushing tag %s:", branch.getParentKey(), tag.name());
+        log.error(msg, e);
+
+        return "I/O exception for revwalk";
+      }
+
+      // If tagger is present, require it matches the user's email.
+      //
+      final PersonIdent tagger = tag.getTaggerIdent();
+      if (tagger != null) {
+        boolean valid;
+        if (user.isIdentifiedUser()) {
+          final String addr = tagger.getEmailAddress();
+          valid = user.asIdentifiedUser().hasEmailAddress(addr);
+        } else {
+          valid = false;
+        }
+        if (!valid && !testAuditLogged(perm, RefPermission.FORGE_COMMITTER)) {
+          return user.getAccountId() + " lacks permission: " + Permission.FORGE_COMMITTER;
+        }
+      }
+
+      RevObject tagObject = tag.getObject();
+      if (tagObject instanceof RevCommit) {
+        String rejectReason = canCreateCommit(repo, (RevCommit) tagObject, ps, user, perm);
+        if (rejectReason != null) {
+          return rejectReason;
+        }
+      } else {
+        String rejectReason = canCreateRef(repo, tagObject, user, branch);
+        if (rejectReason != null) {
+          return rejectReason;
+        }
+      }
+
+      // If the tag has a PGP signature, allow a lower level of permission
+      // than if it doesn't have a PGP signature.
+      //
+      RefControl refControl = ps.controlFor(user).controlForRef(branch);
+      if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
+        return refControl.canPerform(Permission.CREATE_SIGNED_TAG)
+            ? null
+            : user.getAccountId() + " lacks permission: " + Permission.CREATE_SIGNED_TAG;
+      }
+      return refControl.canPerform(Permission.CREATE_TAG)
+          ? null
+          : user.getAccountId() + " lacks permission " + Permission.CREATE_TAG;
+    }
+
+    return null;
+  }
+
+  /**
+   * Check if the user is allowed to create a new commit object if this introduces a new commit to
+   * the project. If not allowed, returns a string describing why it's not allowed. The userId
+   * argument is only used for the error message.
+   */
+  @Nullable
+  private String canCreateCommit(
+      Repository repo,
+      RevCommit commit,
+      ProjectState projectState,
+      IdentifiedUser user,
+      PermissionBackend.ForRef forRef)
+      throws PermissionBackendException {
+    if (projectState.controlFor(user).isReachableFromHeadsOrTags(repo, commit)) {
+      // If the user has no push permissions, check whether the object is
+      // merged into a branch or tag readable by this user. If so, they are
+      // not effectively "pushing" more objects, so they can create the ref
+      // even if they don't have push permission.
+      return null;
+    } else if (testAuditLogged(forRef, RefPermission.UPDATE)) {
+      // If the user has push permissions, they can create the ref regardless
+      // of whether they are pushing any new objects along with the create.
+      return null;
+    }
+    return user.getAccountId()
+        + " lacks permission "
+        + Permission.PUSH
+        + " for creating new commit object";
+  }
+
+  private boolean testAuditLogged(PermissionBackend.ForRef forRef, RefPermission p)
+      throws PermissionBackendException {
+    try {
+      forRef.check(p);
+    } catch (AuthException e) {
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 6e17b2c..58421b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -394,6 +394,10 @@
     return getInheritableBoolean(Project::getRejectImplicitMerges);
   }
 
+  public boolean isPrivateByDefault() {
+    return getInheritableBoolean(Project::getPrivateByDefault);
+  }
+
   public boolean isEnableReviewerByEmail() {
     return getInheritableBoolean(Project::getEnableReviewerByEmail);
   }
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 aa4e488..b0fa036 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
@@ -147,6 +147,10 @@
         p.setRejectImplicitMerges(input.rejectImplicitMerges);
       }
 
+      if (input.privateByDefault != null) {
+        p.setPrivateByDefault(input.privateByDefault);
+      }
+
       if (input.maxObjectSizeLimit != null) {
         p.setMaxObjectSizeLimit(input.maxObjectSizeLimit);
       }
@@ -258,7 +262,7 @@
                               value,
                               v.getKey()));
                     }
-                    //$FALL-THROUGH$
+                    // $FALL-THROUGH$
                   case STRING:
                     cfg.setString(v.getKey(), value);
                     break;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 16c820f..a749759 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -16,11 +16,9 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
@@ -34,7 +32,6 @@
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -44,19 +41,9 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTag;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Manages access control for Git references (aka branches, tags). */
 public class RefControl {
-  private static final Logger log = LoggerFactory.getLogger(RefControl.class);
-
   private final ProjectControl projectControl;
   private final String refName;
 
@@ -209,12 +196,11 @@
   }
 
   private boolean isProjectStatePermittingWrite() {
-    return getProjectControl().getProject().getState().equals(ProjectState.ACTIVE);
+    return getProjectControl().getProject().getState().permitsWrite();
   }
 
   private boolean isProjectStatePermittingRead() {
-    return getProjectControl().getProject().getState().equals(ProjectState.READ_ONLY)
-        || isProjectStatePermittingWrite();
+    return getProjectControl().getProject().getState().permitsRead();
   }
 
   private boolean canPushWithForce() {
@@ -231,108 +217,6 @@
   }
 
   /**
-   * Determines whether the user can create a new Git ref.
-   *
-   * @param repo repository on which user want to create
-   * @param object the object the user will start the reference with.
-   * @return {@code null} if the user specified can create a new Git ref, or a String describing why
-   *     the creation is not allowed.
-   */
-  @Nullable
-  public String canCreate(Repository repo, RevObject object) {
-    if (!isProjectStatePermittingWrite()) {
-      return "project state does not permit write";
-    }
-
-    String userId =
-        getUser().isIdentifiedUser() ? "account " + getUser().getAccountId() : "anonymous user";
-
-    if (object instanceof RevCommit) {
-      if (!canPerform(Permission.CREATE)) {
-        return userId + " lacks permission: " + Permission.CREATE;
-      }
-      return canCreateCommit(repo, (RevCommit) object, userId);
-    } else if (object instanceof RevTag) {
-      final RevTag tag = (RevTag) object;
-      try (RevWalk rw = new RevWalk(repo)) {
-        rw.parseBody(tag);
-      } catch (IOException e) {
-        String msg =
-            String.format(
-                "RevWalk(%s) for pushing tag %s:",
-                projectControl.getProject().getNameKey(), tag.name());
-        log.error(msg, e);
-
-        return "I/O exception for revwalk";
-      }
-
-      // If tagger is present, require it matches the user's email.
-      //
-      final PersonIdent tagger = tag.getTaggerIdent();
-      if (tagger != null) {
-        boolean valid;
-        if (getUser().isIdentifiedUser()) {
-          final String addr = tagger.getEmailAddress();
-          valid = getUser().asIdentifiedUser().hasEmailAddress(addr);
-        } else {
-          valid = false;
-        }
-        if (!valid && !canForgeCommitter()) {
-          return userId + " lacks permission: " + Permission.FORGE_COMMITTER;
-        }
-      }
-
-      RevObject tagObject = tag.getObject();
-      if (tagObject instanceof RevCommit) {
-        String rejectReason = canCreateCommit(repo, (RevCommit) tagObject, userId);
-        if (rejectReason != null) {
-          return rejectReason;
-        }
-      } else {
-        String rejectReason = canCreate(repo, tagObject);
-        if (rejectReason != null) {
-          return rejectReason;
-        }
-      }
-
-      // If the tag has a PGP signature, allow a lower level of permission
-      // than if it doesn't have a PGP signature.
-      //
-      if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
-        return canPerform(Permission.CREATE_SIGNED_TAG)
-            ? null
-            : userId + " lacks permission: " + Permission.CREATE_SIGNED_TAG;
-      }
-      return canPerform(Permission.CREATE_TAG)
-          ? null
-          : userId + " lacks permission " + Permission.CREATE_TAG;
-    }
-
-    return null;
-  }
-
-  /**
-   * Check if the user is allowed to create a new commit object if this introduces a new commit to
-   * the project. If not allowed, returns a string describing why it's not allowed. The userId
-   * argument is only used for the error message.
-   */
-  @Nullable
-  private String canCreateCommit(Repository repo, RevCommit commit, String userId) {
-    if (canUpdate()) {
-      // If the user has push permissions, they can create the ref regardless
-      // of whether they are pushing any new objects along with the create.
-      return null;
-    } else if (projectControl.isReachableFromHeadsOrTags(repo, commit)) {
-      // If the user has no push permissions, check whether the object is
-      // merged into a branch or tag readable by this user. If so, they are
-      // not effectively "pushing" more objects, so they can create the ref
-      // even if they don't have push permission.
-      return null;
-    }
-    return userId + " lacks permission " + Permission.PUSH + " for creating new commit object";
-  }
-
-  /**
    * Determines whether the user can delete the Git ref controlled by this object.
    *
    * @return {@code true} if the user specified can delete a Git ref.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java
new file mode 100644
index 0000000..591fcc2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2017 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.project;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class RemoveReviewerControl {
+  private final PermissionBackend permissionBackend;
+  private final Provider<ReviewDb> dbProvider;
+  private final ChangeControl.GenericFactory changeControlFactory;
+
+  @Inject
+  RemoveReviewerControl(
+      PermissionBackend permissionBackend,
+      Provider<ReviewDb> dbProvider,
+      ChangeControl.GenericFactory changeControlFactory) {
+    this.permissionBackend = permissionBackend;
+    this.dbProvider = dbProvider;
+    this.changeControlFactory = changeControlFactory;
+  }
+
+  /** @throws AuthException if this user is not allowed to remove this approval. */
+  public void checkRemoveReviewer(
+      ChangeNotes notes, CurrentUser currentUser, PatchSetApproval approval)
+      throws PermissionBackendException, AuthException, NoSuchChangeException {
+    if (canRemoveReviewerWithoutPermissionCheck(
+        notes, currentUser, approval.getAccountId(), approval.getValue())) {
+      return;
+    }
+
+    permissionBackend
+        .user(currentUser)
+        .change(notes)
+        .database(dbProvider)
+        .check(ChangePermission.REMOVE_REVIEWER);
+  }
+
+  /** @return true if the user is allowed to remove this reviewer. */
+  public boolean testRemoveReviewer(
+      ChangeNotes notes, CurrentUser currentUser, Account.Id reviewer, int value)
+      throws PermissionBackendException, NoSuchChangeException {
+    if (canRemoveReviewerWithoutPermissionCheck(notes, currentUser, reviewer, value)) {
+      return true;
+    }
+    return permissionBackend
+        .user(currentUser)
+        .change(notes)
+        .database(dbProvider)
+        .test(ChangePermission.REMOVE_REVIEWER);
+  }
+
+  private boolean canRemoveReviewerWithoutPermissionCheck(
+      ChangeNotes notes, CurrentUser currentUser, Account.Id reviewer, int value)
+      throws NoSuchChangeException {
+    ChangeControl changeControl = changeControlFactory.controlFor(notes, currentUser);
+    if (!changeControl.getChange().getStatus().isOpen()) {
+      return false;
+    }
+    // A user can always remove themselves.
+    if (changeControl.getUser().isIdentifiedUser()) {
+      if (changeControl.getUser().getAccountId().equals(reviewer)) {
+        return true; // can remove self
+      }
+    }
+    // The change owner may remove any zero or positive score.
+    if (changeControl.isOwner() && 0 <= value) {
+      return true;
+    }
+    // Users with the remove reviewer permission, the branch owner, project
+    // owner and site admin can remove anyone
+    if (changeControl.getRefControl().isOwner() // branch owner
+        || changeControl.getProjectControl().isOwner() // project owner
+        || changeControl.getProjectControl().isAdmin()) { // project admin
+      return true;
+    }
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
index d6552e2..9213353 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -66,6 +66,11 @@
         email.toLowerCase());
   }
 
+  public static Predicate<AccountState> preferredEmailExact(String email) {
+    return new AccountPredicate(
+        AccountField.PREFERRED_EMAIL_EXACT, AccountQueryBuilder.FIELD_PREFERRED_EMAIL_EXACT, email);
+  }
+
   public static Predicate<AccountState> equalsName(String name) {
     return new AccountPredicate(
         AccountField.NAME_PART, AccountQueryBuilder.FIELD_NAME, name.toLowerCase());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index 9358a7a..946a729 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -37,6 +37,7 @@
   public static final String FIELD_LIMIT = "limit";
   public static final String FIELD_NAME = "name";
   public static final String FIELD_PREFERRED_EMAIL = "preferredemail";
+  public static final String FIELD_PREFERRED_EMAIL_EXACT = "preferredemail_exact";
   public static final String FIELD_USERNAME = "username";
   public static final String FIELD_VISIBLETO = "visibleto";
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index 4821e6f..bbcb811 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query.account;
 
 import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ArrayListMultimap;
@@ -25,6 +26,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -113,17 +115,60 @@
     return query(AccountPredicates.fullName(fullName));
   }
 
+  /**
+   * Queries for accounts that have a preferred email that exactly matches the given email.
+   *
+   * @param email preferred email by which accounts should be found
+   * @return list of accounts that have a preferred email that exactly matches the given email
+   * @throws OrmException if query cannot be parsed
+   */
   public List<AccountState> byPreferredEmail(String email) throws OrmException {
-    return query(AccountPredicates.preferredEmail(email));
+    if (schema().hasField(AccountField.PREFERRED_EMAIL_EXACT)) {
+      return query(AccountPredicates.preferredEmailExact(email));
+    }
+
+    return query(AccountPredicates.preferredEmail(email))
+        .stream()
+        .filter(a -> a.getAccount().getPreferredEmail().equals(email))
+        .collect(toList());
   }
 
+  /**
+   * Makes multiple queries for accounts by preferred email (exact match).
+   *
+   * @param emails preferred emails by which accounts should be found
+   * @return multimap of the given emails to accounts that have a preferred email that exactly
+   *     matches this email
+   * @throws OrmException if query cannot be parsed
+   */
   public Multimap<String, AccountState> byPreferredEmail(String... emails) throws OrmException {
     List<String> emailList = Arrays.asList(emails);
+
+    if (schema().hasField(AccountField.PREFERRED_EMAIL_EXACT)) {
+      List<List<AccountState>> r =
+          query(
+              emailList
+                  .stream()
+                  .map(e -> AccountPredicates.preferredEmailExact(e))
+                  .collect(toList()));
+      Multimap<String, AccountState> accountsByEmail = ArrayListMultimap.create();
+      for (int i = 0; i < emailList.size(); i++) {
+        accountsByEmail.putAll(emailList.get(i), r.get(i));
+      }
+      return accountsByEmail;
+    }
+
     List<List<AccountState>> r =
         query(emailList.stream().map(e -> AccountPredicates.preferredEmail(e)).collect(toList()));
     Multimap<String, AccountState> accountsByEmail = ArrayListMultimap.create();
     for (int i = 0; i < emailList.size(); i++) {
-      accountsByEmail.putAll(emailList.get(i), r.get(i));
+      String email = emailList.get(i);
+      Set<AccountState> matchingAccounts =
+          r.get(i)
+              .stream()
+              .filter(a -> a.getAccount().getPreferredEmail().equals(email))
+              .collect(toSet());
+      accountsByEmail.putAll(email, matchingAccounts);
     }
     return accountsByEmail;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java
index 6d3d9ba..c6cf3be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java
@@ -37,8 +37,7 @@
   }
 
   public static Predicate<AccountGroup> name(String name) {
-    return new GroupPredicate(
-        GroupField.NAME, GroupQueryBuilder.FIELD_NAME, name.toLowerCase(Locale.US));
+    return new GroupPredicate(GroupField.NAME, GroupQueryBuilder.FIELD_NAME, name);
   }
 
   public static Predicate<AccountGroup> owner(AccountGroup.UUID ownerUuid) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
index ba065a0..5146140 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -27,8 +28,11 @@
 public class H2AccountPatchReviewStore extends JdbcAccountPatchReviewStore {
 
   @Inject
-  H2AccountPatchReviewStore(@GerritServerConfig Config cfg, SitePaths sitePaths) {
-    super(cfg, sitePaths);
+  H2AccountPatchReviewStore(
+      @GerritServerConfig Config cfg,
+      SitePaths sitePaths,
+      ThreadSettingsConfig threadSettingsConfig) {
+    super(cfg, sitePaths, threadSettingsConfig);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
index e6a1fc9..c27a096 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.schema;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
 import com.google.common.collect.ImmutableSet;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -22,8 +25,10 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.change.AccountPatchReviewStore;
+import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import java.sql.Connection;
@@ -77,29 +82,30 @@
     }
   }
 
-  private final DataSource ds;
+  private DataSource ds;
 
   public static JdbcAccountPatchReviewStore createAccountPatchReviewStore(
-      Config cfg, SitePaths sitePaths) {
+      Config cfg, SitePaths sitePaths, ThreadSettingsConfig threadSettingsConfig) {
     String url = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, URL);
     if (url == null || url.contains(H2_DB)) {
-      return new H2AccountPatchReviewStore(cfg, sitePaths);
+      return new H2AccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig);
     }
     if (url.contains(POSTGRESQL)) {
-      return new PostgresqlAccountPatchReviewStore(cfg, sitePaths);
+      return new PostgresqlAccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig);
     }
     if (url.contains(MYSQL)) {
-      return new MysqlAccountPatchReviewStore(cfg, sitePaths);
+      return new MysqlAccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig);
     }
     if (url.contains(MARIADB)) {
-      return new MariaDBAccountPatchReviewStore(cfg, sitePaths);
+      return new MariaDBAccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig);
     }
     throw new IllegalArgumentException(
         "unsupported driver type for account patch reviews db: " + url);
   }
 
-  protected JdbcAccountPatchReviewStore(Config cfg, SitePaths sitePaths) {
-    this.ds = createDataSource(getUrl(cfg, sitePaths));
+  protected JdbcAccountPatchReviewStore(
+      Config cfg, SitePaths sitePaths, ThreadSettingsConfig threadSettingsConfig) {
+    this.ds = createDataSource(cfg, sitePaths, threadSettingsConfig);
   }
 
   protected JdbcAccountPatchReviewStore(DataSource ds) {
@@ -114,27 +120,45 @@
     return url;
   }
 
-  protected static DataSource createDataSource(String url) {
+  private static DataSource createDataSource(
+      Config cfg, SitePaths sitePaths, ThreadSettingsConfig threadSettingsConfig) {
     BasicDataSource datasource = new BasicDataSource();
-    if (url.contains(POSTGRESQL)) {
-      datasource.setDriverClassName("org.postgresql.Driver");
-    } else if (url.contains(H2_DB)) {
-      datasource.setDriverClassName("org.h2.Driver");
-    } else if (url.contains(MYSQL)) {
-      datasource.setDriverClassName("com.mysql.jdbc.Driver");
-    } else if (url.contains(MARIADB)) {
-      datasource.setDriverClassName("org.mariadb.jdbc.Driver");
-    }
+    String url = getUrl(cfg, sitePaths);
+    int poolLimit = threadSettingsConfig.getDatabasePoolLimit();
     datasource.setUrl(url);
-    datasource.setMaxActive(50);
-    datasource.setMinIdle(4);
-    datasource.setMaxIdle(16);
-    long evictIdleTimeMs = 1000 * 60;
+    datasource.setDriverClassName(getDriverFromUrl(url));
+    datasource.setMaxActive(cfg.getInt(ACCOUNT_PATCH_REVIEW_DB, "poolLimit", poolLimit));
+    datasource.setMinIdle(cfg.getInt(ACCOUNT_PATCH_REVIEW_DB, "poolminidle", 4));
+    datasource.setMaxIdle(
+        cfg.getInt(ACCOUNT_PATCH_REVIEW_DB, "poolmaxidle", Math.min(poolLimit, 16)));
+    datasource.setInitialSize(datasource.getMinIdle());
+    datasource.setMaxWait(
+        ConfigUtil.getTimeUnit(
+            cfg,
+            ACCOUNT_PATCH_REVIEW_DB,
+            null,
+            "poolmaxwait",
+            MILLISECONDS.convert(30, SECONDS),
+            MILLISECONDS));
+    long evictIdleTimeMs = 1000L * 60;
     datasource.setMinEvictableIdleTimeMillis(evictIdleTimeMs);
     datasource.setTimeBetweenEvictionRunsMillis(evictIdleTimeMs / 2);
     return datasource;
   }
 
+  private static String getDriverFromUrl(String url) {
+    if (url.contains(POSTGRESQL)) {
+      return "org.postgresql.Driver";
+    }
+    if (url.contains(MYSQL)) {
+      return "com.mysql.jdbc.Driver";
+    }
+    if (url.contains(MARIADB)) {
+      return "org.mariadb.jdbc.Driver";
+    }
+    return "org.h2.Driver";
+  }
+
   @Override
   public void start() {
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java
index b73305d..aa05a08 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -27,8 +28,11 @@
 public class MariaDBAccountPatchReviewStore extends JdbcAccountPatchReviewStore {
 
   @Inject
-  MariaDBAccountPatchReviewStore(@GerritServerConfig Config cfg, SitePaths sitePaths) {
-    super(cfg, sitePaths);
+  MariaDBAccountPatchReviewStore(
+      @GerritServerConfig Config cfg,
+      SitePaths sitePaths,
+      ThreadSettingsConfig threadSettingsConfig) {
+    super(cfg, sitePaths, threadSettingsConfig);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MariaDb.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MariaDb.java
index ed18a86..6c5dd35 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MariaDb.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MariaDb.java
@@ -40,6 +40,7 @@
     b.append(port(dbs.optional("port")));
     b.append("/");
     b.append(dbs.required("database"));
+    b.append("?useBulkStmts=false");
     return b.toString();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
index 12ea6b6..af84465 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -27,8 +28,11 @@
 public class MysqlAccountPatchReviewStore extends JdbcAccountPatchReviewStore {
 
   @Inject
-  MysqlAccountPatchReviewStore(@GerritServerConfig Config cfg, SitePaths sitePaths) {
-    super(cfg, sitePaths);
+  MysqlAccountPatchReviewStore(
+      @GerritServerConfig Config cfg,
+      SitePaths sitePaths,
+      ThreadSettingsConfig threadSettingsConfig) {
+    super(cfg, sitePaths, threadSettingsConfig);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java
index d228b91..34f7dba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -27,8 +28,11 @@
 public class PostgresqlAccountPatchReviewStore extends JdbcAccountPatchReviewStore {
 
   @Inject
-  PostgresqlAccountPatchReviewStore(@GerritServerConfig Config cfg, SitePaths sitePaths) {
-    super(cfg, sitePaths);
+  PostgresqlAccountPatchReviewStore(
+      @GerritServerConfig Config cfg,
+      SitePaths sitePaths,
+      ThreadSettingsConfig threadSettingsConfig) {
+    super(cfg, sitePaths, threadSettingsConfig);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_127.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_127.java
index aa28583..d246b75 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_127.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_127.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -32,18 +33,25 @@
 
   private final SitePaths sitePaths;
   private final Config cfg;
+  private final ThreadSettingsConfig threadSettingsConfig;
 
   @Inject
-  Schema_127(Provider<Schema_126> prior, SitePaths sitePaths, @GerritServerConfig Config cfg) {
+  Schema_127(
+      Provider<Schema_126> prior,
+      SitePaths sitePaths,
+      @GerritServerConfig Config cfg,
+      ThreadSettingsConfig threadSettingsConfig) {
     super(prior);
     this.sitePaths = sitePaths;
     this.cfg = cfg;
+    this.threadSettingsConfig = threadSettingsConfig;
   }
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
     JdbcAccountPatchReviewStore jdbcAccountPatchReviewStore =
-        JdbcAccountPatchReviewStore.createAccountPatchReviewStore(cfg, sitePaths);
+        JdbcAccountPatchReviewStore.createAccountPatchReviewStore(
+            cfg, sitePaths, threadSettingsConfig);
     jdbcAccountPatchReviewStore.dropTableIfExists();
     jdbcAccountPatchReviewStore.createTableIfNotExists();
     try (Connection con = jdbcAccountPatchReviewStore.getConnection();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_148.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_148.java
index abb3bb2..421e28d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_148.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_148.java
@@ -72,7 +72,7 @@
                 .open(note.getData(), OBJ_BLOB)
                 .getCachedBytes(ExternalIdReader.MAX_NOTE_SZ);
         try {
-          ExternalId extId = ExternalId.parse(note.getName(), raw);
+          ExternalId extId = ExternalId.parse(note.getName(), raw, note.getData());
 
           if (needsUpdate(extId)) {
             ExternalIdsUpdate.upsert(rw, ins, noteMap, extId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java
index d8a5278..4cbaffd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java
@@ -18,6 +18,7 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.github.rholder.retry.Attempt;
 import com.github.rholder.retry.RetryException;
 import com.github.rholder.retry.RetryListener;
 import com.github.rholder.retry.RetryerBuilder;
@@ -28,6 +29,10 @@
 import com.google.common.base.Throwables;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Histogram0;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.notedb.NotesMigration;
@@ -75,6 +80,30 @@
     }
   }
 
+  @Singleton
+  private static class Metrics {
+    final Histogram0 attemptCounts;
+    final Counter0 timeoutCount;
+
+    @Inject
+    Metrics(MetricMaker metricMaker) {
+      attemptCounts =
+          metricMaker.newHistogram(
+              "batch_update/retry_attempt_counts",
+              new Description(
+                      "Distribution of number of attempts made by RetryHelper"
+                          + " (1 == single attempt, no retry)")
+                  .setCumulative()
+                  .setUnit("attempts"));
+      timeoutCount =
+          metricMaker.newCounter(
+              "batch_update/retry_timeout_count",
+              new Description("Number of executions of RetryHelper that ultimately timed out")
+                  .setCumulative()
+                  .setUnit("timeouts"));
+    }
+  }
+
   public static Options.Builder options() {
     return new AutoValue_RetryHelper_Options.Builder();
   }
@@ -84,6 +113,7 @@
   }
 
   private final NotesMigration migration;
+  private final Metrics metrics;
   private final BatchUpdate.Factory updateFactory;
   private final Duration defaultTimeout;
   private final WaitStrategy waitStrategy;
@@ -91,9 +121,11 @@
   @Inject
   RetryHelper(
       @GerritServerConfig Config cfg,
+      Metrics metrics,
       NotesMigration migration,
       ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory,
       NoteDbBatchUpdate.AssistedFactory noteDbBatchUpdateFactory) {
+    this.metrics = metrics;
     this.migration = migration;
     this.updateFactory =
         new BatchUpdate.Factory(migration, reviewDbBatchUpdateFactory, noteDbBatchUpdateFactory);
@@ -117,18 +149,18 @@
   }
 
   public <T> T execute(Action<T> action, Options opts) throws RestApiException, UpdateException {
+    MetricListener listener = null;
     try {
       RetryerBuilder<T> builder = RetryerBuilder.newBuilder();
       if (migration.disableChangeReviewDb()) {
+        listener = new MetricListener(opts.listener());
         builder
+            .withRetryListener(listener)
             .withStopStrategy(
                 StopStrategies.stopAfterDelay(
                     firstNonNull(opts.timeout(), defaultTimeout).toMillis(), MILLISECONDS))
             .withWaitStrategy(waitStrategy)
             .retryIfException(RetryHelper::isLockFailure);
-        if (opts.listener() != null) {
-          builder.withRetryListener(opts.listener());
-        }
       } else {
         // Either we aren't full-NoteDb, or the underlying ref storage doesn't support atomic
         // transactions. Either way, retrying a partially-failed operation is not idempotent, so
@@ -136,11 +168,18 @@
       }
       return builder.build().call(() -> action.call(updateFactory));
     } catch (ExecutionException | RetryException e) {
+      if (e instanceof RetryException) {
+        metrics.timeoutCount.increment();
+      }
       if (e.getCause() != null) {
         Throwables.throwIfInstanceOf(e.getCause(), UpdateException.class);
         Throwables.throwIfInstanceOf(e.getCause(), RestApiException.class);
       }
       throw new UpdateException(e);
+    } finally {
+      if (listener != null) {
+        metrics.attemptCounts.record(listener.getAttemptCount());
+      }
     }
   }
 
@@ -150,4 +189,26 @@
     }
     return t instanceof LockFailureException;
   }
+
+  private static class MetricListener implements RetryListener {
+    private final RetryListener delegate;
+    private long attemptCount;
+
+    MetricListener(@Nullable RetryListener delegate) {
+      this.delegate = delegate;
+      attemptCount = 1;
+    }
+
+    @Override
+    public <V> void onRetry(Attempt<V> attempt) {
+      attemptCount = attempt.getAttemptNumber();
+      if (delegate != null) {
+        delegate.onRetry(attempt);
+      }
+    }
+
+    long getAttemptCount() {
+      return attemptCount;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
index b5e180df..a840e87 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
@@ -52,7 +52,7 @@
     checkArgument(!Strings.isNullOrEmpty(text), "Empty label vote");
     int e = text.lastIndexOf('=');
     checkArgument(e >= 0, "Label vote missing '=': %s", text);
-    return create(text.substring(0, e), Short.parseShort(text.substring(e + 1), text.length()));
+    return create(text.substring(0, e), Short.parseShort(text.substring(e + 1)));
   }
 
   public static StringBuilder appendTo(StringBuilder sb, String label, short value) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/account/AccountFieldTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/account/AccountFieldTest.java
new file mode 100644
index 0000000..7eed034
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/account/AccountFieldTest.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2017 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.index.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.testutil.GerritBaseTests;
+import java.util.List;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class AccountFieldTest extends GerritBaseTests {
+  @Test
+  public void refStateFieldValues() throws Exception {
+    AllUsersName allUsersName = new AllUsersName(AllUsersNameProvider.DEFAULT);
+    Account account = new Account(new Account.Id(1), TimeUtil.nowTs());
+    String metaId = "0e39795bb25dc914118224995c53c5c36923a461";
+    account.setMetaId(metaId);
+    List<String> values =
+        toStrings(
+            AccountField.REF_STATE.get(
+                new AccountState(
+                    allUsersName,
+                    account,
+                    ImmutableSet.of(),
+                    ImmutableSet.of(),
+                    ImmutableMap.of())));
+    assertThat(values).hasSize(1);
+    String expectedValue =
+        allUsersName.get() + ":" + RefNames.refsUsers(account.getId()) + ":" + metaId;
+    assertThat(Iterables.getOnlyElement(values)).isEqualTo(expectedValue);
+  }
+
+  @Test
+  public void externalIdStateFieldValues() throws Exception {
+    Account.Id id = new Account.Id(1);
+    Account account = new Account(id, TimeUtil.nowTs());
+    ExternalId extId1 =
+        ExternalId.create(
+            ExternalId.Key.create(ExternalId.SCHEME_MAILTO, "foo.bar@example.com"),
+            id,
+            "foo.bar@example.com",
+            null,
+            ObjectId.fromString("1b9a0cf038ea38a0ab08617c39aa8e28413a27ca"));
+    ExternalId extId2 =
+        ExternalId.create(
+            ExternalId.Key.create(ExternalId.SCHEME_USERNAME, "foo"),
+            id,
+            null,
+            "secret",
+            ObjectId.fromString("5b3a73dc9a668a5b89b5f049225261e3e3291d1a"));
+    List<String> values =
+        toStrings(
+            AccountField.EXTERNAL_ID_STATE.get(
+                new AccountState(
+                    null,
+                    account,
+                    ImmutableSet.of(),
+                    ImmutableSet.of(extId1, extId2),
+                    ImmutableMap.of())));
+    String expectedValue1 = extId1.key().sha1().name() + ":" + extId1.blobId().name();
+    String expectedValue2 = extId2.key().sha1().name() + ":" + extId2.blobId().name();
+    assertThat(values).containsExactly(expectedValue1, expectedValue2);
+  }
+
+  private List<String> toStrings(Iterable<byte[]> values) {
+    return Streams.stream(values).map(v -> new String(v, UTF_8)).collect(toList());
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java
index 0af642d..b25ed2b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -28,7 +28,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.change.StalenessChecker.RefState;
+import com.google.gerrit.server.index.RefState;
 import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
 import com.google.gerrit.server.notedb.NoteDbChangeState;
 import com.google.gerrit.testutil.GerritBaseTests;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
index aaf723a..5215561 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -25,6 +25,8 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.mail.Address;
 import java.util.Arrays;
 import java.util.Collections;
@@ -383,6 +385,10 @@
     account.setFullName(name);
     account.setPreferredEmail(email);
     return new AccountState(
-        account, Collections.emptySet(), Collections.emptySet(), new HashMap<>());
+        new AllUsersName(AllUsersNameProvider.DEFAULT),
+        account,
+        Collections.emptySet(),
+        Collections.emptySet(),
+        new HashMap<>());
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListTest.java
index 19adf32..0a7b97cc 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListTest.java
@@ -17,6 +17,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.reviewdb.client.Patch;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.util.Arrays;
 import java.util.Comparator;
 import org.junit.Test;
@@ -65,4 +70,21 @@
         });
     assertThat(names).isEqualTo(want);
   }
+
+  @Test
+  public void largeObjectTombstoneCanBeSerializedAndDeserialized() throws Exception {
+    // Serialize
+    byte[] serializedObject;
+    try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ObjectOutputStream objectStream = new ObjectOutputStream(baos)) {
+      objectStream.writeObject(new PatchListCacheImpl.LargeObjectTombstone());
+      serializedObject = baos.toByteArray();
+      assertThat(serializedObject).isNotNull();
+    }
+    // Deserialize
+    try (InputStream is = new ByteArrayInputStream(serializedObject);
+        ObjectInputStream ois = new ObjectInputStream(is)) {
+      assertThat(ois.readObject()).isInstanceOf(PatchListCacheImpl.LargeObjectTombstone.class);
+    }
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index db89eda..09b2af9 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -185,9 +185,9 @@
   public void byName() throws Exception {
     assertQuery("name:non-existing");
 
-    GroupInfo group = createGroup(name("group"));
+    GroupInfo group = createGroup(name("Group"));
     assertQuery("name:" + group.name, group);
-    assertQuery("name:" + group.name.toUpperCase(Locale.US), group);
+    assertQuery("name:" + group.name.toLowerCase(Locale.US));
 
     // only exact match
     GroupInfo groupWithHyphen = createGroup(name("group-with-hyphen"));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/LabelVoteTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/LabelVoteTest.java
index 0592041..9069928 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/LabelVoteTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/LabelVoteTest.java
@@ -14,75 +14,79 @@
 
 package com.google.gerrit.server.util;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.server.util.LabelVote.parse;
+import static com.google.gerrit.server.util.LabelVote.parseWithEquals;
 
 import org.junit.Test;
 
 public class LabelVoteTest {
   @Test
-  public void parse() {
-    LabelVote l;
-    l = LabelVote.parse("Code-Review-2");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) -2, l.value());
-    l = LabelVote.parse("Code-Review-1");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) -1, l.value());
-    l = LabelVote.parse("-Code-Review");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) 0, l.value());
-    l = LabelVote.parse("Code-Review");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) 1, l.value());
-    l = LabelVote.parse("Code-Review+1");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) 1, l.value());
-    l = LabelVote.parse("Code-Review+2");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) 2, l.value());
+  public void labelVoteParse() {
+    assertLabelVoteEquals(parse("Code-Review-2"), "Code-Review", -2);
+    assertLabelVoteEquals(parse("Code-Review-1"), "Code-Review", -1);
+    assertLabelVoteEquals(parse("-Code-Review"), "Code-Review", 0);
+    assertLabelVoteEquals(parse("Code-Review"), "Code-Review", 1);
+    assertLabelVoteEquals(parse("Code-Review+1"), "Code-Review", 1);
+    assertLabelVoteEquals(parse("Code-Review+2"), "Code-Review", 2);
   }
 
   @Test
-  public void format() {
-    assertEquals("Code-Review-2", LabelVote.parse("Code-Review-2").format());
-    assertEquals("Code-Review-1", LabelVote.parse("Code-Review-1").format());
-    assertEquals("-Code-Review", LabelVote.parse("-Code-Review").format());
-    assertEquals("Code-Review+1", LabelVote.parse("Code-Review+1").format());
-    assertEquals("Code-Review+2", LabelVote.parse("Code-Review+2").format());
+  public void labelVoteFormat() {
+    assertThat(parse("Code-Review-2").format()).isEqualTo("Code-Review-2");
+    assertThat(parse("Code-Review-1").format()).isEqualTo("Code-Review-1");
+    assertThat(parse("-Code-Review").format()).isEqualTo("-Code-Review");
+    assertThat(parse("Code-Review+1").format()).isEqualTo("Code-Review+1");
+    assertThat(parse("Code-Review+2").format()).isEqualTo("Code-Review+2");
   }
 
   @Test
-  public void parseWithEquals() {
-    LabelVote l;
-    l = LabelVote.parseWithEquals("Code-Review=-2");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) -2, l.value());
-    l = LabelVote.parseWithEquals("Code-Review=-1");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) -1, l.value());
-    l = LabelVote.parseWithEquals("Code-Review=0");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) 0, l.value());
-    l = LabelVote.parseWithEquals("Code-Review=1");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) 1, l.value());
-    l = LabelVote.parseWithEquals("Code-Review=+1");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) 1, l.value());
-    l = LabelVote.parseWithEquals("Code-Review=2");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) 2, l.value());
-    l = LabelVote.parseWithEquals("Code-Review=+2");
-    assertEquals("Code-Review", l.label());
-    assertEquals((short) 2, l.value());
+  public void labelVoteParseWithEquals() {
+    assertLabelVoteEquals(parseWithEquals("Code-Review=-2"), "Code-Review", -2);
+    assertLabelVoteEquals(parseWithEquals("Code-Review=-1"), "Code-Review", -1);
+    assertLabelVoteEquals(parseWithEquals("Code-Review=0"), "Code-Review", 0);
+    assertLabelVoteEquals(parseWithEquals("Code-Review=1"), "Code-Review", 1);
+    assertLabelVoteEquals(parseWithEquals("Code-Review=+1"), "Code-Review", 1);
+    assertLabelVoteEquals(parseWithEquals("Code-Review=2"), "Code-Review", 2);
+    assertLabelVoteEquals(parseWithEquals("Code-Review=+2"), "Code-Review", 2);
+    assertLabelVoteEquals(parseWithEquals("R=0"), "R", 0);
+
+    String longName = "A-loooooooooooooooooooooooooooooooooooooooooooooooooong-label";
+    // Regression test: an old bug passed the string length as a radix to Short#parseShort.
+    assertThat(longName.length()).isGreaterThan(Character.MAX_RADIX);
+    assertLabelVoteEquals(parseWithEquals(longName + "=+1"), longName, 1);
+
+    assertParseWithEqualsFails(null);
+    assertParseWithEqualsFails("");
+    assertParseWithEqualsFails("Code-Review");
+    assertParseWithEqualsFails("=1");
+    assertParseWithEqualsFails("=.1");
+    assertParseWithEqualsFails("=a1");
+    assertParseWithEqualsFails("=1a");
+    assertParseWithEqualsFails("=.");
   }
 
   @Test
-  public void formatWithEquals() {
-    assertEquals("Code-Review=-2", LabelVote.parseWithEquals("Code-Review=-2").formatWithEquals());
-    assertEquals("Code-Review=-1", LabelVote.parseWithEquals("Code-Review=-1").formatWithEquals());
-    assertEquals("Code-Review=0", LabelVote.parseWithEquals("Code-Review=0").formatWithEquals());
-    assertEquals("Code-Review=+1", LabelVote.parseWithEquals("Code-Review=+1").formatWithEquals());
-    assertEquals("Code-Review=+2", LabelVote.parseWithEquals("Code-Review=+2").formatWithEquals());
+  public void labelVoteFormatWithEquals() {
+    assertThat(parseWithEquals("Code-Review=-2").formatWithEquals()).isEqualTo("Code-Review=-2");
+    assertThat(parseWithEquals("Code-Review=-1").formatWithEquals()).isEqualTo("Code-Review=-1");
+    assertThat(parseWithEquals("Code-Review=0").formatWithEquals()).isEqualTo("Code-Review=0");
+    assertThat(parseWithEquals("Code-Review=+1").formatWithEquals()).isEqualTo("Code-Review=+1");
+    assertThat(parseWithEquals("Code-Review=+2").formatWithEquals()).isEqualTo("Code-Review=+2");
+  }
+
+  private void assertLabelVoteEquals(LabelVote actual, String expectedLabel, int expectedValue) {
+    assertThat(actual.label()).isEqualTo(expectedLabel);
+    assertThat((int) actual.value()).isEqualTo(expectedValue);
+  }
+
+  private void assertParseWithEqualsFails(String value) {
+    try {
+      parseWithEquals(value);
+      assert_().fail("expected IllegalArgumentException when parsing \"%s\"", value);
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java
index 242f208..c3b588b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java
@@ -20,6 +20,8 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -78,6 +80,11 @@
   }
 
   private static AccountState newState(Account account) {
-    return new AccountState(account, ImmutableSet.of(), ImmutableSet.of(), new HashMap<>());
+    return new AccountState(
+        new AllUsersName(AllUsersNameProvider.DEFAULT),
+        account,
+        ImmutableSet.of(),
+        ImmutableSet.of(),
+        new HashMap<>());
   }
 }
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
deleted file mode 100644
index f3dec88c4..0000000
--- a/gerrit-war/pom.xml
+++ /dev/null
@@ -1,86 +0,0 @@
-<project>
-  <modelVersion>4.0.0</modelVersion>
-  <groupId>com.google.gerrit</groupId>
-  <artifactId>gerrit-war</artifactId>
-  <version>2.15-SNAPSHOT</version>
-  <packaging>war</packaging>
-  <name>Gerrit Code Review - WAR</name>
-  <description>Gerrit WAR</description>
-  <url>https://www.gerritcodereview.com/</url>
-
-  <licenses>
-    <license>
-      <name>The Apache Software License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>
-    </license>
-  </licenses>
-
-  <scm>
-    <url>https://gerrit.googlesource.com/gerrit</url>
-    <connection>https://gerrit.googlesource.com/gerrit</connection>
-  </scm>
-
-  <developers>
-    <developer>
-      <name>Alice Kober-Sotzek</name>
-    </developer>
-    <developer>
-      <name>Andrew Bonventre</name>
-    </developer>
-    <developer>
-      <name>Becky Siegel</name>
-    </developer>
-    <developer>
-      <name>Dave Borowitz</name>
-    </developer>
-    <developer>
-      <name>David Ostrovsky</name>
-    </developer>
-    <developer>
-      <name>David Pursehouse</name>
-    </developer>
-    <developer>
-      <name>Edwin Kempin</name>
-    </developer>
-    <developer>
-      <name>Hugo Arès</name>
-    </developer>
-    <developer>
-      <name>Kasper Nilsson</name>
-    </developer>
-    <developer>
-      <name>Logan Hanks</name>
-    </developer>
-    <developer>
-      <name>Martin Fick</name>
-    </developer>
-    <developer>
-      <name>Saša Živkov</name>
-    </developer>
-    <developer>
-      <name>Shawn Pearce</name>
-    </developer>
-    <developer>
-      <name>Viktar Donich</name>
-    </developer>
-    <developer>
-      <name>Wyatt Allen</name>
-    </developer>
-  </developers>
-
-  <mailingLists>
-    <mailingList>
-      <name>Repo and Gerrit Discussion</name>
-      <post>repo-discuss@googlegroups.com</post>
-      <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe>
-      <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe>
-      <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive>
-    </mailingList>
-  </mailingLists>
-
-  <issueManagement>
-    <url>https://bugs.chromium.org/p/gerrit/issues/list</url>
-    <system>Gerrit Issue Tracker</system>
-  </issueManagement>
-</project>
diff --git a/plugins/replication b/plugins/replication
index 35d87c0..297b749 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 35d87c092ea7ef55085f2608917a85b2bd909b2f
+Subproject commit 297b749038153527291b43cb08b162eb475adcd7
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
new file mode 100644
index 0000000..4e67fda
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
@@ -0,0 +1,152 @@
+<!--
+Copyright (C) 2017 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.
+-->
+
+<script>
+(function(window) {
+  'use strict';
+
+  window.Gerrit = window.Gerrit || {};
+
+  /** @polymerBehavior Gerrit.AccessBehavior */
+  Gerrit.AccessBehavior = {
+    properties: {
+      permissionValues: {
+        type: Object,
+        readOnly: true,
+        value: {
+          abandon: {
+            id: 'abandon',
+            name: 'Abandon',
+          },
+          addPatchSet: {
+            id: 'addPatchSet',
+            name: 'Add Patch Set',
+          },
+          create: {
+            id: 'create',
+            name: 'Create Reference',
+          },
+          createTag: {
+            id: 'createTag',
+            name: 'Create Annotated Tag',
+          },
+          createSignedTag: {
+            id: 'createSignedTag',
+            name: 'Create Signed Tag',
+          },
+          delete: {
+            id: 'delete',
+            name: 'Delete Reference',
+          },
+          deleteDrafts: {
+            id: 'deleteDrafts',
+            name: 'Delete Drafts',
+          },
+          deleteOwnChanges: {
+            id: 'deleteOwnChanges',
+            name: 'Delete Own Changes',
+          },
+          editAssignee: {
+            id: 'editAssignee',
+            name: 'Edit Assignee',
+          },
+          editHashtags: {
+            id: 'editHashtags',
+            name: 'Edit Hashtags',
+          },
+          editTopicName: {
+            id: 'editTopicName',
+            name: 'Edit Topic Name',
+          },
+          forgeAuthor: {
+            id: 'forgeAuthor',
+            name: 'Forge Author Identity',
+          },
+          forgeCommitter: {
+            id: 'forgeCommitter',
+            name: 'Forge Committer Identity',
+          },
+          forgeServerAsCommitter: {
+            id: 'forgeServerAsCommitter',
+            name: 'Forge Server Identity',
+          },
+          owner: {
+            id: 'owner',
+            name: 'Owner',
+          },
+          publishDrafts: {
+            id: 'publishDrafts',
+            name: 'Publish Drafts',
+          },
+          push: {
+            id: 'push',
+            name: 'Push',
+          },
+          pushMerge: {
+            id: 'pushMerge',
+            name: 'Push Merge Commit',
+          },
+          read: {
+            id: 'read',
+            name: 'Read',
+          },
+          rebase: {
+            id: 'rebase',
+            name: 'Rebase',
+          },
+          removeReviewer: {
+            id: 'removeReviewer',
+            name: 'Remove Reviewer',
+          },
+          submit: {
+            id: 'submit',
+            name: 'Submit',
+          },
+          submitAs: {
+            id: 'submitAs',
+            name: 'Submit (On Behalf Of)',
+          },
+          viewDrafts: {
+            id: 'viewDrafts',
+            name: 'View Drafts',
+          },
+          viewPrivateChanges: {
+            id: 'viewPrivateChanges',
+            name: 'View Private Changes',
+          },
+        },
+      },
+    },
+
+    /**
+     * @param {!Object} obj
+     * @return {!Array} returns a sorted array sorted by the id of the original
+     *    object.
+     */
+    toSortedArray(obj) {
+      return Object.keys(obj).map(key => {
+        return {
+          id: key,
+          value: obj[key],
+        };
+      }).sort((a, b) => {
+        // Since IDs are strings, use localeCompare.
+        return a.id.localeCompare(b.id);
+      });
+    },
+  };
+})(window);
+</script>
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
new file mode 100644
index 0000000..62992e1
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>keyboard-shortcut-behavior</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../test/common-test-setup.html"/>
+<link rel="import" href="gr-access-behavior.html">
+
+<test-fixture id="basic">
+  <template>
+    <test-element></test-element>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-access-behavior tests', () => {
+    let element;
+
+    suiteSetup(() => {
+      // Define a Polymer element that uses this behavior.
+      Polymer({
+        is: 'test-element',
+        behaviors: [Gerrit.AccessBehavior],
+      });
+    });
+
+    setup(() => {
+      element = fixture('basic');
+    });
+
+    test('toSortedArray', () => {
+      const rules = {
+        'global:Project-Owners': {
+          action: 'ALLOW', force: false,
+        },
+        '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+          action: 'ALLOW', force: false,
+        },
+      };
+      const expectedResult = [
+        {id: '4c97682e6ce6b7247f3381b6f1789356666de7f', value: {
+          action: 'ALLOW', force: false,
+        }},
+        {id: 'global:Project-Owners', value: {
+          action: 'ALLOW', force: false,
+        }},
+      ];
+      assert.deepEqual(element.toSortedArray(rules), expectedResult);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
new file mode 100644
index 0000000..c2b28d5
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
@@ -0,0 +1,140 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-permission/gr-permission.html">
+
+<script src="../../../scripts/util.js"></script>
+
+<dom-module id="gr-access-section">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: block;
+        margin-bottom: 1em;
+      }
+      fieldset {
+        border: 1px solid #d1d2d3;
+      }
+      .header,
+      .editingRef .editContainer,
+      #deletedContainer {
+        align-items: baseline;
+        background: #f6f6f6;
+        border-bottom: 1px dotted #d1d2d3;
+        display: flex;
+        justify-content: space-between;
+        padding: .7em .7em;
+      }
+      #deletedContainer {
+        border-bottom: 0;
+      }
+      .sectionContent {
+        padding: .7em;
+      }
+      #deletedContainer,
+      .deleted #mainContainer,
+      .global,
+      #addPermission,
+      #updateBtns,
+      .editingRef .header,
+      .editContainer {
+        display: none;
+      }
+      .deleted #deletedContainer,
+      #mainContainer,
+      .editing #addPermission,
+      .editing #updateBtns  {
+        display: block;
+      }
+      .editingRef .editContainer {
+        display: flex;
+      }
+    </style>
+    <style include="gr-form-styles"></style>
+    <fieldset id="section"
+        class$="gr-form-styles [[_computeSectionClass(editing, _editingRef, _deleted)]]">
+      <div id="mainContainer">
+        <div class="header">
+          <span class="name">
+            <h3>[[_computeSectionName(section.id)]]</h3>
+          </span>
+          <div id="updateBtns">
+            <gr-button
+                id="editBtn"
+                class$="[[_computeEditBtnClass(section.id)]]"
+                on-tap="_handleEditReference">Edit Reference</gr-button>
+            <gr-button
+                id="deleteBtn"
+                on-tap="_handleRemoveReference">Remove</gr-button>
+          </div><!-- end updateBtns -->
+        </div><!-- end header -->
+        <div class="editContainer">
+          <input
+              id="editRefInput"
+              bind-value="{{section.id}}"
+              is="iron-input"
+              type="text">
+          <gr-button
+              id="undoEdit"
+              on-tap="_undoReferenceEdit">Undo</gr-button>
+        </div><!-- end editContainer -->
+        <div class="sectionContent">
+          <template
+              is="dom-repeat"
+              items="{{_permissions}}"
+              as="permission">
+            <gr-permission
+                name="[[_computePermissionName(section.id, permission, permissionValues, capabilities)]]"
+                permission="{{permission}}"
+                labels="[[labels]]"
+                section="[[section.id]]"
+                editing="[[editing]]">
+            </gr-permission>
+          </template>
+          <div id="addPermission">
+            Add permission:
+            <select id="permissionSelect">
+              <!-- called with a third parameter so that permissions update
+                  after a new section is added. -->
+              <template
+                  is="dom-repeat"
+                  items="[[_computePermissions(section.id, capabilities, labels, section.value.permissions.*)]]">
+                <option value="[[item.value.id]]">[[item.value.name]]</option>
+              </template>
+            </select>
+            <gr-button id="addBtn" on-tap="_handleAddPermission">Add</gr-button>
+          </div><!-- end addPermission -->
+        </div><!-- end sectionContent -->
+      </div><!-- end mainContainer -->
+      <div id="deletedContainer">
+        [[_computeSectionName(section.id)]] was deleted
+        <gr-button
+            id="undoRemoveBtn"
+            on-tap="_handleUndoRemove">Undo</gr-button>
+      </div><!-- end deletedContainer -->
+    </fieldset>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-access-section.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
new file mode 100644
index 0000000..16e6207
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
@@ -0,0 +1,199 @@
+// Copyright (C) 2017 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.
+(function() {
+  'use strict';
+
+  const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';
+
+  // The name that gets automatically input when a new reference is added.
+  const NEW_NAME = 'refs/heads/*';
+  const REFS_NAME = 'refs/';
+  const ON_BEHALF_OF = '(On Behalf Of)';
+  const LABEL = 'Label';
+
+  Polymer({
+    is: 'gr-access-section',
+
+    properties: {
+      capabilities: Object,
+      /** @type {?} */
+      section: {
+        type: Object,
+        notify: true,
+        observer: '_sectionChanged',
+      },
+      labels: Object,
+      editing: {
+        type: Boolean,
+        value: false,
+      },
+      _originalId: String,
+      _editingRef: {
+        type: Boolean,
+        value: false,
+      },
+      _deleted: {
+        type: Boolean,
+        value: false,
+      },
+      _permissions: Array,
+    },
+
+    behaviors: [
+      Gerrit.AccessBehavior,
+    ],
+
+    _sectionChanged(section) {
+      this._permissions = this.toSortedArray(section.value.permissions);
+      this._originalId = section.id;
+    },
+
+    _computePermissions(name, capabilities, labels) {
+      let allPermissions;
+      if (name === GLOBAL_NAME) {
+        allPermissions = this.toSortedArray(capabilities);
+      } else {
+        const labelOptions = this._computeLabelOptions(labels);
+        allPermissions = labelOptions.concat(
+            this.toSortedArray(this.permissionValues));
+      }
+      return allPermissions.filter(permission => {
+        return !this.section.value.permissions[permission.id];
+      });
+    },
+
+    _computeLabelOptions(labels) {
+      const labelOptions = [];
+      for (const labelName of Object.keys(labels)) {
+        labelOptions.push({
+          id: 'label-' + labelName,
+          value: {
+            name: `${LABEL} ${labelName}`,
+            id: 'label-' + labelName,
+          },
+        });
+        labelOptions.push({
+          id: 'labelAs-' + labelName,
+          value: {
+            name: `${LABEL} ${labelName} ${ON_BEHALF_OF}`,
+            id: 'labelAs-' + labelName,
+          },
+        });
+      }
+      return labelOptions;
+    },
+
+    _computePermissionName(name, permission, permissionValues, capabilities) {
+      if (name === GLOBAL_NAME) {
+        return capabilities[permission.id].name;
+      } else if (permissionValues[permission.id]) {
+        return permissionValues[permission.id].name;
+      } else if (permission.value.label) {
+        let behalfOf = '';
+        if (permission.id.startsWith('labelAs-')) {
+          behalfOf = ON_BEHALF_OF;
+        }
+        return `${LABEL} ${permission.value.label}${behalfOf}`;
+      }
+    },
+
+    _computeSectionName(name) {
+      // When a new section is created, it doesn't yet have a ref. Set into
+      // edit mode so that the user can input one.
+      if (!name) {
+        this._editingRef = true;
+        // Needed for the title value. This is the same default as GWT.
+        name = NEW_NAME;
+        // Needed for the input field value.
+        this.set('section.id', name);
+      }
+      if (name === GLOBAL_NAME) {
+        return 'Global Capabilities';
+      } else if (name.startsWith(REFS_NAME)) {
+        return `Reference: ${name}`;
+      }
+      return name;
+    },
+
+    _handleRemoveReference() {
+      this._deleted = true;
+      this.set('section.value.deleted', true);
+    },
+
+    _handleUndoRemove() {
+      this._deleted = false;
+      delete this.section.value.deleted;
+    },
+
+    _handleEditReference() {
+      this._editingRef = true;
+    },
+
+    _undoReferenceEdit() {
+      this._editingRef = false;
+      this.set('section.id', this._originalId);
+    },
+
+    _computeSectionClass(editing, editingRef, deleted) {
+      const classList = [];
+      if (editing) {
+        classList.push('editing');
+      }
+      if (editingRef) {
+        classList.push('editingRef');
+      }
+      if (deleted) {
+        classList.push('deleted');
+      }
+      return classList.join(' ');
+    },
+
+    _computeEditBtnClass(name) {
+      return name === GLOBAL_NAME ? 'global' : '';
+    },
+
+    _handleAddPermission() {
+      const value = this.$.permissionSelect.value;
+      const permission = {
+        id: value,
+        value: {rules: {}},
+      };
+
+      // This is needed to update the 'label' property of the
+      // 'label-<label-name>' permission.
+      //
+      // The value from the add permission dropdown will either be
+      // label-<label-name> or labelAs-<labelName>.
+      // But, the format of the API response is as such:
+      // "permissions": {
+      //  "label-Code-Review": {
+      //    "label": "Code-Review",
+      //    "rules": {...}
+      //    }
+      //  }
+      // }
+      // When we add a new item, we have to push the new permission in the same
+      // format as the ones that have been returned by the API.
+      if (value.startsWith('label')) {
+        permission.value.label =
+            value.replace('label-', '').replace('labelAs-', '');
+      }
+      // Add to the end of the array (used in dom-repeat) and also to the
+      // section object that is two way bound with its parent element.
+      this.push('_permissions', permission);
+      this.set(['section.value.permissions', permission.id],
+          permission.value);
+    },
+  });
+})();
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
new file mode 100644
index 0000000..38c5d67
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
@@ -0,0 +1,466 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-access-section</title>
+
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-access-section.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-access-section></gr-access-section>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-access-section tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    suite('unit tests', () => {
+      setup(() => {
+        element.section = {
+          id: 'refs/*',
+          value: {
+            permissions: {
+              read: {
+                rules: {},
+              },
+            },
+          },
+        };
+        element.capabilities = {
+          accessDatabase: {
+            id: 'accessDatabase',
+            name: 'Access Database',
+          },
+          administrateServer: {
+            id: 'administrateServer',
+            name: 'Administrate Server',
+          },
+          batchChangesLimit: {
+            id: 'batchChangesLimit',
+            name: 'Batch Changes Limit',
+          },
+          createAccount: {
+            id: 'createAccount',
+            name: 'Create Account',
+          },
+        };
+        element.labels = {
+          'Code-Review': {
+            values: {
+              ' 0': 'No score',
+              '-1': 'I would prefer this is not merged as is',
+              '-2': 'This shall not be merged',
+              '+1': 'Looks good to me, but someone else must approve',
+              '+2': 'Looks good to me, approved',
+            },
+            default_value: 0,
+          },
+        };
+        element._sectionChanged(element.section);
+        flushAsynchronousOperations();
+      });
+
+      test('_sectionChanged', () => {
+        // _sectionChanged was called in setup, so just make assertions.
+        const expectedPermissions = [
+          {
+            id: 'read',
+            value: {
+              rules: {},
+            },
+          },
+        ];
+        assert.deepEqual(element._permissions, expectedPermissions);
+        assert.equal(element._originalId, element.section.id);
+      });
+
+      test('_computeLabelOptions', () => {
+        const expectedLabelOptions = [
+          {
+            id: 'label-Code-Review',
+            value: {
+              name: 'Label Code-Review',
+              id: 'label-Code-Review',
+            },
+          },
+          {
+            id: 'labelAs-Code-Review',
+            value: {
+              name: 'Label Code-Review (On Behalf Of)',
+              id: 'labelAs-Code-Review',
+            },
+          },
+        ];
+
+        assert.deepEqual(element._computeLabelOptions(element.labels),
+            expectedLabelOptions);
+      });
+
+      test('_computePermissions', () => {
+        sandbox.stub(element, 'toSortedArray').returns(
+            [{
+              id: 'push',
+              value: {
+                rules: {},
+              },
+            },
+            {
+              id: 'read',
+              value: {
+                rules: {},
+              },
+            },
+            ]);
+
+        const expectedPermissions = [{
+          id: 'push',
+          value: {
+            rules: {},
+          },
+        },
+        ];
+        const labelOptions = [
+          {
+            id: 'label-Code-Review',
+            value: {
+              name: 'Label Code-Review',
+              id: 'label-Code-Review',
+            },
+          },
+          {
+            id: 'labelAs-Code-Review',
+            value: {
+              name: 'Label Code-Review (On Behalf Of)',
+              id: 'labelAs-Code-Review',
+            },
+          },
+        ];
+
+        // For global capabilities, just return the sorted array filtered by
+        // existing permissions.
+        let name = 'GLOBAL_CAPABILITIES';
+        assert.deepEqual(element._computePermissions(name, element.capabilities,
+            element.labels), expectedPermissions);
+
+        // Uses the capabilities array to come up with possible values.
+        assert.isTrue(element.toSortedArray.lastCall.
+            calledWithExactly(element.capabilities));
+
+
+        // For everything else, include possible label values before filtering.
+        name = 'refs/for/*';
+        assert.deepEqual(element._computePermissions(name, element.capabilities,
+            element.labels), labelOptions.concat(expectedPermissions));
+
+        // Uses permissionValues (defined in gr-access-behavior) to come up with
+        // possible values.
+        assert.isTrue(element.toSortedArray.lastCall.
+            calledWithExactly(element.permissionValues));
+      });
+
+      test('_computePermissionName', () => {
+        let name = 'GLOBAL_CAPABILITIES';
+        let permission = {
+          id: 'administrateServer',
+          value: {},
+        };
+        assert.equal(element._computePermissionName(name, permission,
+            element.permissionValues, element.capabilities),
+            element.capabilities[permission.id].name);
+
+        name = 'refs/for/*';
+        permission = {
+          id: 'abandon',
+          value: {},
+        };
+
+        assert.equal(element._computePermissionName(
+            name, permission, element.permissionValues, element.capabilities),
+            element.permissionValues[permission.id].name);
+
+        name = 'refs/for/*';
+        permission = {
+          id: 'label-Code-Review',
+          value: {
+            label: 'Code-Review',
+          },
+        };
+
+        assert.equal(element._computePermissionName(name, permission,
+            element.permissionValues, element.capabilities),
+            'Label Code-Review');
+
+        permission = {
+          id: 'labelAs-Code-Review',
+          value: {
+            label: 'Code-Review',
+          },
+        };
+
+        assert.equal(element._computePermissionName(name, permission,
+            element.permissionValues, element.capabilities),
+            'Label Code-Review(On Behalf Of)');
+      });
+
+      test('_computeSectionName', () => {
+        let name;
+        // When computing the section name for an undefined name, it means a
+        // new section is being added. In this case, it should defualt to
+        // 'refs/heads/*'.
+        element._editingRef = false;
+        assert.equal(element._computeSectionName(name),
+            'Reference: refs/heads/*');
+        assert.isTrue(element._editingRef);
+        assert.equal(element.section.id, 'refs/heads/*');
+
+        // Reset editing to false.
+        element._editingRef = false;
+        name = 'GLOBAL_CAPABILITIES';
+        assert.equal(element._computeSectionName(name), 'Global Capabilities');
+        assert.isFalse(element._editingRef);
+
+        name = 'refs/for/*';
+        assert.equal(element._computeSectionName(name),
+            'Reference: refs/for/*');
+        assert.isFalse(element._editingRef);
+      });
+
+      test('_handleEditReference', () => {
+        element._handleEditReference();
+        assert.isTrue(element._editingRef);
+      });
+
+      test('_undoReferenceEdit', () => {
+        element._originalId = 'refs/for/old';
+        element.section.id = 'refs/for/new';
+        element.editing = true;
+        element._undoReferenceEdit();
+        assert.isFalse(element._editingRef);
+        assert.equal(element.section.id, 'refs/for/old');
+      });
+
+      test('_computeSectionClass', () => {
+        let editingRef = false;
+        let editing = false;
+        let deleted = false;
+        assert.equal(element._computeSectionClass(editing, editingRef, deleted),
+            '');
+
+        editing = true;
+        assert.equal(element._computeSectionClass(editing, editingRef, deleted),
+            'editing');
+
+        editingRef = true;
+        assert.equal(element._computeSectionClass(editing, editingRef, deleted),
+            'editing editingRef');
+
+        deleted = true;
+        assert.equal(element._computeSectionClass(editing, editingRef, deleted),
+            'editing editingRef deleted');
+
+        editingRef = false;
+        assert.equal(element._computeSectionClass(editing, editingRef, deleted),
+            'editing deleted');
+      });
+
+      test('_computeEditBtnClass', () => {
+        let name = 'GLOBAL_CAPABILITIES';
+        assert.equal(element._computeEditBtnClass(name), 'global');
+        name = 'refs/for/*';
+        assert.equal(element._computeEditBtnClass(name), '');
+      });
+    });
+
+    suite('interactive tests', () => {
+      setup(() => {
+        element.labels = {
+          'Code-Review': {
+            values: {
+              ' 0': 'No score',
+              '-1': 'I would prefer this is not merged as is',
+              '-2': 'This shall not be merged',
+              '+1': 'Looks good to me, but someone else must approve',
+              '+2': 'Looks good to me, approved',
+            },
+            default_value: 0,
+          },
+        };
+      });
+      suite('Global section', () => {
+        setup(() => {
+          element.section = {
+            id: 'GLOBAL_CAPABILITIES',
+            value: {
+              permissions: {
+                accessDatabase: {
+                  rules: {},
+                },
+              },
+            },
+          };
+          element.capabilities = {
+            accessDatabase: {
+              id: 'accessDatabase',
+              name: 'Access Database',
+            },
+            administrateServer: {
+              id: 'administrateServer',
+              name: 'Administrate Server',
+            },
+            batchChangesLimit: {
+              id: 'batchChangesLimit',
+              name: 'Batch Changes Limit',
+            },
+            createAccount: {
+              id: 'createAccount',
+              name: 'Create Account',
+            },
+          };
+          element._sectionChanged(element.section);
+          flushAsynchronousOperations();
+        });
+
+        test('classes are assigned correctly', () => {
+          assert.isFalse(element.$.section.classList.contains('editing'));
+          assert.isFalse(element.$.section.classList.contains('deleted'));
+          assert.isTrue(element.$.editBtn.classList.contains('global'));
+        });
+      });
+
+      suite('Non-global section', () => {
+        setup(() => {
+          element.section = {
+            id: 'refs/*',
+            value: {
+              permissions: {
+                read: {
+                  rules: {},
+                },
+              },
+            },
+          };
+          element.capabilities = {};
+          element._sectionChanged(element.section);
+          flushAsynchronousOperations();
+        });
+
+        test('classes are assigned correctly', () => {
+          assert.isFalse(element.$.section.classList.contains('editing'));
+          assert.isFalse(element.$.section.classList.contains('deleted'));
+          assert.isFalse(element.$.editBtn.classList.contains('global'));
+        });
+
+        test('add permission', () => {
+          element.$.permissionSelect.value = 'label-Code-Review';
+          assert.equal(element._permissions.length, 1);
+          assert.equal(Object.keys(element.section.value.permissions).length,
+              1);
+          MockInteractions.tap(element.$.addBtn);
+          flushAsynchronousOperations();
+
+          // The permission is added to both the permissions array and also
+          // the section's permission object.
+          assert.equal(element._permissions.length, 2);
+          let permission = {
+            id: 'label-Code-Review',
+            value: {
+              label: 'Code-Review',
+              rules: {},
+            },
+          };
+          assert.equal(element._permissions.length, 2);
+          assert.deepEqual(element._permissions[1], permission);
+          assert.equal(Object.keys(element.section.value.permissions).length,
+              2);
+          assert.deepEqual(
+              element.section.value.permissions['label-Code-Review'],
+              permission.value);
+
+
+          element.$.permissionSelect.value = 'abandon';
+          MockInteractions.tap(element.$.addBtn);
+          flushAsynchronousOperations();
+
+          permission = {
+            id: 'abandon',
+            value: {
+              rules: {},
+            },
+          };
+
+          assert.equal(element._permissions.length, 3);
+          assert.deepEqual(element._permissions[2], permission);
+          assert.equal(Object.keys(element.section.value.permissions).length,
+              3);
+          assert.deepEqual(element.section.value.permissions['abandon'],
+              permission.value);
+        });
+
+        test('edit section reference', () => {
+          assert.isFalse(element.$.section.classList.contains('editing'));
+          element.editing = true;
+          assert.isTrue(element.$.section.classList.contains('editing'));
+          assert.isFalse(element._editingRef);
+          MockInteractions.tap(element.$.editBtn);
+          element.$.editRefInput.bindValue='new/ref';
+          flushAsynchronousOperations();
+          assert.equal(element.section.id, 'new/ref');
+          assert.isTrue(element._editingRef);
+          assert.isTrue(element.$.section.classList.contains('editingRef'));
+          MockInteractions.tap(element.$.undoEdit);
+          flushAsynchronousOperations();
+          assert.isFalse(element._editingRef);
+          assert.isFalse(element.$.section.classList.contains('editingRef'));
+          assert.equal(element.section.id, 'refs/*');
+        });
+
+        test('remove section', () => {
+          element.editing = true;
+          assert.isFalse(element._deleted);
+          MockInteractions.tap(element.$.deleteBtn);
+          flushAsynchronousOperations();
+          assert.isTrue(element._deleted);
+          assert.isTrue(element.$.section.classList.contains('deleted'));
+          assert.isTrue(element.section.value.deleted);
+
+          MockInteractions.tap(element.$.undoRemoveBtn);
+          flushAsynchronousOperations();
+          assert.isFalse(element._deleted);
+          assert.isNotOk(element.section.value.deleted);
+        });
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
index fb69208..7217fae 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -29,10 +29,11 @@
 <link rel="import" href="../gr-admin-group-list/gr-admin-group-list.html">
 <link rel="import" href="../gr-admin-project-list/gr-admin-project-list.html">
 <link rel="import" href="../gr-group/gr-group.html">
+<link rel="import" href="../gr-group-audit-log/gr-group-audit-log.html">
 <link rel="import" href="../gr-group-members/gr-group-members.html">
 <link rel="import" href="../gr-plugin-list/gr-plugin-list.html">
 <link rel="import" href="../gr-project/gr-project.html">
-<link rel="import" href="../gr-group-audit-log/gr-group-audit-log.html">
+<link rel="import" href="../gr-project-commands/gr-project-commands.html">
 <link rel="import" href="../gr-project-detail-list/gr-project-detail-list.html">
 
 <dom-module id="gr-admin-view">
@@ -120,6 +121,12 @@
             class="table"></gr-group-audit-log>
       </main>
     </template>
+    <template is="dom-if" if="[[_showProjectCommands]]" restamp="true">
+      <main>
+        <gr-project-commands
+            project="[[params.project]]"></gr-project-commands>
+      </main>
+    </template>
     <template is="dom-if" if="[[params.placeholder]]" restamp="true">
       <gr-placeholder title="Admin" path="[[path]]"></gr-placeholder>
     </template>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index 0616507..9ca6fd4 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -64,6 +64,7 @@
       _showGroupAuditLog: Boolean,
       _showGroupList: Boolean,
       _showGroupMembers: Boolean,
+      _showProjectCommands: Boolean,
       _showProjectMain: Boolean,
       _showProjectList: Boolean,
       _showProjectDetailList: Boolean,
@@ -109,6 +110,13 @@
             view: 'gr-project',
             url: `/admin/projects/${this.encodeURL(this._projectName, true)}`,
             children: [{
+              name: 'Commands',
+              detailType: 'commands',
+              view: 'gr-project-commands',
+              url: `/admin/projects/` +
+                  `${this.encodeURL(this._projectName, true)},commands`,
+            },
+            {
               name: 'Branches',
               detailType: 'branches',
               view: 'gr-project-detail-list',
@@ -171,6 +179,8 @@
       this.set('_showGroupAuditLog', params.adminView === 'gr-group-audit-log');
       this.set('_showGroupList', params.adminView === 'gr-admin-group-list');
       this.set('_showGroupMembers', params.adminView === 'gr-group-members');
+      this.set('_showProjectCommands',
+          params.adminView === 'gr-project-commands');
       this.set('_showProjectMain', params.adminView === 'gr-project');
       this.set('_showProjectList',
           params.adminView === 'gr-admin-project-list');
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
new file mode 100644
index 0000000..7c6acff
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
@@ -0,0 +1,108 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
+<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../shared/gr-select/gr-select.html">
+
+<dom-module id="gr-create-change-dialog">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles">
+      :host {
+        display: inline-block;
+      }
+      input {
+        width: 25em;
+      }
+      gr-autocomplete {
+        border: none;
+        float: right;
+        --gr-autocomplete: {
+          border: 1px solid #d1d2d3;
+          border-radius: 2px;
+          font-size: 1em;
+          height: 2em;
+          padding: 0 .15em;
+          width: 20em;
+        }
+      }
+    </style>
+    <div class="gr-form-styles">
+      <div id="form">
+        <section>
+          <span class="title">Select branch for new change</span>
+          <span class="value">
+            <gr-autocomplete
+                id="branchInput"
+                text="{{branch}}"
+                query="[[_query]]"
+                placeholder="Destination branch">
+            </gr-autocomplete>
+          </span>
+        </section>
+        <section>
+          <span class="title">Enter topic for new change (optional)</span>
+          <input
+              is="iron-input"
+              id="tagNameInput"
+              bind-value="{{topic}}">
+        </section>
+        <section>
+          <span class="title">Description</span>
+          <iron-autogrow-textarea
+              id="messageInput"
+              class="message"
+              autocomplete="on"
+              rows="4"
+              max-rows="15"
+              bind-value="{{subject}}"
+              placeholder="Insert the description of the change.">
+          </iron-autogrow-textarea>
+        </section>
+        <section>
+          <span class="title">Options</span>
+          <section>
+            <label for="privateChangeCheckBox">Private Change</label>
+            <input
+                type="checkbox"
+                id="privateChangeCheckBox"
+                checked$="{{checkedPrivate}}"
+                hidden$="[[_serverConfig.change.private_by_default]]">
+          </section>
+          <section>
+            <label for="wipChangeCheckBox">WIP Change</label>
+            <input
+                type="checkbox"
+                id="wipChangeCheckBox">
+          </section>
+        </section>
+      </div>
+    </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-create-change-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
new file mode 100644
index 0000000..8da0496
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
@@ -0,0 +1,102 @@
+// Copyright (C) 2017 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.
+(function() {
+  'use strict';
+
+  const SUGGESTIONS_LIMIT = 15;
+  const REF_PREFIX = 'refs/heads/';
+
+  Polymer({
+    is: 'gr-create-change-dialog',
+
+    properties: {
+      projectName: String,
+      branch: String,
+      /** @type {?} */
+      _serverConfig: Object,
+      subject: String,
+      topic: String,
+      _query: {
+        type: Function,
+        value() {
+          return this._getProjectBranchesSuggestions.bind(this);
+        },
+      },
+      checkedPrivate: {
+        type: Boolean,
+        value: true,
+      },
+      canCreate: {
+        type: Boolean,
+        notify: true,
+        value: false,
+      },
+    },
+
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+      Gerrit.URLEncodingBehavior,
+    ],
+
+    attached() {
+      this.$.restAPI.getConfig().then(config => {
+        this._serverConfig = config;
+      });
+    },
+
+    observers: [
+      '_allowCreate(branch, subject)',
+    ],
+
+    _allowCreate(branch, subject) {
+      this.canCreate = !!branch && !!subject;
+    },
+
+    handleCreateChange() {
+      const isPrivate = this.$.privateChangeCheckBox.checked;
+      const isWip = this.$.wipChangeCheckBox.checked;
+      return this.$.restAPI.createChange(this.projectName, this.branch,
+          this.subject, this.topic, isPrivate, isWip)
+          .then(changeCreated => {
+            if (!changeCreated) {
+              return;
+            }
+            Gerrit.Nav.navigateToChange(changeCreated);
+          });
+    },
+
+    _getProjectBranchesSuggestions(input) {
+      if (input.startsWith(REF_PREFIX)) {
+        input = input.substring(REF_PREFIX.length);
+      }
+      return this.$.restAPI.getProjectBranches(
+          input, this.projectName, SUGGESTIONS_LIMIT).then(response => {
+            const branches = [];
+            let branch;
+            for (const key in response) {
+              if (!response.hasOwnProperty(key)) { continue; }
+              if (response[key].ref.startsWith('refs/heads/')) {
+                branch = response[key].ref.substring('refs/heads/'.length);
+              } else {
+                branch = response[key].ref;
+              }
+              branches.push({
+                name: branch,
+              });
+            }
+            return branches;
+          });
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
new file mode 100644
index 0000000..a66ffbd
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-create-change-dialog</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-create-change-dialog.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-create-change-dialog></gr-create-change-dialog>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-create-change-dialog tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      stub('gr-rest-api-interface', {
+        getLoggedIn() { return Promise.resolve(true); },
+        getProjectBranches(input) {
+          if (input.startsWith('test')) {
+            return Promise.resolve([
+              {
+                ref: 'refs/heads/test-branch',
+                revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+                can_delete: true,
+              },
+            ]);
+          } else {
+            return Promise.resolve({});
+          }
+        },
+      });
+      element = fixture('basic');
+      element.projectName = 'test-project';
+      element._serverConfig = {
+        change: {},
+      };
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('new change created', () => {
+      const configInputObj = {
+        branch: 'test-branch',
+        topic: 'test-topic',
+        subject: 'first change created with polygerrit ui',
+        is_private: true,
+        work_in_progress: false,
+        project: element.projectName,
+      };
+
+      const saveStub = sandbox.stub(element.$.restAPI,
+          'createChange', () => {
+            return Promise.resolve({});
+          });
+
+      element.project = element.projectName;
+      element.branch = 'test-branch';
+      element.topic = 'test-topic';
+      element.subject = 'first change created with polygerrit ui';
+      element.is_private = true;
+      element.work_in_progress = false;
+
+      element.$.branchInput.bindValue = configInputObj.branch;
+      element.$.tagNameInput.bindValue = configInputObj.topic;
+      element.$.messageInput.bindValue = configInputObj.subject;
+      element.$.privateChangeCheckBox.checked = configInputObj.is_private;
+      element.$.wipChangeCheckBox.checked =
+          configInputObj.work_in_progress;
+
+      element.handleCreateChange().then(() => {
+        assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj));
+      });
+    });
+
+    test('_getProjectBranchesSuggestions empty', done => {
+      element._getProjectBranchesSuggestions('nonexistent').then(branches => {
+        assert.equal(branches.length, 0);
+        done();
+      });
+    });
+
+    test('_getProjectBranchesSuggestions non-empty', done => {
+      element._getProjectBranchesSuggestions('test-branch').then(branches => {
+        assert.equal(branches.length, 1);
+        assert.equal(branches[0].name, 'test-branch');
+        done();
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
new file mode 100644
index 0000000..1dfb06c
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
@@ -0,0 +1,120 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
+<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/gr-menu-page-styles.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-rule-editor/gr-rule-editor.html">
+
+<dom-module id="gr-permission">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: block;
+        margin-bottom: .7em;
+      }
+      .header {
+        align-items: baseline;
+        display: flex;
+        justify-content: space-between;
+        margin: .3em .7em;
+      }
+      #deletedContainer {
+        border: 1px solid #d1d2d3;
+        padding: .7em;
+      }
+      .rules {
+        background: #fafafa;
+        border: 1px solid #d1d2d3;
+        border-bottom: 0;
+      }
+      .editing .rules {
+        border-bottom: 1px solid #d1d2d3;
+      }
+      .title {
+        margin-bottom: .3em;
+      }
+      #addRule,
+      #removeBtn {
+        display: none;
+      }
+      .editing #removeBtn {
+        display: block;
+      }
+      .editing #addRule {
+        display: block;
+        padding: .7em;
+      }
+      #deletedContainer,
+      .deleted #mainContainer {
+        display: none;
+      }
+      .deleted #deletedContainer,
+      #mainContainer {
+        display: block;
+      }
+    </style>
+    <style include="gr-form-styles"></style>
+    <style include="gr-menu-page-styles"></style>
+    <section
+        id="permission"
+        class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
+      <div id="mainContainer">
+        <div class="header">
+          <span class="title">[[name]]</span>
+          <gr-button
+              id="removeBtn"
+              on-tap="_handleRemovePermission">Remove</gr-button>
+        </div><!-- end header -->
+        <div class="rules">
+          <template
+              is="dom-repeat"
+              items="{{_rules}}"
+              as="rule">
+            <gr-rule-editor
+                label="[[_label]]"
+                editing="[[editing]]"
+                group="[[rule.id]]"
+                permission="[[permission.id]]"
+                rule="{{rule}}"
+                section="[[section]]"></gr-rule-editor>
+          </template>
+          <div id="addRule">
+            <gr-autocomplete
+                text="{{_groupFilter}}"
+                query="[[_query]]"
+                placeholder="Add group"
+                on-commit="_handleAddRuleItem">
+            </gr-autocomplete>
+          </div> <!-- end addRule -->
+        </div> <!-- end rules -->
+      </div><!-- end mainContainer -->
+      <div id="deletedContainer">
+        [[name]] was deleted
+        <gr-button
+            id="undoRemoveBtn"
+            on-tap="_handleUndoRemove">Undo</gr-button>
+      </div><!-- end deletedContainer -->
+    </section>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-permission.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
new file mode 100644
index 0000000..97d4fe5
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -0,0 +1,174 @@
+// Copyright (C) 2017 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.
+(function() {
+  'use strict';
+
+  const MAX_AUTOCOMPLETE_RESULTS = 20;
+
+  Polymer({
+    is: 'gr-permission',
+
+    properties: {
+      labels: Object,
+      name: String,
+      /** @type {?} */
+      permission: {
+        type: Object,
+        observer: '_sortPermission',
+        notify: true,
+      },
+      section: String,
+      editing: {
+        type: Boolean,
+        value: false,
+      },
+      _label: {
+        type: Object,
+        computed: '_computeLabel(permission, labels)',
+      },
+      _groupFilter: String,
+      _query: {
+        type: Function,
+        value() {
+          return this._getGroupSuggestions.bind(this);
+        },
+      },
+      _rules: Array,
+      _groupsWithRules: Object,
+      _deleted: {
+        type: Boolean,
+        value: false,
+      },
+    },
+
+    behaviors: [
+      Gerrit.AccessBehavior,
+    ],
+
+    observers: [
+      '_handleRulesChanged(_rules.splices)',
+    ],
+
+    _handleRulesChanged(changeRecord) {
+      // Update the groups to exclude in the autocomplete.
+      this._groupsWithRules = this._computeGroupsWithRules(this._rules);
+    },
+
+    _sortPermission(permission) {
+      this._rules = this.toSortedArray(permission.value.rules);
+    },
+
+    _handleRemovePermission() {
+      this._deleted = true;
+      this.set('permission.value.deleted', true);
+    },
+
+    _computeSectionClass(editing, deleted) {
+      const classList = [];
+      if (editing) {
+        classList.push('editing');
+      }
+      if (deleted) {
+        classList.push('deleted');
+      }
+      return classList.join(' ');
+    },
+
+    _handleUndoRemove() {
+      this._deleted = false;
+      delete this.permission.value.deleted;
+    },
+
+    _computeLabel(permission, labels) {
+      if (!permission.value.label) { return; }
+
+      const labelName = permission.value.label;
+      const label = {
+        name: labelName,
+        values: this._computeLabelValues(labels[labelName].values),
+      };
+      return label;
+    },
+
+    _computeLabelValues(values) {
+      const valuesArr = [];
+      const keys = Object.keys(values).sort((a, b) => {
+        return parseInt(a, 10) - parseInt(b, 10);
+      });
+
+      for (const key of keys) {
+        if (!values[key]) { return; }
+        // The value from the server being used to choose which item is
+        // selected is in integer form, so this must be converted.
+        valuesArr.push({value: parseInt(key, 10), text: values[key]});
+      }
+      return valuesArr;
+    },
+
+    /**
+     * @param {!Array} rules
+     * @return {!Object} Object with groups with rues as keys, and true as
+     *    value.
+     */
+    _computeGroupsWithRules(rules) {
+      const groups = {};
+      for (const rule of rules) {
+        groups[rule.id] = true;
+      }
+      return groups;
+    },
+
+    _getGroupSuggestions() {
+      return this.$.restAPI.getSuggestedGroups(
+          this._groupFilter,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(response => {
+            const groups = [];
+            for (const key in response) {
+              if (!response.hasOwnProperty(key)) { continue; }
+              groups.push({
+                name: key,
+                value: response[key],
+              });
+            }
+            // Does not return groups in which we already have rules for.
+            return groups.filter(group => {
+              return !this._groupsWithRules[group.value.id];
+            });
+          });
+    },
+
+    /**
+     * Handles adding a skeleton item to the dom-repeat.
+     * gr-rule-editor handles setting the default values.
+     */
+    _handleAddRuleItem(e) {
+      this.set(['permission', 'value', 'rules', e.detail.value.id], {});
+
+      // Purposely don't recompute sorted array so that the newly added rule
+      // is the last item of the array.
+      this.push('_rules', {
+        id: e.detail.value.id,
+      });
+
+      // Wait for new rule to get value populated via gr-rule editor, and then
+      // add to permission values as well, so that the change gets propogated
+      // back to the section. Since the rule is inside a dom-repeat, a flush
+      // is needed.
+      Polymer.dom.flush();
+      this.set(['permission', 'value', 'rules', e.detail.value.id],
+          this._rules[this._rules.length - 1].value);
+    },
+  });
+})();
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
new file mode 100644
index 0000000..3c229d1
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -0,0 +1,304 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-permission</title>
+
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-permission.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-permission></gr-permission>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-permission tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+      sandbox.stub(element.$.restAPI, 'getSuggestedGroups').returns(
+          Promise.resolve({
+            'Administrators': {
+              id: '4c97682e6ce61b7247f3381b6f1789356666de7f',
+            },
+            'Anonymous Users': {
+              id: 'global%3AAnonymous-Users',
+            },
+          }));
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    suite('unit tests', () => {
+      test('_sortPermission', () => {
+        const permission = {
+          id: 'submit',
+          value: {
+            rules: {
+              'global:Project-Owners': {
+                action: 'ALLOW',
+                force: false,
+              },
+              '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+                action: 'ALLOW',
+                force: false,
+              },
+            },
+          },
+        };
+
+        const expectedRules = [
+          {
+            id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
+            value: {action: 'ALLOW', force: false},
+          },
+          {
+            id: 'global:Project-Owners',
+            value: {action: 'ALLOW', force: false},
+          },
+        ];
+
+        element._sortPermission(permission);
+        assert.deepEqual(element._rules, expectedRules);
+      });
+
+      test('_computeLabel and _computeLabelValues', () => {
+        const labels = {
+          'Code-Review': {
+            default_value: 0,
+            values: {
+              ' 0': 'No score',
+              '-1': 'I would prefer this is not merged as is',
+              '-2': 'This shall not be merged',
+              '+1': 'Looks good to me, but someone else must approve',
+              '+2': 'Looks good to me, approved',
+            },
+          },
+        };
+        const permission = {
+          id: 'label-Code-Review',
+          value: {
+            label: 'Code-Review',
+            rules: {
+              'global:Project-Owners': {
+                action: 'ALLOW',
+                force: false,
+                min: -2,
+                max: 2,
+              },
+              '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+                action: 'ALLOW',
+                force: false,
+                min: -2,
+                max: 2,
+              },
+            },
+          },
+        };
+
+        const expectedLabelValues = [
+          {value: -2, text: 'This shall not be merged'},
+          {value: -1, text: 'I would prefer this is not merged as is'},
+          {value: 0, text: 'No score'},
+          {value: 1, text: 'Looks good to me, but someone else must approve'},
+          {value: 2, text: 'Looks good to me, approved'},
+        ];
+
+        const expectedLabel = {
+          name: 'Code-Review',
+          values: expectedLabelValues,
+        };
+
+        assert.deepEqual(element._computeLabelValues(
+            labels['Code-Review'].values), expectedLabelValues);
+
+        assert.deepEqual(element._computeLabel(permission, labels),
+            expectedLabel);
+      });
+
+      test('_computeSectionClass', () => {
+        let deleted = true;
+        let editing = false;
+        assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
+
+        deleted = false;
+        assert.equal(element._computeSectionClass(editing, deleted), '');
+
+        editing = true;
+        assert.equal(element._computeSectionClass(editing, deleted), 'editing');
+
+        deleted = true;
+        assert.equal(element._computeSectionClass(editing, deleted),
+            'editing deleted');
+      });
+
+
+      test('_computeGroupsWithRules', () => {
+        const rules = [
+          {
+            id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
+            value: {action: 'ALLOW', force: false},
+          },
+          {
+            id: 'global:Project-Owners',
+            value: {action: 'ALLOW', force: false},
+          },
+        ];
+        const groupsWithRules = {
+          '4c97682e6ce6b7247f3381b6f1789356666de7f': true,
+          'global:Project-Owners': true,
+        };
+        assert.deepEqual(element._computeGroupsWithRules(rules),
+            groupsWithRules);
+      });
+
+      test('_getGroupSuggestions without existing rules', done => {
+        element._groupsWithRules = {};
+
+        element._getGroupSuggestions().then(groups => {
+          assert.deepEqual(groups, [
+            {
+              name: 'Administrators',
+              value: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f'},
+            }, {
+              name: 'Anonymous Users',
+              value: {id: 'global%3AAnonymous-Users'},
+            },
+          ]);
+          done();
+        });
+      });
+
+      test('_getGroupSuggestions with existing rules filters them', done => {
+        element._groupsWithRules = {
+          '4c97682e6ce61b7247f3381b6f1789356666de7f': true,
+        };
+
+        element._getGroupSuggestions().then(groups => {
+          assert.deepEqual(groups, [{
+            name: 'Anonymous Users',
+            value: {id: 'global%3AAnonymous-Users'},
+          }]);
+          done();
+        });
+      });
+
+      test('_handleRemovePermission', () => {
+        element.permission = {value: {rules: {}}};
+        element._handleRemovePermission();
+        assert.isTrue(element._deleted);
+        assert.isTrue(element.permission.value.deleted);
+      });
+
+      test('_handleUndoRemove', () => {
+        element.permission = {value: {deleted: true, rules: {}}};
+        element._handleUndoRemove();
+        assert.isFalse(element._deleted);
+        assert.isNotOk(element.permission.value.deleted);
+      });
+    });
+
+    suite('interactions', () => {
+      setup(() => {
+        sandbox.spy(element, '_computeLabel');
+        element.name = 'Priority';
+        element.section = 'refs/*';
+        element.labels = {
+          'Code-Review': {
+            values: {
+              ' 0': 'No score',
+              '-1': 'I would prefer this is not merged as is',
+              '-2': 'This shall not be merged',
+              '+1': 'Looks good to me, but someone else must approve',
+              '+2': 'Looks good to me, approved',
+            },
+            default_value: 0,
+          },
+        };
+        element.permission = {
+          id: 'label-Code-Review',
+          value: {
+            label: 'Code-Review',
+            rules: {
+              'global:Project-Owners': {
+                action: 'ALLOW',
+                force: false,
+                min: -2,
+                max: 2,
+              },
+              '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+                action: 'ALLOW',
+                force: false,
+                min: -2,
+                max: 2,
+              },
+            },
+          },
+        };
+        flushAsynchronousOperations();
+      });
+
+      test('adding a rule', () => {
+        element.name = 'Priority';
+        element.section = 'refs/*';
+        const e = {
+          detail: {
+            value: {
+              id: 'newUserGroupId',
+            },
+          },
+        };
+
+        assert.equal(element._rules.length, 2);
+        assert.equal(Object.keys(element._groupsWithRules).length, 2);
+        element._handleAddRuleItem(e);
+        flushAsynchronousOperations();
+        assert.equal(element._rules.length, 3);
+        assert.equal(Object.keys(element._groupsWithRules).length, 3);
+        assert.deepEqual(element.permission.value.rules['newUserGroupId'],
+            {action: 'ALLOW', min: -2, max: 2});
+      });
+
+      test('removing the permission', () => {
+        element.editing = true;
+        element.name = 'Priority';
+        element.section = 'refs/*';
+
+        assert.isFalse(element.$.permission.classList.contains('deleted'));
+        assert.isFalse(element._deleted);
+        MockInteractions.tap(element.$.removeBtn);
+        assert.isTrue(element.$.permission.classList.contains('deleted'));
+        assert.isTrue(element._deleted);
+        MockInteractions.tap(element.$.undoRemoveBtn);
+        assert.isFalse(element.$.permission.classList.contains('deleted'));
+        assert.isFalse(element._deleted);
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.html b/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.html
new file mode 100644
index 0000000..6c0908a
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.html
@@ -0,0 +1,92 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-create-change-dialog/gr-create-change-dialog.html">
+
+<dom-module id="gr-project-commands">
+  <template>
+    <style include="shared-styles">
+      main {
+        margin: 2em 1em;
+      }
+      .loading {
+        display: none;
+      }
+      #loading.loading {
+        display: block;
+      }
+      #loading:not(.loading) {
+        display: none;
+      }
+    </style>
+    <style include="gr-form-styles"></style>
+    <main class="gr-form-styles read-only">
+      <h1 id="Title">Project Commands</h1>
+      <div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div>
+      <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
+        <h2 id="options">Command</h2>
+        <div id="form">
+          <fieldset>
+            <h3 id="createChange">Create Change</h3>
+            <fieldset>
+              <gr-button id="createNewChange" on-tap="_createNewChange">
+                Create Change
+              </gr-button>
+            </fieldset>
+            <h3 id="runGC" hidden$="[[!_projectConfig.actions.gc.enabled]]">
+                Run GC
+            </h3>
+            <fieldset>
+              <gr-button
+                  on-tap="_handleRunningGC"
+                  hidden$="[[!_projectConfig.actions.gc.enabled]]">
+                Run GC
+              </gr-button>
+            </fieldset>
+          </fieldset>
+        </div>
+      </div>
+    </main>
+    <gr-overlay id="createChangeOverlay" with-backdrop>
+      <gr-confirm-dialog
+          id="createChangeDialog"
+          confirm-label="Create"
+          disabled="[[!_canCreate]]"
+          on-confirm="_handleCreateChange"
+          on-cancel="_handleCloseCreateChange">
+        <div class="header">
+          Create Change
+        </div>
+        <div class="main">
+          <gr-create-change-dialog
+              id="createNewChangeModal"
+              can-create="{{_canCreate}}"
+              project-name="[[project]]"></gr-create-change-dialog>
+        </div>
+      </gr-confirm-dialog>
+    </gr-overlay>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-project-commands.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.js b/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.js
new file mode 100644
index 0000000..88cf058
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.js
@@ -0,0 +1,80 @@
+// Copyright (C) 2017 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.
+(function() {
+  'use strict';
+
+  const GC_MESSAGE = 'Garbage collection completed successfully.';
+
+  Polymer({
+    is: 'gr-project-commands',
+
+    properties: {
+      params: Object,
+      project: String,
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+      /** @type {?} */
+      _projectConfig: Object,
+      _canCreate: Boolean,
+    },
+
+    attached() {
+      this._loadProject();
+
+      this.fire('title-change', {title: 'Project Commands'});
+    },
+
+    _loadProject() {
+      if (!this.project) { return Promise.resolve(); }
+
+      return this.$.restAPI.getProjectConfig(this.project).then(
+          config => {
+            this._projectConfig = config;
+            this._loading = false;
+          });
+    },
+
+    _computeLoadingClass(loading) {
+      return loading ? 'loading' : '';
+    },
+
+    _isLoading() {
+      return this._loading || this._loading === undefined;
+    },
+
+    _handleRunningGC() {
+      return this.$.restAPI.runProjectGC(this.project).then(response => {
+        if (response.status === 200) {
+          this.dispatchEvent(new CustomEvent('show-alert',
+              {detail: {message: GC_MESSAGE}, bubbles: true}));
+        }
+      });
+    },
+
+    _createNewChange() {
+      this.$.createChangeOverlay.open();
+    },
+
+    _handleCreateChange() {
+      this.$.createNewChangeModal.handleCreateChange();
+      this._handleCloseCreateChange();
+    },
+
+    _handleCloseCreateChange() {
+      this.$.createChangeOverlay.close();
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands_test.html b/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands_test.html
new file mode 100644
index 0000000..693f07e
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands_test.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-project-commands</title>
+
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-project-commands.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-project-commands></gr-project-commands>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-project-commands tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    suite('create new change dialog', () => {
+      test('_createNewChange opens modal', () => {
+        const openStub = sandbox.stub(element.$.createChangeOverlay, 'open');
+        element._createNewChange();
+        assert.isTrue(openStub.called);
+      });
+
+      test('_handleCreateChange called when confirm fired', () => {
+        sandbox.stub(element, '_handleCreateChange');
+        element.$.createChangeDialog.fire('confirm');
+        assert.isTrue(element._handleCreateChange.called);
+      });
+
+      test('_handleCloseCreateChange called when cancel fired', () => {
+        sandbox.stub(element, '_handleCloseCreateChange');
+        element.$.createChangeDialog.fire('cancel');
+        assert.isTrue(element._handleCloseCreateChange.called);
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-project/gr-project.html b/polygerrit-ui/app/elements/admin/gr-project/gr-project.html
index 780c706..aee0256 100644
--- a/polygerrit-ui/app/elements/admin/gr-project/gr-project.html
+++ b/polygerrit-ui/app/elements/admin/gr-project/gr-project.html
@@ -44,6 +44,12 @@
       #loading:not(.loading) {
         display: none;
       }
+      #noteDbSettings {
+        display: none;
+      }
+      #noteDbSettings.showNoteDb {
+        display: block;
+      }
     </style>
     <style include="gr-form-styles"></style>
     <main class="gr-form-styles read-only">
@@ -167,6 +173,38 @@
                   </gr-select>
                 </span>
               </section>
+              <section id="noteDbSettings class$=[[_computeNoteDbClass(_noteDbEnabled)]]">
+                <span class="title">
+                  Enable adding unregistered users as reviewers and CCs on changes</span>
+                <span class="value">
+                  <gr-select
+                      id="unRegisteredCcSelect"
+                      bind-value="{{_projectConfig.enable_reviewer_by_email.configured_value}}">
+                    <select disabled$="[[_readOnly]]">
+                      <template is="dom-repeat"
+                          items="[[_formatBooleanSelect(_projectConfig.enable_reviewer_by_email)]]">
+                        <option value="[[item.value]]">[[item.label]]</option>
+                      </template>
+                  </select>
+                  </gr-select>
+                </span>
+              </section>
+              <section>
+                <span class="title">
+                  Set all new changes private by default</span>
+                <span class="value">
+                  <gr-select
+                      id="setAllnewChangesPrivateByDefaultSelect"
+                      bind-value="{{_projectConfig.private_by_default.configured_value}}">
+                    <select disabled$="[[_readOnly]]">
+                      <template is="dom-repeat"
+                          items="[[_formatBooleanSelect(_projectConfig.private_by_default)]]">
+                        <option value="[[item.value]]">[[item.label]]</option>
+                      </template>
+                    </select>
+                  </gr-select>
+                </span>
+              </section>
               <section>
                 <span class="title">Maximum Git object size limit</span>
                 <span class="value">
diff --git a/polygerrit-ui/app/elements/admin/gr-project/gr-project.js b/polygerrit-ui/app/elements/admin/gr-project/gr-project.js
index b8a638a..0f14088 100644
--- a/polygerrit-ui/app/elements/admin/gr-project/gr-project.js
+++ b/polygerrit-ui/app/elements/admin/gr-project/gr-project.js
@@ -97,6 +97,10 @@
       },
       _selectedScheme: String,
       _schemesObj: Object,
+      _noteDbEnabled: {
+        type: Boolean,
+        value: false,
+      },
     },
 
     observers: [
@@ -134,6 +138,7 @@
 
       promises.push(this.$.restAPI.getConfig().then(config => {
         this._schemesObj = config.download.schemes;
+        this._noteDbEnabled = !!config.note_db_enabled;
       }));
 
       return Promise.all(promises);
@@ -249,5 +254,9 @@
       }
       return commands;
     },
+
+    _computeNoteDbClass(noteDB) {
+      return noteDB ? 'showNoteDb': '';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-project/gr-project_test.html b/polygerrit-ui/app/elements/admin/gr-project/gr-project_test.html
index fe5557a..4decadb 100644
--- a/polygerrit-ui/app/elements/admin/gr-project/gr-project_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-project/gr-project_test.html
@@ -77,10 +77,18 @@
               value: false,
               configured_value: 'FALSE',
             },
+            private_by_default: {
+              value: false,
+              configured_value: 'FALSE',
+            },
             match_author_to_committer_date: {
               value: false,
               configured_value: 'FALSE',
             },
+            enable_reviewer_by_email: {
+              value: false,
+              configured_value: 'FALSE',
+            },
             max_object_size_limit: {},
             submit_type: 'MERGE_IF_NECESSARY',
           });
@@ -221,6 +229,16 @@
       });
 
       test('fields update and save correctly', done => {
+        element._noteDbEnabled = false;
+
+        assert.equal(
+            element._computeNoteDbClass(element._noteDbEnabled), '');
+
+        element._noteDbEnabled = true;
+
+        assert.equal(
+            element._computeNoteDbClass(element._noteDbEnabled), 'showNoteDb');
+
         const configInputObj = {
           description: 'new description',
           use_contributor_agreements: 'TRUE',
@@ -229,10 +247,12 @@
           create_new_change_for_all_not_in_target: 'TRUE',
           require_change_id: 'TRUE',
           reject_implicit_merges: 'TRUE',
+          private_by_default: 'TRUE',
           match_author_to_committer_date: 'TRUE',
           max_object_size_limit: 10,
           submit_type: 'FAST_FORWARD_ONLY',
           state: 'READ_ONLY',
+          enable_reviewer_by_email: 'TRUE',
         };
 
         const saveStub = sandbox.stub(element.$.restAPI, 'saveProjectConfig'
@@ -256,6 +276,8 @@
               configInputObj.require_change_id;
           element.$.rejectImplicitMergesSelect.bindValue =
               configInputObj.reject_implicit_merges;
+          element.$.setAllnewChangesPrivateByDefaultSelect.bindValue =
+              configInputObj.private_by_default;
           element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
               configInputObj.match_author_to_committer_date;
           element.$.maxGitObjSizeInput.bindValue =
@@ -264,6 +286,8 @@
               configInputObj.use_contributor_agreements;
           element.$.useSignedOffBySelect.bindValue =
               configInputObj.use_signed_off_by;
+          element.$.unRegisteredCcSelect.bindValue =
+              configInputObj.enable_reviewer_by_email;
 
           assert.isFalse(button.hasAttribute('disabled'));
           assert.isTrue(element.$.configurations.classList.contains('edited'));
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
index 60b9ddf..0a8f382 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
@@ -16,6 +16,7 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 
+<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -30,10 +31,17 @@
         display: block;
       }
       .buttons {
+        display: none;
+      }
+      .editing .buttons {
         display: flex;
       }
-      .buttons gr-button {
-        margin-left: .3em;
+      #options {
+        align-items: baseline;
+        display: flex;
+      }
+      #options > * {
+        margin-right: .5em;
       }
       #mainContainer {
         align-items: baseline;
@@ -59,12 +67,12 @@
     </style>
     <style include="gr-form-styles"></style>
     <div id="mainContainer"
-        class$="gr-form-styles [[_computeDeletedClass(_deleted)]]">
+        class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
       <div id="options">
         <gr-select id="action"
             bind-value="{{rule.value.action}}"
             on-change="_handleValueChange">
-          <select>
+          <select disabled$="[[!editing]]">
             <template is="dom-repeat" items="[[_computeOptions(permission)]]">
               <option value="[[item]]">[[item]]</option>
             </template>
@@ -75,7 +83,7 @@
               id="labelMin"
               bind-value="{{rule.value.min}}"
               on-change="_handleValueChange">
-            <select>
+            <select disabled$="[[!editing]]">
               <template is="dom-repeat" items="[[label.values]]">
                 <option value="[[item.value]]">[[item.value]]</option>
               </template>
@@ -85,20 +93,20 @@
               id="labelMax"
               bind-value="{{rule.value.max}}"
               on-change="_handleValueChange">
-            <select>
+            <select disabled$="[[!editing]]">
               <template is="dom-repeat" items="[[label.values]]">
                 <option value="[[item.value]]">[[item.value]]</option>
               </template>
             </select>
           </gr-select>
         </template>
-        [[group]]
+        <span>[[group]]</span>
         <gr-select
             id="force"
             class$="[[_computeForceClass(permission)]]"
             bind-value="{{rule.value.force}}"
             on-change="_handleValueChange">
-          <select>
+          <select disabled$="[[!editing]]">
             <template
                 is="dom-repeat"
                 items="[[_computeForceOptions(permission)]]">
@@ -117,7 +125,7 @@
     </div>
     <div
         id="deletedContainer"
-        class$="gr-form-styles [[_computeDeletedClass(_deleted)]]">
+        class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
       [[group]] was deleted
       <gr-button id="undoRemoveBtn" on-tap="_handleUndoRemove">Undo</gr-button>
     </div>
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index da026db..6f404bf 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -53,6 +53,10 @@
     properties: {
       /** @type {?} */
       label: Object,
+      editing: {
+        type: Boolean,
+        value: false,
+      },
       group: String,
       permission: String,
       /** @type {?} */
@@ -72,6 +76,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.AccessBehavior,
+    ],
+
     observers: [
       '_handleValueChange(rule.value.*)',
     ],
@@ -91,21 +99,29 @@
     },
 
     _computeForce(permission) {
-      return 'push' === permission || 'editTopicName' === permission;
+      return this.permissionValues.push.id === permission ||
+          this.permissionValues.editTopicName.id === permission;
     },
 
     _computeForceClass(permission) {
       return this._computeForce(permission) ? 'force' : '';
     },
 
-    _computeDeletedClass(deleted) {
-      return deleted ? 'deleted' : '';
+    _computeSectionClass(editing, deleted) {
+      const classList = [];
+      if (editing) {
+        classList.push('editing');
+      }
+      if (deleted) {
+        classList.push('deleted');
+      }
+      return classList.join(' ');
     },
 
     _computeForceOptions(permission) {
-      if (permission === 'push') {
+      if (permission === this.permissionValues.push.id) {
         return FORCE_PUSH_OPTIONS;
-      } else if (permission === 'editTopicName') {
+      } else if (permission === this.permissionValues.editTopicName.id) {
         return FORCE_EDIT_OPTIONS;
       }
       return [];
@@ -140,12 +156,12 @@
 
     _handleRemoveRule() {
       this._deleted = true;
-      this.rule.deleted = true;
+      this.set('rule.value.deleted', true);
     },
 
     _handleUndoRemove() {
       this._deleted = false;
-      delete this.rule.deleted;
+      delete this.rule.value.deleted;
     },
 
     _handleUndoChange() {
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
index 376fb79..2b02419 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
@@ -86,9 +86,20 @@
             assert.deepEqual(element._computeForceOptions(permission), []);
           });
 
-      test('_computeDeletedClass', () => {
-        assert.equal(element._computeDeletedClass(true), 'deleted');
-        assert.equal(element._computeDeletedClass(false), '');
+      test('_computeSectionClass', () => {
+        let deleted = true;
+        let editing = false;
+        assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
+
+        deleted = false;
+        assert.equal(element._computeSectionClass(editing, deleted), '');
+
+        editing = true;
+        assert.equal(element._computeSectionClass(editing, deleted), 'editing');
+
+        deleted = true;
+        assert.equal(element._computeSectionClass(editing, deleted),
+            'editing deleted');
       });
 
       test('_getDefaultRuleValues', () => {
@@ -206,16 +217,28 @@
         assert.isFalse(element._modified);
       });
 
+      test('all selects are disabled when not in edit mode', () => {
+        const selects = Polymer.dom(element.root).querySelectorAll('select');
+        for (select of selects) {
+          assert.isTrue(select.disabled);
+        }
+        element.editing = true;
+        for (select of selects) {
+          assert.isFalse(select.disabled);
+        }
+      });
+
       test('remove rule and undo remove', () => {
-        element.rule = {id: 123};
+        element.editing = true;
+        element.rule = {id: 123, value: {action: 'ALLOW'}};
         assert.isFalse(
             element.$.deletedContainer.classList.contains('deleted'));
         MockInteractions.tap(element.$.removeBtn);
         assert.isTrue(element.$.deletedContainer.classList.contains('deleted'));
-        assert.isTrue(element.rule.deleted);
+        assert.isTrue(element.rule.value.deleted);
 
         MockInteractions.tap(element.$.undoRemoveBtn);
-        assert.isNotOk(element.rule.deleted);
+        assert.isNotOk(element.rule.value.deleted);
       });
     });
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
index d841fbc..35537e7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -74,18 +74,17 @@
       </tr>
       <template is="dom-repeat" items="[[sections]]" as="changeSection"
           index-as="sectionIndex">
-        <template is="dom-if" if="[[_sectionTitle(sectionIndex)]]">
+        <template is="dom-if" if="[[changeSection.sectionName]]">
           <tr class="groupHeader">
             <td class="cell"
                 colspan$="[[_computeColspan(changeTableColumns, labelNames)]]">
-              <a
-                  href$="[[_sectionHref(sectionIndex)]]">
-                [[_sectionTitle(sectionIndex)]]
+              <a href$="[[_sectionHref(changeSection.query)]]">
+                [[changeSection.sectionName]]
               </a>
             </td>
           </tr>
         </template>
-        <template is="dom-if" if="[[!changeSection.length]]">
+        <template is="dom-if" if="[[!changeSection.results.length]]">
           <tr class="noChanges">
             <td class="cell"
                 colspan$="[[_computeColspan(changeTableColumns, labelNames)]]">
@@ -93,7 +92,7 @@
             </td>
           </tr>
         </template>
-        <template is="dom-repeat" items="[[changeSection]]" as="change">
+        <template is="dom-repeat" items="[[changeSection.results]]" as="change">
           <gr-change-list-item
               selected$="[[_computeItemSelected(index, sectionIndex, selectedIndex)]]"
               assigned$="[[_computeItemAssigned(account, change)]]"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index d302120..63b42bb 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -55,15 +55,17 @@
       /**
        * ChangeInfo objects grouped into arrays. The sections and changes
        * properties should not be used together.
+       *
+       * @type {!Array<{
+       *   sectionName: string,
+       *   query: string,
+       *   results: !Array<!Object>
+       * }>}
        */
       sections: {
         type: Array,
         value() { return []; },
       },
-      sectionMetadata: {
-        type: Array,
-        value() { return []; },
-      },
       labelNames: {
         type: Array,
         computed: '_computeLabelNames(sections)',
@@ -152,10 +154,9 @@
       const nonExistingLabel = function(item) {
         return !labels.includes(item);
       };
-      for (let i = 0; i < sections.length; i++) {
-        const section = sections[i];
-        for (let j = 0; j < section.length; j++) {
-          const change = section[j];
+      for (const section of sections) {
+        if (!section.results) { continue; }
+        for (const change of section.results) {
           if (!change.labels) { continue; }
           const currentLabels = Object.keys(change.labels);
           labels = labels.concat(currentLabels.filter(nonExistingLabel));
@@ -171,17 +172,10 @@
     },
 
     _changesChanged(changes) {
-      this.sections = changes ? [changes] : [];
+      this.sections = changes ? [{results: changes}] : [];
     },
 
-    _sectionTitle(sectionIndex) {
-      if (sectionIndex > this.sectionMetadata.length - 1) { return null; }
-      return this.sectionMetadata[sectionIndex].name;
-    },
-
-    _sectionHref(sectionIndex) {
-      if (sectionIndex > this.sectionMetadata.length - 1) { return null; }
-      const query = this.sectionMetadata[sectionIndex].query;
+    _sectionHref(query) {
       return `${this.getBaseUrl()}/q/${this.encodeURL(query, true)}`;
     },
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index d758475..582a559 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -107,14 +107,26 @@
 
     test('computed fields', () => {
       assert.equal(element._computeLabelNames(
-          [[{_number: 0, labels: {}}]]).length, 0);
-      assert.equal(element._computeLabelNames([[
-            {_number: 0, labels: {Verified: {approved: {}}}},
-        {_number: 1, labels: {
-          'Verified': {approved: {}}, 'Code-Review': {approved: {}}}},
-        {_number: 2, labels: {
-          'Verified': {approved: {}}, 'Library-Compliance': {approved: {}}}},
-      ]]).length, 3);
+            [{results: [{_number: 0, labels: {}}]}]).length, 0);
+      assert.equal(element._computeLabelNames([
+        {results: [
+          {_number: 0, labels: {Verified: {approved: {}}}},
+          {
+            _number: 1,
+            labels: {
+              'Verified': {approved: {}},
+              'Code-Review': {approved: {}},
+            },
+          },
+          {
+            _number: 2,
+            labels: {
+              'Verified': {approved: {}},
+              'Library-Compliance': {approved: {}},
+            },
+          },
+        ]},
+      ]).length, 3);
 
       assert.equal(element._computeLabelShortcut('Code-Review'), 'CR');
       assert.equal(element._computeLabelShortcut('Verified'), 'V');
@@ -244,7 +256,7 @@
     });
 
     test('empty sections', () => {
-      element.sections = [[], []];
+      element.sections = [{results: []}, {results: []}];
       flushAsynchronousOperations();
       const listItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
@@ -382,31 +394,26 @@
     test('keyboard shortcuts', () => {
       element.selectedIndex = 0;
       element.sections = [
-        [
-          {_number: 0},
-          {_number: 1},
-          {_number: 2},
-        ],
-        [
-          {_number: 3},
-          {_number: 4},
-          {_number: 5},
-        ],
-        [
-          {_number: 6},
-          {_number: 7},
-          {_number: 8},
-        ],
-      ];
-      element.sectionMetadata = [
         {
-          name: 'Group 1',
+          results: [
+            {_number: 0},
+            {_number: 1},
+            {_number: 2},
+          ],
         },
         {
-          name: 'Group 2',
+          results: [
+            {_number: 3},
+            {_number: 4},
+            {_number: 5},
+          ],
         },
         {
-          name: 'Group 3',
+          results: [
+            {_number: 6},
+            {_number: 7},
+            {_number: 8},
+          ],
         },
       ];
       flushAsynchronousOperations();
@@ -469,14 +476,12 @@
     });
 
     test('_sectionHref', () => {
-      element.sectionMetadata = [
-        {query: 'is:open owner:self'},
-        {query: 'is:open ((reviewer:self -is:ignored) OR assignee:self)'},
-      ];
-
-      assert.equal(element._sectionHref(10), null);
-      assert.equal(element._sectionHref(0), '/q/is:open+owner:self');
-      assert.equal(element._sectionHref(1),
+      assert.equal(
+          element._sectionHref('is:open owner:self'),
+          '/q/is:open+owner:self');
+      assert.equal(
+          element._sectionHref(
+              'is:open ((reviewer:self -is:ignored) OR assignee:self)'),
           '/q/is:open+((reviewer:self+-is:ignored)+OR+assignee:self)');
     });
   });
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
index 63a6e0b..bec7429 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -46,8 +46,7 @@
           show-reviewed-state
           account="[[account]]"
           selected-index="{{viewState.selectedChangeIndex}}"
-          sections="{{_results}}"
-          section-metadata="[[sectionMetadata]]"></gr-change-list>
+          sections="[[_results]]"></gr-change-list>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index 69550b9..6c5bad3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -17,20 +17,22 @@
   const DEFAULT_SECTIONS = [
     {
       name: 'Work in progress',
-      query: 'is:open owner:self is:wip',
+      query: 'is:open owner:${user} is:wip',
+      selfOnly: true,
     },
     {
       name: 'Outgoing reviews',
-      query: 'is:open owner:self -is:wip',
+      query: 'is:open owner:${user} -is:wip',
     },
     {
       name: 'Incoming reviews',
-      query: 'is:open ((reviewer:self -owner:self -is:ignored) OR ' +
-          'assignee:self) -is:wip',
+      query: 'is:open ((reviewer:${user} -owner:${user} -is:ignored) OR ' +
+          'assignee:${user}) -is:wip',
     },
     {
       name: 'Recently closed',
-      query: 'is:closed (owner:self OR reviewer:self OR assignee:self)',
+      query: 'is:closed (owner:${user} OR reviewer:${user} OR ' +
+          'assignee:${user})',
       suffixForDashboard: '-age:4w limit:10',
     },
   ];
@@ -53,11 +55,10 @@
       viewState: Object,
       params: {
         type: Object,
-        observer: '_paramsChanged',
       },
 
       _results: Array,
-      sectionMetadata: {
+      _sectionMetadata: {
         type: Array,
         value() { return DEFAULT_SECTIONS; },
       },
@@ -71,6 +72,10 @@
       },
     },
 
+    observers: [
+      '_userChanged(params.user)',
+    ],
+
     behaviors: [
       Gerrit.RESTClientBehavior,
     ],
@@ -83,38 +88,53 @@
       );
     },
 
-    attached() {
-      this.fire('title-change', {title: 'My Reviews'});
+    _computeTitle(user) {
+      if (user === 'self') {
+        return 'My Reviews';
+      }
+      return 'Dashboard for ' + user;
     },
 
     /**
      * Allows a refresh if menu item is selected again.
      */
-    _paramsChanged() {
+    _userChanged(user) {
+      if (!user) { return; }
+
+      // NOTE: This method may be called before attachment. Fire title-change
+      // in an async so that attachment to the DOM can take place first.
+      this.async(
+          () => this.fire('title-change', {title: this._computeTitle(user)}));
+
       this._loading = true;
-      this._getChanges().then(results => {
-        this._results = results;
-        this._loading = false;
-      }).catch(err => {
-        this._loading = false;
-        console.warn(err.message);
-      });
+      const sections = this._sectionMetadata.filter(
+          section => (user === 'self' || !section.selfOnly));
+      const queries =
+          sections.map(
+              section => this._dashboardQueryForSection(section, user));
+      this.$.restAPI.getChanges(null, queries, null, this.options)
+          .then(results => {
+            this._results = sections.map((section, i) => {
+              return {
+                sectionName: section.name,
+                query: queries[i],
+                results: results[i],
+              };
+            });
+            this._loading = false;
+          }).catch(err => {
+            this._loading = false;
+            console.warn(err.message);
+          });
     },
 
-    _getChanges() {
-      return this.$.restAPI.getChanges(
-          null,
-          this.sectionMetadata.map(
-              section => this._dashboardQueryForSection(section)),
-          null,
-          this.options);
+    _dashboardQueryForSection(section, user) {
+      const query =
+          section.suffixForDashboard ?
+          section.query + ' ' + section.suffixForDashboard :
+          section.query;
+      return query.replace(/\$\{user\}/g, user);
     },
 
-    _dashboardQueryForSection(section) {
-      if (section.suffixForDashboard) {
-        return section.query + ' ' + section.suffixForDashboard;
-      }
-      return section.query;
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
index 0e73342..2edf26f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -39,30 +39,62 @@
     setup(() => {
       element = fixture('basic');
       sandbox = sinon.sandbox.create();
+      getChangesStub = sandbox.stub(element.$.restAPI, 'getChanges',
+          () => Promise.resolve());
     });
 
     teardown(() => {
       sandbox.restore();
     });
 
-    test('content is refreshed with same dropdown selected twice', () => {
-      const getChangesStub = sandbox.stub(element, '_getChanges',
-          () => Promise.resolve());
+    test('nothing happens when user param is falsy', () => {
+      element.params = {};
+      flushAsynchronousOperations();
+      assert.equal(getChangesStub.callCount, 0);
 
-      element.params = {view: Gerrit.Nav.View.DASHBOARD};
+      element.params = {user: ''};
+      flushAsynchronousOperations();
+      assert.equal(getChangesStub.callCount, 0);
+    });
 
+    test('content is refreshed when user param is updated', () => {
+      element.params = {user: 'self'};
+      flushAsynchronousOperations();
       assert.equal(getChangesStub.callCount, 1);
-      element.params = {view: Gerrit.Nav.View.DASHBOARD};
-      assert.equal(getChangesStub.callCount, 2);
+    });
+
+    test('viewing another user\'s dashboard omits selfOnly sections', () => {
+      element._sectionMetadata = [
+        {query: '1'},
+        {query: '2', selfOnly: true},
+      ];
+
+      element.params = {user: 'self'};
+      flushAsynchronousOperations();
+      assert.isTrue(
+          getChangesStub.calledWith(null, ['1', '2'], null, element.options));
+
+      element.params = {user: 'user'};
+      flushAsynchronousOperations();
+      assert.isTrue(
+          getChangesStub.calledWith(null, ['1'], null, element.options));
     });
 
     test('_dashboardQueryForSection', () => {
-      const query = 'query';
-      const suffixForDashboard = 'suffix';
-      assert.equal(element._dashboardQueryForSection({query}), 'query');
+      const query = 'query for ${user}';
+      const suffixForDashboard = 'suffix for ${user}';
       assert.equal(
-          element._dashboardQueryForSection({query, suffixForDashboard}),
-          'query suffix');
+          element._dashboardQueryForSection({query}, 'user'),
+          'query for user');
+      assert.equal(
+          element._dashboardQueryForSection(
+              {query, suffixForDashboard}, 'user'),
+          'query for user suffix for user');
+    });
+
+    test('_computeTitle', () => {
+      assert.equal(element._computeTitle('self'), 'My Reviews');
+      assert.equal(element._computeTitle('not self'), 'Dashboard for not self');
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index e9e60e0..4691836 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -118,6 +118,9 @@
     __type: 'revision',
   };
 
+  const AWAIT_CHANGE_ATTEMPTS = 5;
+  const AWAIT_CHANGE_TIMEOUT_MS = 1000;
+
   Polymer({
     is: 'gr-change-actions',
 
@@ -824,10 +827,9 @@
     // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved.
     _setLabelValuesOnRevert(newChangeId) {
       const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change);
-      if (labels) {
-        this.$.restAPI.getChangeURLAndSend(newChangeId,
-            this.actions.revert.method, 'current', '/review', {labels});
-      }
+      if (!labels) { return Promise.resolve(); }
+      return this.$.restAPI.getChangeURLAndSend(newChangeId,
+          this.actions.revert.method, 'current', '/review', {labels});
     },
 
     _handleResponse(action, response) {
@@ -835,10 +837,16 @@
       return this.$.restAPI.getResponseObject(response).then(obj => {
         switch (action.__key) {
           case ChangeActions.REVERT:
-            this._setLabelValuesOnRevert(obj.change_id);
-            /* falls through */
+            this._waitForChangeReachable(obj._number)
+                .then(() => this._setLabelValuesOnRevert(obj._number))
+                .then(() => {
+                  Gerrit.Nav.navigateToChange(obj);
+                });
+            break;
           case RevisionActions.CHERRYPICK:
-            page.show(this.changePath(obj._number));
+            this._waitForChangeReachable(obj._number).then(() => {
+              Gerrit.Nav.navigateToChange(obj);
+            });
             break;
           case ChangeActions.DELETE:
           case RevisionActions.DELETE:
@@ -1015,5 +1023,39 @@
         };
       });
     },
+
+    /**
+     * Occasionally, a change created by a change action is not yet knwon to the
+     * API for a brief time. Wait for the given change number to be recognized.
+     *
+     * Returns a promise that resolves with true if a request is recognized, or
+     * false if the change was never recognized after all attempts.
+     *
+     * @param  {number} changeNum
+     * @return {Promise<boolean>}
+     */
+    _waitForChangeReachable(changeNum) {
+      let attempsRemaining = AWAIT_CHANGE_ATTEMPTS;
+      return new Promise(resolve => {
+        const check = () => {
+          attempsRemaining--;
+          // Pass a no-op error handler to avoid the "not found" error toast.
+          this.$.restAPI.getChange(changeNum, () => {}).then(response => {
+            // If the response is 404, the response will be undefined.
+            if (response) {
+              resolve(true);
+              return;
+            }
+
+            if (attempsRemaining) {
+              this.async(check, AWAIT_CHANGE_TIMEOUT_MS);
+            } else {
+              resolve(false);
+            }
+          });
+        };
+        check();
+      });
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 1658da8..2390c21 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -1038,6 +1038,37 @@
         assert.strictEqual(
             element.$.moreActions.items[4].id, 'submit-revision');
       });
+
+      suite('_waitForChangeReachable', () => {
+        setup(() => {
+          sandbox.stub(element, 'async', fn => fn());
+        });
+
+        const makeGetChange = numTries => {
+          return () => {
+            if (numTries === 1) {
+              return Promise.resolve({_number: 123});
+            } else {
+              numTries--;
+              return Promise.resolve(undefined);
+            }
+          };
+        };
+
+        test('succeed', () => {
+          sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(5));
+          return element._waitForChangeReachable(123).then(success => {
+            assert.isTrue(success);
+          });
+        });
+
+        test('fail', () => {
+          sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(6));
+          return element._waitForChangeReachable(123).then(success => {
+            assert.isFalse(success);
+          });
+        });
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 449ef96..5a834b7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -16,20 +16,24 @@
 
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../plugins/gr-external-style/gr-external-style.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
+<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
 <link rel="import" href="../../shared/gr-label/gr-label.html">
 <link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
-<link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-change-metadata">
   <template>
     <style include="shared-styles">
+      .hideDisplay {
+        display: none;
+      }
       section:not(:first-of-type) {
         margin-top: 1em;
       }
@@ -137,6 +141,13 @@
           <gr-account-link account="[[change.owner]]"></gr-account-link>
         </span>
       </section>
+      <section class$="[[_computeShowUploaderHide(change)]]">
+        <span class="title">Uploader</span>
+        <span class="value">
+          <gr-account-link
+              account="[[_computeShowUploader(change)]]"></gr-account-link>
+        </span>
+      </section>
       <section class="assignee">
         <span class="title">Assignee</span>
         <span class="value">
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 32a7796..7ea7201 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -343,5 +343,25 @@
     _computeIsWip(change) {
       return !!change.work_in_progress;
     },
+
+    _computeShowUploaderHide(change) {
+      return this._computeShowUploader(change) ? '' : 'hideDisplay';
+    },
+
+    _computeShowUploader(change) {
+      if (!change.current_revision ||
+          !change.revisions[change.current_revision]) {
+        return null;
+      }
+
+      const rev = change.revisions[change.current_revision];
+
+      if (!rev || !rev.uploader ||
+        change.owner._account_id === rev.uploader._account_id) {
+        return null;
+      }
+
+      return rev.uploader;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 458e6ab..636f662 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -165,6 +165,117 @@
       assert.isTrue(showMissingSpy.called);
     });
 
+    test('_computeShowUploader test for uploader', () => {
+      const change = {
+        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+        owner: {
+          _account_id: 1019328,
+        },
+        revisions: {
+          rev1: {
+            _number: 1,
+            uploader: {
+              _account_id: 1011123,
+            },
+          },
+        },
+        current_revision: 'rev1',
+        status: 'NEW',
+        labels: {},
+        mergeable: true,
+      };
+      assert.deepEqual(element._computeShowUploader(change),
+          {_account_id: 1011123});
+    });
+
+    test('_computeShowUploader test that it does not return uploader', () => {
+      const change = {
+        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+        owner: {
+          _account_id: 1011123,
+        },
+        revisions: {
+          rev1: {
+            _number: 1,
+            uploader: {
+              _account_id: 1011123,
+            },
+          },
+        },
+        current_revision: 'rev1',
+        status: 'NEW',
+        labels: {},
+        mergeable: true,
+      };
+      assert.isNotOk(element._computeShowUploader(change));
+    });
+
+    test('no current_revision makes _computeShowUploader return null', () => {
+      const change = {
+        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+        owner: {
+          _account_id: 1011123,
+        },
+        revisions: {
+          rev1: {
+            _number: 1,
+            uploader: {
+              _account_id: 1011123,
+            },
+          },
+        },
+        status: 'NEW',
+        labels: {},
+        mergeable: true,
+      };
+      assert.isNotOk(element._computeShowUploader(change));
+    });
+
+    test('_computeShowUploaderHide test for string which equals true', () => {
+      const change = {
+        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+        owner: {
+          _account_id: 1019328,
+        },
+        revisions: {
+          rev1: {
+            _number: 1,
+            uploader: {
+              _account_id: 1011123,
+            },
+          },
+        },
+        current_revision: 'rev1',
+        status: 'NEW',
+        labels: {},
+        mergeable: true,
+      };
+      assert.equal(element._computeShowUploaderHide(change), '');
+    });
+
+    test('_computeShowUploaderHide test for hideDisplay', () => {
+      const change = {
+        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+        owner: {
+          _account_id: 1011123,
+        },
+        revisions: {
+          rev1: {
+            _number: 1,
+            uploader: {
+              _account_id: 1011123,
+            },
+          },
+        },
+        current_revision: 'rev1',
+        status: 'NEW',
+        labels: {},
+        mergeable: true,
+      };
+      assert.equal(
+          element._computeShowUploaderHide(change), 'hideDisplay');
+    });
+
     suite('Topic removal', () => {
       let change;
       setup(() => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index f9f1d03..527c83a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -163,10 +163,13 @@
         border: 1px solid #ddd;
         margin: 1em var(--default-horizontal-margin);
       }
-      .patchInfo--oldPatchSet .patchInfo-header {
+      .patchInfoEdit .patchInfo-header {
+        background-color: #fcfad6;
+      }
+      .patchInfoOldPatchSet .patchInfo-header {
         background-color: #fff9c4;
       }
-      .patchInfo--oldPatchSet .latestPatchContainer {
+      .patchInfoOldPatchSet .latestPatchContainer {
         display: initial;
       }
       .patchInfo-header,
@@ -200,7 +203,8 @@
         height: 0;
         margin-bottom: 1em;
       }
-      #diffPrefsContainer {
+      #diffPrefsContainer,
+      .rightControls {
         margin: auto 0 auto auto;
       }
       .patchInfo-header-wrapper {
@@ -239,6 +243,15 @@
         margin-left: 1em;
         padding-top: var(--related-change-btn-top-padding, 0);
       }
+      .showOnEdit {
+        display: none;
+      }
+      .editLoaded .hideOnEdit {
+        display: none;
+      }
+      .editLoaded .showOnEdit {
+        display: initial;
+      }
       @media screen and (min-width: 80em) {
         .commitMessage {
           max-width: var(--commit-message-max-width, 100ch);
@@ -316,7 +329,9 @@
       }
     </style>
     <div class="container loading" hidden$="[[!_loading]]">Loading...</div>
-    <div class="container" hidden$="{{_loading}}">
+    <div
+        class$="container [[_computeEditLoadedClass(_editLoaded)]]"
+        hidden$="{{_loading}}">
       <div class$="[[_computeHeaderClass(_change)]]">
         <span class="header-title">
           <gr-change-star
@@ -486,7 +501,7 @@
                   class="download"
                   on-tap="_handleDownloadTap">Download</gr-button>
             </span>
-            <span class="descriptionContainer">
+            <span class="descriptionContainer hideOnEdit">
               /
               <gr-editable-label
                   id="descriptionLabel"
@@ -497,6 +512,7 @@
                   on-changed="_handleDescriptionChanged"></gr-editable-label>
             </span>
             <span id="diffPrefsContainer"
+                class="hideOnEdit"
                 hidden$="[[_computePrefsButtonHidden(_diffPrefs, _loggedIn)]]"
                 hidden>
               <gr-button link
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 9c6ad1d..edf7dd3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -694,11 +694,15 @@
     },
 
     _computePatchInfoClass(patchNum, allPatchSets) {
+      if (this.patchNumEquals(patchNum, this.EDIT_NAME)) {
+        return 'patchInfoEdit';
+      }
+
       const latestNum = this.computeLatestPatchNum(allPatchSets);
       if (this.patchNumEquals(patchNum, latestNum)) {
         return '';
       }
-      return 'patchInfo--oldPatchSet';
+      return 'patchInfoOldPatchSet';
     },
 
     /**
@@ -1354,5 +1358,9 @@
       const patchRange = patchRangeRecord.base || {};
       return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
     },
+
+    _computeEditLoadedClass(editLoaded) {
+      return editLoaded ? 'editLoaded' : '';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 32b07cc..9bf7a7d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -922,9 +922,9 @@
     test('class is applied to file list on old patch set', () => {
       const allPatchSets = [{num: 1}, {num: 2}, {num: 4}];
       assert.equal(element._computePatchInfoClass('1', allPatchSets),
-          'patchInfo--oldPatchSet');
+          'patchInfoOldPatchSet');
       assert.equal(element._computePatchInfoClass('2', allPatchSets),
-          'patchInfo--oldPatchSet');
+          'patchInfoOldPatchSet');
       assert.equal(element._computePatchInfoClass('4', allPatchSets), '');
     });
 
@@ -1336,5 +1336,31 @@
       element._processEdit(mockChange = _.cloneDeep(change), edit);
       assert.equal(element._patchRange.patchNum, 'baz');
     });
+
+    suite('editLoaded behavior', () => {
+      setup(() => {
+        element._loggedIn = true;
+        element._diffPrefs = {};
+      });
+
+      const isVisible = el => {
+        assert.ok(el);
+        return getComputedStyle(el).getPropertyValue('display') !== 'none';
+      };
+
+      test('patch specific elements', () => {
+        sandbox.stub(element, 'computeLatestPatchNum').returns('2');
+        element._patchRange = {patchNum: element.EDIT_NAME};
+        flushAsynchronousOperations();
+
+        assert.isFalse(isVisible(element.$.diffPrefsContainer));
+        assert.isFalse(isVisible(element.$$('.descriptionContainer')));
+        element.set('_patchRange.patchNum', 1);
+        flushAsynchronousOperations();
+
+        assert.isTrue(isVisible(element.$$('.descriptionContainer')));
+        assert.isTrue(isVisible(element.$.diffPrefsContainer));
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
index 7260110..de63a02 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
@@ -53,6 +53,9 @@
         border: groove;
         width: 100%;
       }
+      .warning {
+        color: red;
+      }
     </style>
     <gr-confirm-dialog
         confirm-label="Move Change"
@@ -60,6 +63,9 @@
         on-cancel="_handleCancelTap">
       <div class="header">Move Change to Another Branch</div>
       <div class="main">
+        <p class="warning">
+          Warning: moving a change will not change its parents.
+        </p>
         <label for="branchInput">
           Move change to branch
         </label>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index 4ef0428..f330581 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -102,7 +102,9 @@
       // Get conflicts if change is open and is mergeable.
       if (this.changeIsOpen(this.change.status) && this.change.mergeable) {
         promises.push(this._getConflicts().then(response => {
-          this._conflicts = response;
+          // Because the server doesn't always return a response and the
+          // template expects an array, always return an array.
+          this._conflicts = response ? response : [];
         }));
       }
 
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
index 6336433..66d6b02 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -231,6 +231,34 @@
           change1, change2).indexOf('thisChange'), -1);
     });
 
+    suite('_getConflicts resolves undefined', () => {
+      let element;
+
+      setup(() => {
+        element = fixture('basic');
+
+        sandbox.stub(element, '_getRelatedChanges')
+            .returns(Promise.resolve({changes: []}));
+        sandbox.stub(element, '_getSubmittedTogether')
+            .returns(Promise.resolve());
+        sandbox.stub(element, '_getCherryPicks')
+            .returns(Promise.resolve());
+        sandbox.stub(element, '_getConflicts')
+            .returns(Promise.resolve());
+      });
+
+      test('_conflicts are an empty array', () => {
+        element.patchNum = 7;
+        element.change = {
+          change_id: 123,
+          status: 'NEW',
+          mergeable: true,
+        };
+        element.reload();
+        assert.equal(element._conflicts.length, 0);
+      });
+    });
+
     suite('get conflicts tests', () => {
       let element;
       let conflictsStub;
@@ -238,15 +266,14 @@
       setup(() => {
         element = fixture('basic');
 
-        sandbox.stub(element, '_getRelatedChanges', () => {
-          return Promise.resolve({changes: []});
-        });
-        sandbox.stub(element, '_getSubmittedTogether',
-            () => { return Promise.resolve(); });
-        sandbox.stub(element, '_getCherryPicks',
-            () => { return Promise.resolve(); });
-        conflictsStub = sandbox.stub(element, '_getConflicts',
-            () => { return Promise.resolve(['test data']); });
+        sandbox.stub(element, '_getRelatedChanges')
+            .returns(Promise.resolve({changes: []}));
+        sandbox.stub(element, '_getSubmittedTogether')
+            .returns(Promise.resolve());
+        sandbox.stub(element, '_getCherryPicks')
+            .returns(Promise.resolve());
+        conflictsStub = sandbox.stub(element, '_getConflicts')
+          .returns(Promise.resolve());
       });
 
       test('request conflicts if open and mergeable', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index 1242699..7089b2e 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -258,8 +258,8 @@
        */
       getUrlForOwner(owner) {
         return this._getUrlFor({
-          view: Gerrit.Nav.View.SEARCH,
-          owner,
+          view: Gerrit.Nav.View.DASHBOARD,
+          user: owner,
         });
       },
 
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index c952898..1e2cc70f 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -37,538 +37,32 @@
     getReporting().timeEnd('WebComponentsReady');
   });
 
-  const encode = window.Gerrit.URLEncodingBehavior.encodeURL;
-  const patchNumEquals = window.Gerrit.PatchSetBehavior.patchNumEquals;
-  const EDIT_NAME = window.Gerrit.PatchSetBehavior.EDIT_NAME;
-
-  function startRouter(generateUrl) {
-    const base = window.Gerrit.BaseUrlBehavior.getBaseUrl();
-    if (base) {
-      page.base(base);
-    }
-
-    const restAPI = document.createElement('gr-rest-api-interface');
-    const reporting = getReporting();
-
-    Gerrit.Nav.setup(url => { page.show(url); }, generateUrl);
-
-    /**
-     * Given a set of params without a project, gets the project from the rest
-     * API project lookup and then sets the app params.
-     *
-     * @param {?Object} params
-     */
-    const normalizeLegacyRouteParams = params => {
-      if (!params.changeNum) { return; }
-
-      restAPI.getFromProjectLookup(params.changeNum).then(project => {
-        params.project = project;
-        normalizePatchRangeParams(params);
-        page.redirect(generateUrl(params));
-      });
-    };
-
-    // Middleware
-    page((ctx, next) => {
-      document.body.scrollTop = 0;
-
-      // Fire asynchronously so that the URL is changed by the time the event
-      // is processed.
-      app.async(() => {
-        app.fire('location-change', {
-          hash: window.location.hash,
-          pathname: window.location.pathname,
-        });
-        reporting.locationChanged();
-      }, 1);
-      next();
-    });
-
-    function loadUser(ctx, next) {
-      restAPI.getLoggedIn().then(() => { next(); });
-    }
-
-    // Routes.
-    page('/', loadUser, data => {
-      if (data.querystring.match(/^closeAfterLogin/)) {
-        // Close child window on redirect after login.
-        window.close();
-      }
-      // For backward compatibility with GWT links.
-      if (data.hash) {
-        // In certain login flows the server may redirect to a hash without
-        // a leading slash, which page.js doesn't handle correctly.
-        if (data.hash[0] !== '/') {
-          data.hash = '/' + data.hash;
-        }
-        if (data.hash.includes('/ /') && data.canonicalPath.includes('/+/')) {
-          // Path decodes all '+' to ' ' -- this breaks project-based URLs.
-          // See Issue 6888.
-          data.hash = data.hash.replace('/ /', '/+/');
-        }
-        const hash = data.hash;
-        let newUrl = base + hash;
-        if (hash.startsWith('/VE/')) {
-          newUrl = base + '/settings' + data.hash;
-        }
-        page.redirect(newUrl);
-        return;
-      }
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          page.redirect('/dashboard/self');
-        } else {
-          page.redirect('/q/status:open');
-        }
-      });
-    });
-
-    function redirectToLogin(data) {
-      const basePath = base || '';
-      page('/login/' + encodeURIComponent(data.substring(basePath.length)));
-    }
-
-    page('/dashboard/(.*)', loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          data.params.view = Gerrit.Nav.View.DASHBOARD;
-          app.params = data.params;
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    // Matches /admin/groups/<group>,info (backwords compat with gwtui)
-    // Redirects to /admin/groups/<group>
-    page(/^\/admin\/groups\/(.+),info$/, loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          page.redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    // Matches /admin/groups/<group>,audit-log
-    page(/^\/admin\/groups\/(.+),audit-log$/, loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-group-audit-log',
-            detailType: 'audit-log',
-            groupId: data.params[0],
-          };
-        } else {
-          page.redirect('/login/' + encodeURIComponent(data.canonicalPath));
-        }
-      });
-    });
-
-    // Matches /admin/groups/<group>,members
-    page(/^\/admin\/groups\/(.+),members$/, loadUser, data => {
-      app.params = {
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-group-members',
-        detailType: 'members',
-        groupId: data.params[0],
-      };
-    });
-
-    // Matches /admin/groups[,<offset>][/].
-    page(/^\/admin\/groups(,(\d+))?(\/)?$/, loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-admin-group-list',
-            offset: data.params[1] || 0,
-            filter: null,
-          };
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    page('/admin/groups/q/filter::filter,:offset', loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-admin-group-list',
-            offset: data.params.offset,
-            filter: data.params.filter,
-          };
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    page('/admin/groups/q/filter::filter', loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-admin-group-list',
-            filter: data.params.filter || null,
-          };
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    // Matches /admin/groups/<group>
-    page(/^\/admin\/groups\/([^,]+)$/, loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-group',
-            groupId: data.params[0],
-          };
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    // Matches /admin/projects/<project>,branches[,<offset>].
-    page(/^\/admin\/projects\/(.+),branches(,(.+))?$/, loadUser, data => {
-      app.params = {
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-project-detail-list',
-        detailType: 'branches',
-        project: data.params[0],
-        offset: data.params[2] || 0,
-        filter: null,
-      };
-    });
-
-    page('/admin/projects/:project,branches/q/filter::filter,:offset',
-        loadUser, data => {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-project-detail-list',
-            detailType: 'branches',
-            project: data.params.project,
-            offset: data.params.offset,
-            filter: data.params.filter,
-          };
-        });
-
-    page('/admin/projects/:project,branches/q/filter::filter',
-        loadUser, data => {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-project-detail-list',
-            detailType: 'branches',
-            project: data.params.project,
-            filter: data.params.filter || null,
-          };
-        });
-
-    // Matches /admin/projects/<project>,tags[,<offset>].
-    page(/^\/admin\/projects\/(.+),tags(,(.+))?$/, loadUser, data => {
-      app.params = {
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-project-detail-list',
-        detailType: 'tags',
-        project: data.params[0],
-        offset: data.params[2] || 0,
-        filter: null,
-      };
-    });
-
-    page('/admin/projects/:project,tags/q/filter::filter,:offset',
-        loadUser, data => {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-project-detail-list',
-            detailType: 'tags',
-            project: data.params.project,
-            offset: data.params.offset,
-            filter: data.params.filter,
-          };
-        });
-
-    page('/admin/projects/:project,tags/q/filter::filter',
-        loadUser, data => {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-project-detail-list',
-            detailType: 'tags',
-            project: data.params.project,
-            filter: data.params.filter || null,
-          };
-        });
-
-    // Matches /admin/projects[,<offset>][/].
-    page(/^\/admin\/projects(,(\d+))?(\/)?$/, loadUser, data => {
-      app.params = {
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-admin-project-list',
-        offset: data.params[1] || 0,
-        filter: null,
-      };
-    });
-
-    page('/admin/projects/q/filter::filter,:offset', loadUser, data => {
-      app.params = {
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-admin-project-list',
-        offset: data.params.offset,
-        filter: data.params.filter,
-      };
-    });
-
-    page('/admin/projects/q/filter::filter', loadUser, data => {
-      app.params = {
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-admin-project-list',
-        filter: data.params.filter || null,
-      };
-    });
-
-    // Matches /admin/projects/<project>
-    page(/^\/admin\/projects\/([^,]+)$/, loadUser, data => {
-      app.params = {
-        view: Gerrit.Nav.View.ADMIN,
-        project: data.params[0],
-        adminView: 'gr-project',
-      };
-    });
-
-    // Matches /admin/plugins[,<offset>][/].
-    page(/^\/admin\/plugins(,(\d+))?(\/)?$/, loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-plugin-list',
-            offset: data.params[1] || 0,
-            filter: null,
-          };
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    page('/admin/plugins/q/filter::filter,:offset', loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-plugin-list',
-            offset: data.params.offset,
-            filter: data.params.filter,
-          };
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    page('/admin/plugins/q/filter::filter', loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-plugin-list',
-            filter: data.params.filter || null,
-          };
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    page(/^\/admin\/plugins(\/)?$/, loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          app.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-plugin-list',
-          };
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    page('/admin/(.*)', loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          data.params.view = Gerrit.Nav.View.ADMIN;
-          data.params.placeholder = true;
-          app.params = data.params;
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    function queryHandler(data) {
-      data.params.view = Gerrit.Nav.View.SEARCH;
-      app.params = data.params;
-    }
-
-    page('/q/:query,:offset', queryHandler);
-    page('/q/:query', queryHandler);
-
-    page(/^\/(\d+)\/?/, ctx => {
-      page.redirect('/c/' + encodeURIComponent(ctx.params[0]));
-    });
-
-    /**
-     * Normalizes the params object, and determines if the URL needs to be
-     * modified to fit the proper schema.
-     *
-     * @param {*} params
-     * @return {boolean} whether or not the URL needs to be upgraded.
-     */
-    const normalizePatchRangeParams = params => {
-      let needsRedirect = false;
-      if (params.basePatchNum &&
-          patchNumEquals(params.basePatchNum, params.patchNum)) {
-        needsRedirect = true;
-        params.basePatchNum = null;
-      } else if (params.basePatchNum && !params.patchNum) {
-        // Regexes set basePatchNum instead of patchNum when only one is
-        // specified. Redirect is not needed in this case.
-        params.patchNum = params.basePatchNum;
-        params.basePatchNum = null;
-      }
-      // In GWTUI, edits are represented in URLs with either 0 or 'edit'.
-      // TODO(kaspern): Remove this normalization when GWT UI is gone.
-      if (patchNumEquals(params.basePatchNum, 0)) {
-        params.basePatchNum = EDIT_NAME;
-        needsRedirect = true;
-      }
-      if (patchNumEquals(params.patchNum, 0)) {
-        params.patchNum = EDIT_NAME;
-        needsRedirect = true;
-      }
-      return needsRedirect;
-    };
-
-    // Matches
-    // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>]/[path].
-    // TODO(kaspern): Migrate completely to project based URLs, with backwards
-    // compatibility for change-only.
-    // eslint-disable-next-line max-len
-    page(/^\/c\/(.+)\/\+\/(\d+)(\/?((\d+|edit)(\.\.(\d+|edit))?(\/(.+))?))?\/?$/,
-        ctx => {
-          // Parameter order is based on the regex group number matched.
-          const params = {
-            project: ctx.params[0],
-            changeNum: ctx.params[1],
-            basePatchNum: ctx.params[4],
-            patchNum: ctx.params[6],
-            path: ctx.params[8],
-            view: ctx.params[8] ? Gerrit.Nav.View.DIFF : Gerrit.Nav.View.CHANGE,
-            hash: ctx.hash,
-          };
-          const needsRedirect = normalizePatchRangeParams(params);
-          if (needsRedirect) {
-            page.redirect(generateUrl(params));
-          } else {
-            app.params = params;
-            restAPI.setInProjectLookup(params.changeNum, params.project);
-          }
-        });
-
-    // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
-    page(/^\/c\/(\d+)\/?(((\d+|edit)(\.\.(\d+|edit))?))?\/?$/, ctx => {
-      // Parameter order is based on the regex group number matched.
-      const params = {
-        changeNum: ctx.params[0],
-        basePatchNum: ctx.params[3],
-        patchNum: ctx.params[5],
-        view: Gerrit.Nav.View.CHANGE,
-      };
-
-      normalizeLegacyRouteParams(params);
-    });
-
-    // Matches /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
-    page(/^\/c\/(\d+)\/((\d+|edit)(\.\.(\d+|edit))?)\/(.+)/, ctx => {
-      // Check if path has an '@' which indicates it was using GWT style line
-      // numbers. Even if the filename had an '@' in it, it would have already
-      // been URI encoded. Redirect to hash version of path.
-      if (ctx.path.includes('@')) {
-        page.redirect(ctx.path.replace('@', '#'));
-        return;
-      }
-
-      // Parameter order is based on the regex group number matched.
-      const params = {
-        changeNum: ctx.params[0],
-        basePatchNum: ctx.params[2],
-        patchNum: ctx.params[4],
-        path: ctx.params[5],
-        hash: ctx.hash,
-        view: Gerrit.Nav.View.DIFF,
-      };
-
-      normalizeLegacyRouteParams(params);
-    });
-
-    page(/^\/settings\/(agreements|new-agreement)/, loadUser, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          data.params.view = Gerrit.Nav.View.AGREEMENTS;
-          app.params = data.params;
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    page(/^\/settings\/VE\/(\S+)/, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          app.params = {
-            view: Gerrit.Nav.View.SETTINGS,
-            emailToken: data.params[0],
-          };
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    page(/^\/settings\/?/, data => {
-      restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          app.params = {view: Gerrit.Nav.View.SETTINGS};
-        } else {
-          redirectToLogin(data.canonicalPath);
-        }
-      });
-    });
-
-    page(/^\/register(\/.*)?/, ctx => {
-      app.params = {justRegistered: true};
-      const path = ctx.params[0] || '/';
-      if (path[0] !== '/') { return; }
-      page.show(base + path);
-    });
-
-    page.start();
-  }
-
   Polymer({
     is: 'gr-router',
-    behaviors: [Gerrit.PatchSetBehavior],
+
+    properties: {
+      _restAPI: {
+        type: Object,
+        value: () => document.createElement('gr-rest-api-interface'),
+      },
+    },
+
+    behaviors: [
+      Gerrit.URLEncodingBehavior,
+      Gerrit.PatchSetBehavior,
+    ],
+
     start() {
       if (!app) { return; }
-      startRouter(this._generateUrl.bind(this));
+      this._startRouter();
+    },
+
+    _setParams(params) {
+      app.params = params;
+    },
+
+    _redirect(url) {
+      page.redirect(url);
     },
 
     _generateUrl(params) {
@@ -578,28 +72,30 @@
       if (params.view === Gerrit.Nav.View.SEARCH) {
         const operators = [];
         if (params.owner) {
-          operators.push('owner:' + encode(params.owner));
+          operators.push('owner:' + this.encodeURL(params.owner, false));
         }
         if (params.project) {
-          operators.push('project:' + encode(params.project));
+          operators.push('project:' + this.encodeURL(params.project, false));
         }
         if (params.branch) {
-          operators.push('branch:' + encode(params.branch));
+          operators.push('branch:' + this.encodeURL(params.branch, false));
         }
         if (params.topic) {
-          operators.push('topic:"' + encode(params.topic) + '"');
+          operators.push('topic:"' + this.encodeURL(params.topic, false) + '"');
         }
         if (params.hashtag) {
           operators.push('hashtag:"' +
-              encode(params.hashtag.toLowerCase()) + '"');
+              this.encodeURL(params.hashtag.toLowerCase(), false) + '"');
         }
         if (params.statuses) {
           if (params.statuses.length === 1) {
-            operators.push('status:' + encode(params.statuses[0]));
+            operators.push(
+                'status:' + this.encodeURL(params.statuses[0], false));
           } else if (params.statuses.length > 1) {
             operators.push(
                 '(' +
-                params.statuses.map(s => `status:${encode(s)}`).join(' OR ') +
+                params.statuses.map(s => `status:${this.encodeURL(s, false)}`)
+                    .join(' OR ') +
                 ')');
           }
         }
@@ -612,11 +108,13 @@
         } else {
           url = `/c/${params.changeNum}${range}`;
         }
+      } else if (params.view === Gerrit.Nav.View.DASHBOARD) {
+        url = `/dashboard/${params.user || 'self'}`;
       } else if (params.view === Gerrit.Nav.View.DIFF) {
         let range = this._getPatchRangeExpression(params);
         if (range.length) { range = '/' + range; }
 
-        let suffix = `${range}/${encode(params.path, true)}`;
+        let suffix = `${range}/${this.encodeURL(params.path, true)}`;
         if (params.lineNum) {
           suffix += '#';
           if (params.leftSide) { suffix += 'b'; }
@@ -641,5 +139,573 @@
       if (params.basePatchNum) { range = params.basePatchNum + '..' + range; }
       return range;
     },
+
+    /**
+     * Given a set of params without a project, gets the project from the rest
+     * API project lookup and then sets the app params.
+     *
+     * @param {?Object} params
+     */
+    _normalizeLegacyRouteParams(params) {
+      if (!params.changeNum) { return Promise.resolve(); }
+
+      return this._restAPI.getFromProjectLookup(params.changeNum)
+          .then(project => {
+            params.project = project;
+            this._normalizePatchRangeParams(params);
+            this._redirect(this._generateUrl(params));
+          });
+    },
+
+    /**
+     * Normalizes the params object, and determines if the URL needs to be
+     * modified to fit the proper schema.
+     *
+     * @param {*} params
+     * @return {boolean} whether or not the URL needs to be upgraded.
+     */
+    _normalizePatchRangeParams(params) {
+      const hasBasePatchNum = params.basePatchNum !== null &&
+          params.basePatchNum !== undefined;
+      const hasPatchNum = params.patchNum !== null &&
+          params.patchNum !== undefined;
+      let needsRedirect = false;
+      if (hasBasePatchNum &&
+          this.patchNumEquals(params.basePatchNum, params.patchNum)) {
+        needsRedirect = true;
+        params.basePatchNum = null;
+      } else if (hasBasePatchNum && !hasPatchNum) {
+        // Regexes set basePatchNum instead of patchNum when only one is
+        // specified. Redirect is not needed in this case.
+        params.patchNum = params.basePatchNum;
+        params.basePatchNum = null;
+      }
+      // In GWTUI, edits are represented in URLs with either 0 or 'edit'.
+      // TODO(kaspern): Remove this normalization when GWT UI is gone.
+      if (this.patchNumEquals(params.basePatchNum, 0)) {
+        params.basePatchNum = this.EDIT_NAME;
+        needsRedirect = true;
+      }
+      if (this.patchNumEquals(params.patchNum, 0)) {
+        params.patchNum = this.EDIT_NAME;
+        needsRedirect = true;
+      }
+      return needsRedirect;
+    },
+
+    _redirectToLogin(data) {
+      const basePath = window.Gerrit.BaseUrlBehavior.getBaseUrl() || '';
+      page('/login/' + encodeURIComponent(data.substring(basePath.length)));
+    },
+
+    /**
+     * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c"
+     * is parsed to have a hash of "b" rather than "b#c". Instead, this method
+     * parses hashes correctly. Will return an empty string if there is no hash.
+     * @param {!string} canonicalPath
+     * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c").
+     */
+    _getHashFromCanonicalPath(canonicalPath) {
+      return canonicalPath.split('#').slice(1).join('#');
+    },
+
+    _startRouter() {
+      const base = window.Gerrit.BaseUrlBehavior.getBaseUrl();
+      if (base) {
+        page.base(base);
+      }
+
+      const reporting = getReporting();
+
+      Gerrit.Nav.setup(url => { page.show(url); },
+          this._generateUrl.bind(this));
+
+      // Middleware
+      page((ctx, next) => {
+        document.body.scrollTop = 0;
+
+        // Fire asynchronously so that the URL is changed by the time the event
+        // is processed.
+        this.async(() => {
+          app.fire('location-change', {
+            hash: window.location.hash,
+            pathname: window.location.pathname,
+          });
+          reporting.locationChanged();
+        }, 1);
+        next();
+      });
+
+      const loadUser = (ctx, next) => {
+        this._restAPI.getLoggedIn().then(() => { next(); });
+      };
+
+      // Routes.
+      page('/', loadUser, data => {
+        if (data.querystring.match(/^closeAfterLogin/)) {
+          // Close child window on redirect after login.
+          window.close();
+        }
+        let hash = this._getHashFromCanonicalPath(data.canonicalPath);
+        // For backward compatibility with GWT links.
+        if (hash) {
+          // In certain login flows the server may redirect to a hash without
+          // a leading slash, which page.js doesn't handle correctly.
+          if (hash[0] !== '/') {
+            hash = '/' + hash;
+          }
+          if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) {
+            // Path decodes all '+' to ' ' -- this breaks project-based URLs.
+            // See Issue 6888.
+            hash = hash.replace('/ /', '/+/');
+          }
+          let newUrl = base + hash;
+          if (hash.startsWith('/VE/')) {
+            newUrl = base + '/settings' + hash;
+          }
+          this._redirect(newUrl);
+          return;
+        }
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._redirect('/dashboard/self');
+          } else {
+            this._redirect('/q/status:open');
+          }
+        });
+      });
+
+      page('/dashboard/(.*)', loadUser, data => {
+        if (!data.params[0]) {
+          page.redirect('/dashboard/self');
+          return;
+        }
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (!loggedIn) {
+            if (data.params[0].toLowerCase() === 'self') {
+              this._redirectToLogin(data.canonicalPath);
+            } else {
+              this._redirect('/q/owner:' + data.params[0]);
+            }
+          } else {
+            this._setParams({
+              view: Gerrit.Nav.View.DASHBOARD,
+              user: data.params[0],
+            });
+          }
+        });
+      });
+
+      // Matches /admin/groups/<group>,info (backwords compat with gwtui)
+      // Redirects to /admin/groups/<group>
+      page(/^\/admin\/groups\/(.+),info$/, loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._redirect(
+                '/admin/groups/' + encodeURIComponent(data.params[0]));
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      // Matches /admin/groups/<group>,audit-log
+      page(/^\/admin\/groups\/(.+),audit-log$/, loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-group-audit-log',
+              detailType: 'audit-log',
+              groupId: data.params[0],
+            });
+          } else {
+            this._redirect('/login/' + encodeURIComponent(data.canonicalPath));
+          }
+        });
+      });
+
+      // Matches /admin/groups/<group>,members
+      page(/^\/admin\/groups\/(.+),members$/, loadUser, data => {
+        this._setParams({
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-group-members',
+          detailType: 'members',
+          groupId: data.params[0],
+        });
+      });
+
+      // Matches /admin/groups[,<offset>][/].
+      page(/^\/admin\/groups(,(\d+))?(\/)?$/, loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-admin-group-list',
+              offset: data.params[1] || 0,
+              filter: null,
+            });
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      page('/admin/groups/q/filter::filter,:offset', loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-admin-group-list',
+              offset: data.params.offset,
+              filter: data.params.filter,
+            });
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      page('/admin/groups/q/filter::filter', loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-admin-group-list',
+              filter: data.params.filter || null,
+            });
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      // Matches /admin/groups/<group>
+      page(/^\/admin\/groups\/([^,]+)$/, loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-group',
+              groupId: data.params[0],
+            });
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      // Matches /admin/projects/<project>,commands.
+      page(/^\/admin\/projects\/(.+),commands$/, loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-project-commands',
+              detailType: 'commands',
+              project: data.params[0],
+            });
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      // Matches /admin/projects/<project>,branches[,<offset>].
+      page(/^\/admin\/projects\/(.+),branches(,(.+))?$/, loadUser, data => {
+        this._setParams({
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-project-detail-list',
+          detailType: 'branches',
+          project: data.params[0],
+          offset: data.params[2] || 0,
+          filter: null,
+        });
+      });
+
+      page('/admin/projects/:project,branches/q/filter::filter,:offset',
+          loadUser, data => {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-project-detail-list',
+              detailType: 'branches',
+              project: data.params.project,
+              offset: data.params.offset,
+              filter: data.params.filter,
+            });
+          });
+
+      page('/admin/projects/:project,branches/q/filter::filter',
+          loadUser, data => {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-project-detail-list',
+              detailType: 'branches',
+              project: data.params.project,
+              filter: data.params.filter || null,
+            });
+          });
+
+      // Matches /admin/projects/<project>,tags[,<offset>].
+      page(/^\/admin\/projects\/(.+),tags(,(.+))?$/, loadUser, data => {
+        this._setParams({
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-project-detail-list',
+          detailType: 'tags',
+          project: data.params[0],
+          offset: data.params[2] || 0,
+          filter: null,
+        });
+      });
+
+      page('/admin/projects/:project,tags/q/filter::filter,:offset',
+          loadUser, data => {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-project-detail-list',
+              detailType: 'tags',
+              project: data.params.project,
+              offset: data.params.offset,
+              filter: data.params.filter,
+            });
+          });
+
+      page('/admin/projects/:project,tags/q/filter::filter',
+          loadUser, data => {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-project-detail-list',
+              detailType: 'tags',
+              project: data.params.project,
+              filter: data.params.filter || null,
+            });
+          });
+
+      // Matches /admin/projects[,<offset>][/].
+      page(/^\/admin\/projects(,(\d+))?(\/)?$/, loadUser, data => {
+        this._setParams({
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-admin-project-list',
+          offset: data.params[1] || 0,
+          filter: null,
+        });
+      });
+
+      page('/admin/projects/q/filter::filter,:offset', loadUser, data => {
+        this._setParams({
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-admin-project-list',
+          offset: data.params.offset,
+          filter: data.params.filter,
+        });
+      });
+
+      page('/admin/projects/q/filter::filter', loadUser, data => {
+        this._setParams({
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-admin-project-list',
+          filter: data.params.filter || null,
+        });
+      });
+
+      // Matches /admin/projects/<project>
+      page(/^\/admin\/projects\/([^,]+)$/, loadUser, data => {
+        this._setParams({
+          view: Gerrit.Nav.View.ADMIN,
+          project: data.params[0],
+          adminView: 'gr-project',
+        });
+      });
+
+      // Matches /admin/plugins[,<offset>][/].
+      page(/^\/admin\/plugins(,(\d+))?(\/)?$/, loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-plugin-list',
+              offset: data.params[1] || 0,
+              filter: null,
+            });
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      page('/admin/plugins/q/filter::filter,:offset', loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-plugin-list',
+              offset: data.params.offset,
+              filter: data.params.filter,
+            });
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      page('/admin/plugins/q/filter::filter', loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-plugin-list',
+              filter: data.params.filter || null,
+            });
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      page(/^\/admin\/plugins(\/)?$/, loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._setParams({
+              view: Gerrit.Nav.View.ADMIN,
+              adminView: 'gr-plugin-list',
+            });
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      page('/admin/(.*)', loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            data.params.view = Gerrit.Nav.View.ADMIN;
+            data.params.placeholder = true;
+            this._setParams(data.params);
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      const queryHandler = data => {
+        data.params.view = Gerrit.Nav.View.SEARCH;
+        this._setParams(data.params);
+      };
+
+      page('/q/:query,:offset', queryHandler);
+      page('/q/:query', queryHandler);
+
+      page(/^\/(\d+)\/?/, ctx => {
+        this._redirect('/c/' + encodeURIComponent(ctx.params[0]));
+      });
+
+      // Matches
+      // /c/<project>/+/<changeNum>/
+      //     [<basePatchNum|edit>..][<patchNum|edit>]/[path].
+      // TODO(kaspern): Migrate completely to project based URLs, with backwards
+      // compatibility for change-only.
+      // eslint-disable-next-line max-len
+      page(/^\/c\/(.+)\/\+\/(\d+)(\/?((\d+|edit)(\.\.(\d+|edit))?(\/(.+))?))?\/?$/,
+          ctx => {
+            // Parameter order is based on the regex group number matched.
+            const params = {
+              project: ctx.params[0],
+              changeNum: ctx.params[1],
+              basePatchNum: ctx.params[4],
+              patchNum: ctx.params[6],
+              path: ctx.params[8],
+              view: ctx.params[8] ?
+                Gerrit.Nav.View.DIFF : Gerrit.Nav.View.CHANGE,
+              hash: ctx.hash,
+            };
+            const needsRedirect = this._normalizePatchRangeParams(params);
+            if (needsRedirect) {
+              this._redirect(this._generateUrl(params));
+            } else {
+              this._setParams(params);
+              this._restAPI.setInProjectLookup(params.changeNum,
+                  params.project);
+            }
+          });
+
+      // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
+      page(/^\/c\/(\d+)\/?(((\d+|edit)(\.\.(\d+|edit))?))?\/?$/, ctx => {
+        // Parameter order is based on the regex group number matched.
+        const params = {
+          changeNum: ctx.params[0],
+          basePatchNum: ctx.params[3],
+          patchNum: ctx.params[5],
+          view: Gerrit.Nav.View.CHANGE,
+        };
+
+        this._normalizeLegacyRouteParams(params);
+      });
+
+      // Matches /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
+      page(/^\/c\/(\d+)\/((\d+|edit)(\.\.(\d+|edit))?)\/(.+)/, ctx => {
+        // Check if path has an '@' which indicates it was using GWT style line
+        // numbers. Even if the filename had an '@' in it, it would have already
+        // been URI encoded. Redirect to hash version of path.
+        if (ctx.path.includes('@')) {
+          this._redirect(ctx.path.replace('@', '#'));
+          return;
+        }
+
+        // Parameter order is based on the regex group number matched.
+        const params = {
+          changeNum: ctx.params[0],
+          basePatchNum: ctx.params[2],
+          patchNum: ctx.params[4],
+          path: ctx.params[5],
+          hash: ctx.hash,
+          view: Gerrit.Nav.View.DIFF,
+        };
+
+        this._normalizeLegacyRouteParams(params);
+      });
+
+      page(/^\/settings\/(agreements|new-agreement)/, loadUser, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            data.params.view = Gerrit.Nav.View.AGREEMENTS;
+            this._setParams(data.params);
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      page(/^\/settings\/VE\/(\S+)/, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._setParams({
+              view: Gerrit.Nav.View.SETTINGS,
+              emailToken: data.params[0],
+            });
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      page(/^\/settings\/?/, data => {
+        this._restAPI.getLoggedIn().then(loggedIn => {
+          if (loggedIn) {
+            this._setParams({view: Gerrit.Nav.View.SETTINGS});
+          } else {
+            this._redirectToLogin(data.canonicalPath);
+          }
+        });
+      });
+
+      page(/^\/register(\/.*)?/, ctx => {
+        this._setParams({justRegistered: true});
+        const path = ctx.params[0] || '/';
+        if (path[0] !== '/') { return; }
+        page.show(base + path);
+      });
+
+      page.start();
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index d1392ee..031cf85 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -33,13 +33,39 @@
 
 <script>
   suite('gr-router tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('_getHashFromCanonicalPath', () => {
+      let url = '/foo/bar';
+      let hash = element._getHashFromCanonicalPath(url);
+      assert.equal(hash, '');
+
+      url = '';
+      hash = element._getHashFromCanonicalPath(url);
+      assert.equal(hash, '');
+
+      url = '/foo#bar';
+      hash = element._getHashFromCanonicalPath(url);
+      assert.equal(hash, 'bar');
+
+      url = '/foo#bar#baz';
+      hash = element._getHashFromCanonicalPath(url);
+      assert.equal(hash, 'bar#baz');
+
+      url = '#foo#bar#baz';
+      hash = element._getHashFromCanonicalPath(url);
+      assert.equal(hash, 'foo#bar#baz');
+    });
+
     suite('generateUrl', () => {
-      let element;
-
-      setup(() => {
-        element = fixture('basic');
-      });
-
       test('search', () => {
         let params = {
           view: Gerrit.Nav.View.SEARCH,
@@ -109,6 +135,104 @@
         assert.equal(element._generateUrl(params),
             '/c/test/+/42/2/file.cpp#b123');
       });
+
+      test('_getPatchRangeExpression', () => {
+        const params = {};
+        let actual = element._getPatchRangeExpression(params);
+        assert.equal(actual, '');
+
+        params.patchNum = 4;
+        actual = element._getPatchRangeExpression(params);
+        assert.equal(actual, '4');
+
+        params.basePatchNum = 2;
+        actual = element._getPatchRangeExpression(params);
+        assert.equal(actual, '2..4');
+
+        delete params.patchNum;
+        actual = element._getPatchRangeExpression(params);
+        assert.equal(actual, '2..');
+      });
+    });
+
+    suite('param normalization', () => {
+      let projectLookupStub;
+
+      setup(() => {
+        projectLookupStub = sandbox
+            .stub(element._restAPI, 'getFromProjectLookup')
+            .returns(Promise.resolve('foo/bar'));
+        sandbox.stub(element, '_generateUrl');
+      });
+
+      suite('_normalizeLegacyRouteParams', () => {
+        let rangeStub;
+
+        setup(() => {
+          rangeStub = sandbox.stub(element, '_normalizePatchRangeParams')
+              .returns(Promise.resolve());
+        });
+
+        test('w/o changeNum', () => {
+          const params = {};
+          return element._normalizeLegacyRouteParams(params).then(() => {
+            assert.isFalse(projectLookupStub.called);
+            assert.isFalse(rangeStub.called);
+            assert.isNotOk(params.project);
+          });
+        });
+
+        test('w/ changeNum', () => {
+          const params = {changeNum: 1234};
+          return element._normalizeLegacyRouteParams(params).then(() => {
+            assert.isTrue(projectLookupStub.called);
+            assert.isTrue(rangeStub.called);
+            assert.equal(params.project, 'foo/bar');
+          });
+        });
+      });
+
+      suite('_normalizePatchRangeParams', () => {
+        test('range n..n normalizes to n', () => {
+          const params = {basePatchNum: 4, patchNum: 4};
+          const needsRedirect = element._normalizePatchRangeParams(params);
+          assert.isTrue(needsRedirect);
+          assert.isNotOk(params.basePatchNum);
+          assert.equal(params.patchNum, 4);
+        });
+
+        test('range n.. normalizes to n', () => {
+          const params = {basePatchNum: 4};
+          const needsRedirect = element._normalizePatchRangeParams(params);
+          assert.isFalse(needsRedirect);
+          assert.isNotOk(params.basePatchNum);
+          assert.equal(params.patchNum, 4);
+        });
+
+        test('range 0..n normalizes to edit..n', () => {
+          const params = {basePatchNum: 0, patchNum: 4};
+          const needsRedirect = element._normalizePatchRangeParams(params);
+          assert.isTrue(needsRedirect);
+          assert.equal(params.basePatchNum, 'edit');
+          assert.equal(params.patchNum, 4);
+        });
+
+        test('range n..0 normalizes to n..edit', () => {
+          const params = {basePatchNum: 4, patchNum: 0};
+          const needsRedirect = element._normalizePatchRangeParams(params);
+          assert.isTrue(needsRedirect);
+          assert.equal(params.basePatchNum, 4);
+          assert.equal(params.patchNum, 'edit');
+        });
+
+        test('range 0..0 normalizes to edit', () => {
+          const params = {basePatchNum: 0, patchNum: 0};
+          const needsRedirect = element._normalizePatchRangeParams(params);
+          assert.isTrue(needsRedirect);
+          assert.isNotOk(params.basePatchNum);
+          assert.equal(params.patchNum, 'edit');
+        });
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index 813d3c1..07e306a 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -16,33 +16,33 @@
 
   // Possible static search options for auto complete.
   const SEARCH_OPERATORS = [
-    'added',
-    'age',
+    'added:',
+    'age:',
     'age:1week', // Give an example age
-    'author',
-    'branch',
-    'bug',
-    'cc',
+    'author:',
+    'branch:',
+    'bug:',
+    'cc:',
     'cc:self',
-    'change',
-    'comment',
-    'commentby',
-    'commit',
-    'committer',
-    'conflicts',
-    'deleted',
-    'delta',
-    'file',
-    'from',
-    'has',
+    'change:',
+    'comment:',
+    'commentby:',
+    'commit:',
+    'committer:',
+    'conflicts:',
+    'deleted:',
+    'delta:',
+    'file:',
+    'from:',
+    'has:',
     'has:draft',
     'has:edit',
     'has:star',
     'has:stars',
     'has:unresolved',
-    'hashtag',
-    'intopic',
-    'is',
+    'hashtag:',
+    'intopic:',
+    'is:',
     'is:abandoned',
     'is:closed',
     'is:draft',
@@ -56,22 +56,22 @@
     'is:reviewer',
     'is:starred',
     'is:watched',
-    'label',
-    'message',
-    'owner',
-    'ownerin',
-    'parentproject',
-    'project',
-    'projects',
-    'query',
-    'ref',
-    'reviewedby',
-    'reviewer',
+    'label:',
+    'message:',
+    'owner:',
+    'ownerin:',
+    'parentproject:',
+    'project:',
+    'projects:',
+    'query:',
+    'ref:',
+    'reviewedby:',
+    'reviewer:',
     'reviewer:self',
-    'reviewerin',
-    'size',
-    'star',
-    'status',
+    'reviewerin:',
+    'size:',
+    'star:',
+    'status:',
     'status:abandoned',
     'status:closed',
     'status:draft',
@@ -79,8 +79,8 @@
     'status:open',
     'status:pending',
     'status:reviewed',
-    'topic',
-    'tr',
+    'topic:',
+    'tr:',
   ];
 
   const SELF_EXPRESSION = 'self';
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index b14ea7f..014b590 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -226,13 +226,14 @@
     },
 
     /**
-     * Get a short address for the location of the cursor. Such as '123' for
-     * line 123 of the revision, or 'b321' for line 321 of the base patch.
-     * Returns an empty string if an address is not available.
-     * @return {string}
+     * Get an object describing the location of the cursor. Such as
+     * {leftSide: false, number: 123} for line 123 of the revision, or
+     * {leftSide: true, number: 321} for line 321 of the base patch.
+     * Returns null if an address is not available.
+     * @return {?Object}
      */
     getAddress() {
-      if (!this.diffRow) { return ''; }
+      if (!this.diffRow) { return null; }
 
       // Get the line-number cell targeted by the cursor. If the mode is unified
       // then prefer the revision cell if available.
@@ -245,12 +246,15 @@
       } else {
         cell = this.diffRow.querySelector('.lineNum.' + this.side);
       }
-      if (!cell) { return ''; }
+      if (!cell) { return null; }
 
       const number = cell.getAttribute('data-value');
-      if (!number || number === 'FILE') { return ''; }
+      if (!number || number === 'FILE') { return null; }
 
-      return (cell.matches('.left') ? 'b' : '') + number;
+      return {
+        leftSide: cell.matches('.left'),
+        number: parseInt(number, 10),
+      };
     },
 
     _getViewMode() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index 15a405a..54f7d6f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -240,27 +240,31 @@
 
     test('getAddress', () => {
       // It should initialize to the first chunk: line 5 of the revision.
-      assert.equal(cursorElement.getAddress(), '5');
+      assert.deepEqual(cursorElement.getAddress(),
+          {leftSide: false, number: 5});
 
       // Revision line 4 is up.
       cursorElement.moveUp();
-      assert.equal(cursorElement.getAddress(), '4');
+      assert.deepEqual(cursorElement.getAddress(),
+          {leftSide: false, number: 4});
 
       // Base line 4 is left.
       cursorElement.moveLeft();
-      assert.equal(cursorElement.getAddress(), 'b4');
+      assert.deepEqual(cursorElement.getAddress(), {leftSide: true, number: 4});
 
       // Moving to the next chunk takes it back to the start.
       cursorElement.moveToNextChunk();
-      assert.equal(cursorElement.getAddress(), '5');
+      assert.deepEqual(cursorElement.getAddress(),
+          {leftSide: false, number: 5});
 
       // The following chunk is a removal starting on line 10 of the base.
       cursorElement.moveToNextChunk();
-      assert.equal(cursorElement.getAddress(), 'b10');
+      assert.deepEqual(cursorElement.getAddress(),
+          {leftSide: true, number: 10});
 
-      // Should be an empty string if there is no selection.
+      // Should be null if there is no selection.
       cursorElement.$.cursorManager.unsetCursor();
-      assert.equal(cursorElement.getAddress(), '');
+      assert.isNotOk(cursorElement.getAddress());
     });
 
     test('_findRowByNumberAndFile', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index fbd5b1b..409fd09 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -727,7 +727,13 @@
 
     _onLineSelected(e, detail) {
       this.$.cursor.moveToLineNumber(detail.number, detail.side);
-      history.replaceState(null, '', '#' + this.$.cursor.getAddress());
+      if (!this._change) { return; }
+      const cursorAddress = this.$.cursor.getAddress();
+      const url = Gerrit.Nav.getUrlForDiffById(this._changeNum,
+          this._change.project, this._path, this._patchRange.patchNum,
+          this._patchRange.basePatchNum, cursorAddress.number,
+          cursorAddress.leftSide);
+      history.replaceState(null, '', url);
     },
 
     _computeDownloadLink(changeNum, patchRange, path) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index e9c5de8..250e0f5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -620,9 +620,18 @@
     });
 
     test('_onLineSelected', () => {
+      const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
       const replaceStateStub = sandbox.stub(history, 'replaceState');
       const moveStub = sandbox.stub(element.$.cursor, 'moveToLineNumber');
+      sandbox.stub(element.$.cursor, 'getAddress')
+          .returns({number: 123, isLeftSide: false});
 
+      element._changeNum = 321;
+      element._change = {_number: 321, project: 'foo/bar'};
+      element._patchRange = {
+        basePatchNum: '3',
+        patchNum: '5',
+      };
       const e = {};
       const detail = {number: 123, side: 'right'};
 
@@ -633,6 +642,7 @@
       assert.equal(moveStub.lastCall.args[1], detail.side);
 
       assert.isTrue(replaceStateStub.called);
+      assert.isTrue(getUrlStub.called);
     });
 
     test('_getDiffViewMode', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index 9a287fc..a90158f 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -31,7 +31,8 @@
 
     _computeOwnerLink(account) {
       if (!account) { return; }
-      return Gerrit.Nav.getUrlForOwner(account.email || account._account_id);
+      return Gerrit.Nav.getUrlForOwner(
+          account.email || account.username || account._account_id);
     },
 
     _computeShowEmail(account) {
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
index fa3176c..e9ee51a 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -37,11 +37,6 @@
       Gerrit.URLEncodingBehavior,
     ],
 
-    listeners: {
-      'next-page': '_handleNextPage',
-      'previous-page': '_handlePreviousPage',
-    },
-
     detached() {
       this.cancelDebouncer('reload');
     },
@@ -74,7 +69,7 @@
       const newOffset = Math.max(0, offset + (itemsPerPage * direction));
       let href = this.getBaseUrl() + path;
       if (filter) {
-        href += '/q/filter:' + filter;
+        href += '/q/filter:' + this.encodeURL(filter, false);
       }
       if (newOffset > 0) {
         href += ',' + newOffset;
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
index 62494ce..680bf93 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
@@ -49,7 +49,7 @@
     test('_computeNavLink', () => {
       const offset = 25;
       const projectsPerPage = 25;
-      const filter = 'test';
+      let filter = 'test';
       const path = '/admin/projects';
 
       sandbox.stub(element, 'getBaseUrl', () => '');
@@ -69,6 +69,11 @@
       assert.equal(
           element._computeNavLink(offset, -1, projectsPerPage, null, path),
           '/admin/projects');
+
+      filter = 'plugins/';
+      assert.equal(
+          element._computeNavLink(offset, 1, projectsPerPage, filter, path),
+          '/admin/projects/q/filter:plugins%252F,50');
     });
 
     test('_onValueChange', done => {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
index c2da446..4179b46 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
@@ -68,7 +68,9 @@
      * @return {!Promise<!Response>}
      */
     fetch(url, opt_options) {
-      const options = Object.assign({}, this._defaultOptions, opt_options);
+      const options = Object.assign({
+        headers: new Headers(),
+      }, this._defaultOptions, opt_options);
       if (this._type === Gerrit.Auth.TYPE.ACCESS_TOKEN) {
         return this._getAccessToken().then(
             accessToken => this._fetchWithAccessToken(url, options, accessToken)
@@ -105,7 +107,6 @@
       if (options.method && options.method !== 'GET') {
         const token = this._getCookie('XSRF_TOKEN');
         if (token) {
-          options.headers = options.headers || new Headers();
           options.headers.append('X-Gerrit-Auth', token);
         }
       }
@@ -136,8 +137,8 @@
     },
 
     _fetchWithAccessToken(url, options, accessToken) {
-      const method = options.method || 'GET';
       const params = [];
+
       if (accessToken) {
         params.push(`access_token=${accessToken}`);
         const baseUrl = Gerrit.BaseUrlBehavior.getBaseUrl();
@@ -147,20 +148,30 @@
           url = url.replace(pathname, '/a' + pathname);
         }
       }
-      let contentType = options.headers && options.headers.get('Content-Type');
-      if (contentType) {
-        options.headers.set('Content-Type', 'text/plain');
+
+      const method = options.method || 'GET';
+      let contentType = options.headers.get('Content-Type');
+
+      // For all requests with body, ensure json content type.
+      if (!contentType && options.body) {
+        contentType = 'application/json';
       }
+
       if (method !== 'GET') {
         options.method = 'POST';
         params.push(`$m=${method}`);
-        if (!contentType && options.body) {
-          contentType = 'application/json';
+        // If a request is not GET, and does not have a body, ensure text/plain
+        // content type.
+        if (!contentType) {
+          contentType = 'text/plain';
         }
       }
+
       if (contentType) {
+        options.headers.set('Content-Type', 'text/plain');
         params.push(`$ct=${encodeURIComponent(contentType)}`);
       }
+
       if (params.length) {
         url = url + (url.indexOf('?') === -1 ? '?' : '&') + params.join('&');
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
index 7bbc0ca..c254e62 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
@@ -93,7 +93,7 @@
           const [url, options] = fetch.lastCall.args;
           assert.equal(url, '/url');
           assert.equal(options.bar, 'bar');
-          assert.isUndefined(options.headers);
+          assert.equal(Object.keys(options.headers).length, 0);
         });
       });
 
@@ -151,7 +151,7 @@
         }));
       });
 
-      test('HTTP PUT', () => {
+      test('HTTP PUT with content type', () => {
         const originalOptions = {
           method: 'PUT',
           headers: new Headers({'Content-Type': 'mail/pigeon'}),
@@ -166,6 +166,21 @@
           assert.equal(options.headers.get('Content-Type'), 'text/plain');
         });
       });
+
+      test('HTTP PUT without content type', () => {
+        const originalOptions = {
+          method: 'PUT',
+        };
+        return auth.fetch('/url', originalOptions).then(() => {
+          assert.isTrue(getToken.called);
+          const [url, options] = fetch.lastCall.args;
+          assert.include(url, '$ct=text%2Fplain');
+          assert.include(url, '$m=PUT');
+          assert.include(url, 'access_token=zbaz');
+          assert.equal(options.method, 'POST');
+          assert.equal(options.headers.get('Content-Type'), 'text/plain');
+        });
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index cd85b2c..0820e47 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -217,6 +217,15 @@
           opt_errFn, opt_ctx);
     },
 
+    runProjectGC(project, opt_errFn, opt_ctx) {
+      if (!project) {
+        return '';
+      }
+      const encodeName = encodeURIComponent(project);
+      return this.send('POST', `/projects/${encodeName}/gc`, '',
+          opt_errFn, opt_ctx);
+    },
+
     /**
      * @param {?Object} config
      * @param {function(?Response, string=)=} opt_errFn
@@ -1175,6 +1184,22 @@
       });
     },
 
+    /**
+     * @param {!string} project
+     * @param {!string} branch
+     * @param {!string} subject
+     * @param {!string} topic
+     * @param {!boolean} isPrivate
+     * @param {!boolean} workInProgress
+     */
+    createChange(project, branch, subject, topic, isPrivate,
+        workInProgress) {
+      return this.send('POST', '/changes/',
+          {project, branch, subject, topic, is_private: isPrivate,
+            work_in_progress: workInProgress})
+          .then(response => this.getResponseObject(response));
+    },
+
     getFileInChangeEdit(changeNum, path) {
       const e = '/edit/' + encodeURIComponent(path);
       return this.getChangeURLAndSend(changeNum, 'GET', null, e);
@@ -1713,11 +1738,12 @@
      * Given a changeNum, gets the change.
      *
      * @param {number|string} changeNum
+     * @param {function(?Response, string=)=} opt_errFn
      * @return {!Promise<?Object>} The change
      */
-    getChange(changeNum) {
+    getChange(changeNum, opt_errFn) {
       // Cannot use _changeBaseURL, as this function is used by _projectLookup.
-      return this.fetchJSON(`/changes/${changeNum}`);
+      return this.fetchJSON(`/changes/${changeNum}`, opt_errFn);
     },
 
     /**
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index b39a3f0..b63258f 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -775,7 +775,7 @@
       });
     });
 
-    test.only('_getChangeDetail calls errFn on 500', () => {
+    test('_getChangeDetail calls errFn on 500', () => {
       const errFn = sinon.stub();
       sandbox.stub(element, '_fetchRawJSON')
           .returns(Promise.resolve({ok: false, status: 500}));
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index 22b483d..d68433c 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -58,6 +58,5 @@
     // Character is an ellipsis.
     return '\u2026/' + pathPieces[pathPieces.length - 1];
   };
-
   window.util = util;
 })(window);
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index b171e9c..3a3c242 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -30,18 +30,22 @@
     // This seemed to be flakey when it was farther down the list. Keep at the
     // beginning.
     'gr-app_test.html',
+    'admin/gr-access-section/gr-access-section_test.html',
     'admin/gr-admin-group-list/gr-admin-group-list_test.html',
     'admin/gr-admin-project-list/gr-admin-project-list_test.html',
     'admin/gr-admin-view/gr-admin-view_test.html',
     'admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html',
+    'admin/gr-create-change-dialog/gr-create-change-dialog_test.html',
     'admin/gr-create-group-dialog/gr-create-group-dialog_test.html',
     'admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html',
     'admin/gr-create-project-dialog/gr-create-project-dialog_test.html',
     'admin/gr-group/gr-group_test.html',
     'admin/gr-group-audit-log/gr-group-audit-log_test.html',
     'admin/gr-group-members/gr-group-members_test.html',
+    'admin/gr-permission/gr-permission_test.html',
     'admin/gr-plugin-list/gr-plugin-list_test.html',
     'admin/gr-project/gr-project_test.html',
+    'admin/gr-project-commands/gr-project-commands_test.html',
     'admin/gr-project-detail-list/gr-project-detail-list_test.html',
     'admin/gr-rule-editor/gr-rule-editor_test.html',
     'change-list/gr-change-list-item/gr-change-list-item_test.html',
@@ -152,6 +156,7 @@
     'docs-url-behavior/docs-url-behavior_test.html',
     'keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html',
     'rest-client-behavior/rest-client-behavior_test.html',
+    'gr-access-behavior/gr-access-behavior_test.html',
     'gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html',
     'gr-change-table-behavior/gr-change-table-behavior_test.html',
     'gr-patch-set-behavior/gr-patch-set-behavior_test.html',
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index f7cab2e..79cf4bf 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -200,7 +200,7 @@
 
 // Any path prefixes that should resolve to index.html.
 var (
-	fePaths    = []string{"/q/", "/c/", "/dashboard/"}
+	fePaths    = []string{"/q/", "/c/", "/dashboard/", "/admin/"}
 	issueNumRE = regexp.MustCompile(`^\/\d+\/?$`)
 )
 
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
index 5b9242e..9448ed1 100644
--- a/tools/bzl/classpath.bzl
+++ b/tools/bzl/classpath.bzl
@@ -1,5 +1,5 @@
 def _classpath_collector(ctx):
-    all = set()
+    all = depset()
     for d in ctx.attr.deps:
         if hasattr(d, 'java'):
             all += d.java.transitive_runtime_deps
diff --git a/tools/bzl/gwt.bzl b/tools/bzl/gwt.bzl
index ef182bf..b0e250d 100644
--- a/tools/bzl/gwt.bzl
+++ b/tools/bzl/gwt.bzl
@@ -189,7 +189,7 @@
   )
 
 def _get_transitive_closure(ctx):
-  deps = set()
+  deps = depset()
   for dep in ctx.attr.module_deps:
     deps += dep.java.transitive_runtime_deps
     deps += dep.java.transitive_source_jars
diff --git a/tools/bzl/javadoc.bzl b/tools/bzl/javadoc.bzl
index 341b9c1..18ca129 100644
--- a/tools/bzl/javadoc.bzl
+++ b/tools/bzl/javadoc.bzl
@@ -17,8 +17,8 @@
 def _impl(ctx):
   zip_output = ctx.outputs.zip
 
-  transitive_jar_set = set()
-  source_jars = set()
+  transitive_jar_set = depset()
+  source_jars = depset()
   for l in ctx.attr.libs:
     source_jars += l.java.source_jars
     transitive_jar_set += l.java.transitive_deps
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 788301c..39d6acf 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -124,18 +124,18 @@
 )
 
 def _bower_component_impl(ctx):
-  transitive_zipfiles = set([ctx.file.zipfile])
+  transitive_zipfiles = depset([ctx.file.zipfile])
   for d in ctx.attr.deps:
     transitive_zipfiles += d.transitive_zipfiles
 
-  transitive_licenses = set()
+  transitive_licenses = depset()
   if ctx.file.license:
-    transitive_licenses += set([ctx.file.license])
+    transitive_licenses += depset([ctx.file.license])
 
   for d in ctx.attr.deps:
     transitive_licenses += d.transitive_licenses
 
-  transitive_versions = set(ctx.files.version_json)
+  transitive_versions = depset(ctx.files.version_json)
   for d in ctx.attr.deps:
     transitive_versions += d.transitive_versions
 
@@ -173,13 +173,13 @@
     command = cmd,
     mnemonic = "GenBowerZip")
 
-  licenses = set()
+  licenses = depset()
   if ctx.file.license:
-    licenses += set([ctx.file.license])
+    licenses += depset([ctx.file.license])
 
   return struct(
     transitive_zipfiles=list([ctx.outputs.zip]),
-    transitive_versions=set([]),
+    transitive_versions=depset(),
     transitive_licenses=licenses)
 
 js_component = rule(
@@ -219,15 +219,15 @@
 
 def _bower_component_bundle_impl(ctx):
   """A bunch of bower components zipped up."""
-  zips = set([])
+  zips = depset()
   for d in ctx.attr.deps:
     zips += d.transitive_zipfiles
 
-  versions = set([])
+  versions = depset()
   for d in ctx.attr.deps:
     versions += d.transitive_versions
 
-  licenses = set([])
+  licenses = depset()
   for d in ctx.attr.deps:
     licenses += d.transitive_versions
 
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index edaaab0..ebb632f 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -73,7 +73,7 @@
   ]
 
   # Add lib
-  transitive_lib_deps = set()
+  transitive_lib_deps = depset()
   for l in ctx.attr.libs:
     if hasattr(l, 'java'):
       transitive_lib_deps += l.java.transitive_runtime_deps
@@ -85,7 +85,7 @@
     inputs.append(dep)
 
   # Add pgm lib
-  transitive_pgmlib_deps = set()
+  transitive_pgmlib_deps = depset()
   for l in ctx.attr.pgmlibs:
     transitive_pgmlib_deps += l.java.transitive_runtime_deps
 
@@ -95,7 +95,7 @@
       inputs.append(dep)
 
   # Add context
-  transitive_context_deps = set()
+  transitive_context_deps = depset()
   if ctx.attr.context:
     for jar in ctx.attr.context:
       if hasattr(jar, 'java'):
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 11ac572..1223f02 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -59,7 +59,7 @@
   if gwt_module:
     native.java_library(
       name = name + '__gwt_module',
-      resources = list(set(srcs + resources)),
+      resources = depset(srcs + resources).to_list(),
       runtime_deps = deps + GWT_PLUGIN_DEPS,
       visibility = ['//visibility:public'],
       **kwargs
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
index f7b5aa8..2426b9f 100755
--- a/tools/maven/mvn.py
+++ b/tools/maven/mvn.py
@@ -19,6 +19,10 @@
 from subprocess import check_output
 from sys import stderr
 
+
+def mvn():
+  return ['mvn', '--file', path.join(root, 'fake_pom.xml'), '-DgroupId=com.google.gerrit']
+
 opts = OptionParser()
 opts.add_option('--repository', help='maven repository id')
 opts.add_option('--url', help='maven repository url')
@@ -37,14 +41,12 @@
   root = path.dirname(root)
 
 if 'install' == args.a:
-  cmd = [
-    'mvn',
+  cmd = mvn() + [
     'install:install-file',
     '-Dversion=%s' % args.v,
   ]
 elif 'deploy' == args.a:
-  cmd = [
-    'mvn',
+  cmd = mvn() + [
     'gpg:sign-and-deploy-file',
     '-DrepositoryId=%s' % args.repository,
     '-Durl=%s' % args.url,
@@ -56,7 +58,7 @@
 for spec in args.s:
   artifact, packaging_type, src = spec.split(':')
   exe = cmd + [
-    '-DpomFile=%s' % path.join(root, '%s/pom.xml' % artifact),
+    '-DartifactId=%s' % artifact,
     '-Dpackaging=%s' % packaging_type,
     '-Dfile=%s' % src,
   ]
diff --git a/tools/version.py b/tools/version.py
deleted file mode 100755
index fed6d5d..0000000
--- a/tools/version.py
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env python
-# Copyright (C) 2014 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from __future__ import print_function
-from optparse import OptionParser
-import os.path
-import re
-import sys
-
-parser = OptionParser()
-opts, args = parser.parse_args()
-
-if not len(args):
-  parser.error('not enough arguments')
-elif len(args) > 1:
-  parser.error('too many arguments')
-
-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-acceptance-framework', 'gerrit-extension-api',
-                'gerrit-plugin-api', 'gerrit-plugin-gwtui',
-                'gerrit-war']:
-  pom = os.path.join(project, 'pom.xml')
-  replace_in_file(pom, src_pattern)
-
-src_pattern = re.compile(r'^(GERRIT_VERSION = ")([-.\w]+)(")$', re.MULTILINE)
-replace_in_file('version.bzl', src_pattern)