Merge "Add configuration of key exchange algorithms for sshd"
diff --git a/.gitmodules b/.gitmodules
index d75c98c..6c4d53c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,23 +1,34 @@
 [submodule "plugins/commit-message-length-validator"]
 	path = plugins/commit-message-length-validator
 	url = ../plugins/commit-message-length-validator
+	branch = .
 
 [submodule "plugins/cookbook-plugin"]
 	path = plugins/cookbook-plugin
 	url = ../plugins/cookbook-plugin
+	branch = .
 
 [submodule "plugins/download-commands"]
 	path = plugins/download-commands
 	url = ../plugins/download-commands
+	branch = .
+
+[submodule "plugins/hooks"]
+	path = plugins/hooks
+	url = ../plugins/hooks
+	branch = .
 
 [submodule "plugins/replication"]
 	path = plugins/replication
 	url = ../plugins/replication
+	branch = .
 
 [submodule "plugins/reviewnotes"]
 	path = plugins/reviewnotes
 	url = ../plugins/reviewnotes
+	branch = .
 
 [submodule "plugins/singleusergroup"]
 	path = plugins/singleusergroup
 	url = ../plugins/singleusergroup
+	branch = .
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 995b155..51b1c68 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -421,10 +421,10 @@
 To block push permission to `+refs/drafts/*+` the following permission rule can
 be configured:
 
-====
+----
   [access "refs/drafts/*"]
     push = block group Anonymous Users
-====
+----
 
 
 [[access_categories]]
@@ -564,6 +564,19 @@
 new changes for code review, this depends on which namespace the
 permission is granted to.
 
+[[category_add_patch_set]]
+=== Add Patch Set
+
+This category controls which users are allowed to upload new patch sets to
+existing changes. Irrespective of this permission, change owners are always
+allowed to upload new patch sets for their changes. This permission needs to be
+set on `refs/for/*`.
+
+The absence of this permission will prevent users from uploading a
+patch set to a change they do not own. By default, this permission is granted to
+`Registered Users` on `refs/for/*` allowing all registered users to upload a new
+patch set to any change on that ref.
+
 
 [[category_push_direct]]
 ==== Direct Push
@@ -637,15 +650,15 @@
 project's repository.  Typically this would be done with a command line
 such as:
 
-====
+----
   git push ssh://USER@HOST:PORT/PROJECT tag v1.0
-====
+----
 
 Or:
 
-====
+----
   git push https://HOST/PROJECT tag v1.0
-====
+----
 
 Tags must be annotated (created with `git tag -a`), should exist in
 the `refs/tags/` namespace, and should be new.
@@ -677,15 +690,15 @@
 project's repository.  Typically this would be done with a command
 line such as:
 
-====
+----
   git push ssh://USER@HOST:PORT/PROJECT tag v1.0
-====
+----
 
 Or:
 
-====
+----
   git push https://HOST/PROJECT tag v1.0
-====
+----
 
 Tags must be signed (created with `git tag -s`), should exist in the
 `refs/tags/` namespace, and should be new.
@@ -1069,10 +1082,10 @@
 '-2' and '+2', but keep their existing voting permissions for the '-1..+1'
 range intact we would define:
 
-====
+----
   [access "refs/heads/*"]
     label-Code-Review = block -2..+2 group X
-====
+----
 
 The interpretation of the 'min..max' range in case of a blocking rule is: block
 every vote from '-INFINITE..min' and 'max..INFINITE'. For the example above it
@@ -1083,16 +1096,17 @@
 When an access section of a project contains a 'BLOCK' and an 'ALLOW' rule for
 the same permission then this 'ALLOW' rule overrides the 'BLOCK' rule:
 
-====
+----
   [access "refs/heads/*"]
     push = block group X
     push = group Y
-====
+----
 
 In this case a user which is a member of the group 'Y' will still be allowed to
 push to 'refs/heads/*' even if it is a member of the group 'X'.
 
-NOTE: An 'ALLOW' rule overrides a 'BLOCK' rule only when both of them are
+[NOTE]
+An 'ALLOW' rule overrides a 'BLOCK' rule only when both of them are
 inside the same access section of the same project. An 'ALLOW' rule in a
 different access section of the same project or in any access section in an
 inheriting project cannot override a 'BLOCK' rule.
@@ -1108,22 +1122,22 @@
 reproducibility of a build must be guaranteed. To achieve that we block 'push'
 permission for the <<anonymous_users,'Anonymous Users'>> in "`All-Projects`":
 
-====
+----
   [access "refs/tags/*"]
     push = block group Anonymous Users
-====
+----
 
 By blocking the <<anonymous_users,'Anonymous Users'>> we effectively block
 everyone as everyone is a member of that group. Note that the permission to
 create a tag is still necessary. Assuming that only <<category_owner,project
 owners>> are allowed to create tags, we would extend the example above:
 
-====
+----
   [access "refs/tags/*"]
     push = block group Anonymous Users
     create = group Project Owners
     pushTag = group Project Owners
-====
+----
 
 
 ==== Let only a dedicated group vote in a special category
@@ -1136,11 +1150,11 @@
 in this category and, of course, allow 'Release Engineers' to vote in that
 category. In the "`All-Projects`" we define the following rules:
 
-====
+----
   [access "refs/heads/stable*"]
     label-Release-Process = block -1..+1 group Anonymous Users
     label-Release-Process = -1..+1 group Release Engineers
-====
+----
 
 [[global_capabilities]]
 == Global Capabilities
diff --git a/Documentation/cmd-apropos.txt b/Documentation/cmd-apropos.txt
index 8882af18..31d21c1 100644
--- a/Documentation/cmd-apropos.txt
+++ b/Documentation/cmd-apropos.txt
@@ -4,8 +4,9 @@
 gerrit apropos - Search Gerrit documentation index
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit apropos'
+_ssh_ -p <port> <host> _gerrit apropos_
   <query>
 --
 
@@ -19,12 +20,14 @@
 == SCRIPTING
 This command is intended to be used in scripts.
 
-Note: this feature is only available if documentation index was built.
+[NOTE]
+This feature is only available if documentation index was built.
 
 == EXAMPLES
 
-=====
+----
 $ ssh -p 29418 review.example.com gerrit apropos capabilities
+
     Gerrit Code Review - /config/ REST API:
     http://localhost:8080/Documentation/rest-api-config.html
 
@@ -45,7 +48,7 @@
 
     Gerrit Code Review - /access/ REST API:
     http://localhost:8080/Documentation/rest-api-access.html
-=====
+----
 
 == SEE ALSO
 
diff --git a/Documentation/cmd-ban-commit.txt b/Documentation/cmd-ban-commit.txt
index d5c09af..80f41f0 100644
--- a/Documentation/cmd-ban-commit.txt
+++ b/Documentation/cmd-ban-commit.txt
@@ -4,8 +4,9 @@
 gerrit ban-commit - Bans a commit from a project's repository.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit ban-commit'
+_ssh_ -p <port> <host> _gerrit ban-commit_
   [--reason <REASON>]
   <PROJECT>
   <COMMIT> ...
@@ -43,10 +44,10 @@
 Ban commit `421919d015c062fd28901fe144a78a555d0b5984` from project
 `myproject`:
 
-====
+----
 	$ ssh -p 29418 review.example.com gerrit ban-commit myproject \
 	421919d015c062fd28901fe144a78a555d0b5984
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-cherry-pick.txt b/Documentation/cmd-cherry-pick.txt
index 0c4cf91..de0b71b 100644
--- a/Documentation/cmd-cherry-pick.txt
+++ b/Documentation/cmd-cherry-pick.txt
@@ -4,10 +4,11 @@
 gerrit-cherry-pick - Download and cherry pick one or more changes
 
 == SYNOPSIS
+[verse]
 --
-'gerrit-cherry-pick' <remote> <changeid>...
-'gerrit-cherry-pick' --continue | --skip | --abort
-'gerrit-cherry-pick' --close <remote>
+_gerrit-cherry-pick_ <remote> <changeid>...
+_gerrit-cherry-pick_ --continue | --skip | --abort
+_gerrit-cherry-pick_ --close <remote>
 --
 
 == DESCRIPTION
@@ -32,11 +33,11 @@
 To obtain the 'gerrit-cherry-pick' script use scp, curl or wget to
 copy it to your local system:
 
-====
+----
   $ scp -p -P 29418 john.doe@review.example.com:bin/gerrit-cherry-pick ~/bin/
 
   $ curl -Lo ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-close-connection.txt b/Documentation/cmd-close-connection.txt
index 3314326..973441e 100644
--- a/Documentation/cmd-close-connection.txt
+++ b/Documentation/cmd-close-connection.txt
@@ -4,8 +4,9 @@
 gerrit close-connection - Close the specified SSH connection
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit close-connection' <SESSION_ID>
+_ssh_ -p <port> <host> _gerrit close-connection_ <SESSION_ID>
    [--wait]
 --
 
diff --git a/Documentation/cmd-create-account.txt b/Documentation/cmd-create-account.txt
index 2159e0e..62bd0aa 100644
--- a/Documentation/cmd-create-account.txt
+++ b/Documentation/cmd-create-account.txt
@@ -4,8 +4,9 @@
 gerrit create-account - Create a new user account.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit create-account'
+_ssh_ -p <port> <host> _gerrit create-account_
   [--group <GROUP>]
   [--full-name <FULLNAME>]
   [--email <EMAIL>]
@@ -67,9 +68,9 @@
 Create a new batch/role access user account called `watcher` in
 the 'Non-Interactive Users' group.
 
-====
+----
 	$ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit create-account --group "'Non-Interactive Users'" --ssh-key - watcher
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-create-branch.txt b/Documentation/cmd-create-branch.txt
index 671adfe..336af56d 100644
--- a/Documentation/cmd-create-branch.txt
+++ b/Documentation/cmd-create-branch.txt
@@ -4,8 +4,9 @@
 gerrit create-branch - Create a new branch
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit create-branch'
+_ssh_ -p <port> <host> _gerrit create-branch_
   <PROJECT>
   <NAME>
   <REVISION>
@@ -38,9 +39,9 @@
 Create a new branch called 'newbranch' from the 'master' branch of
 the project 'myproject'.
 
-====
+----
     $ ssh -p 29418 review.example.com gerrit create-branch myproject newbranch master
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-create-group.txt b/Documentation/cmd-create-group.txt
index d02e2ea..7f1f463 100644
--- a/Documentation/cmd-create-group.txt
+++ b/Documentation/cmd-create-group.txt
@@ -4,8 +4,9 @@
 gerrit create-group - Create a new account group.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit create-group'
+_ssh_ -p <port> <host> _gerrit create-group_
   [--owner <GROUP> | -o <GROUP>]
   [--description <DESC> | -d <DESC>]
   [--member <USERNAME>]
@@ -66,16 +67,16 @@
 Create a new account group called `gerritdev` with two initial members
 `developer1` and `developer2`.  The group should be owned by itself:
 
-====
+----
 	$ ssh -p 29418 user@review.example.com gerrit create-group --member developer1 --member developer2 gerritdev
-====
+----
 
 Create a new account group called `Foo` owned by the `Foo-admin` group.
 Put `developer1` as the initial member and include group description:
 
-====
+----
 	$ ssh -p 29418 user@review.example.com gerrit create-group --owner Foo-admin --member developer1 --description "'Foo description'" Foo
-====
+----
 
 Note that it is necessary to quote the description twice.  The local
 shell needs double quotes around the value to ensure the single quotes
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index d1108b5..503bd12 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -4,8 +4,9 @@
 gerrit create-project - Create a new hosted project
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit create-project'
+_ssh_ -p <port> <host> _gerrit create-project_
   [--owner <GROUP> ... | -o <GROUP> ...]
   [--parent <NAME> | -p <NAME> ]
   [--suggest-parents | -S ]
@@ -170,15 +171,15 @@
 == EXAMPLES
 Create a new project called `tools/gerrit`:
 
-====
+----
 	$ ssh -p 29418 review.example.com gerrit create-project tools/gerrit.git
-====
+----
 
 Create a new project with a description:
 
-====
+----
 	$ ssh -p 29418 review.example.com gerrit create-project tool.git --description "'Tools used by build system'"
-====
+----
 
 Note that it is necessary to quote the description twice.  The local
 shell needs double quotes around the value to ensure the single quotes
@@ -189,9 +190,9 @@
 If the replication plugin is installed, the plugin will attempt to
 perform remote repository creation by a Bourne shell script:
 
-====
+----
   mkdir -p '/base/project.git' && cd '/base/project.git' && git init --bare && git update-ref HEAD refs/heads/master
-====
+----
 
 For this to work successfully the remote system must be able to run
 arbitrary shell scripts, and must have `git` in the user's PATH
diff --git a/Documentation/cmd-flush-caches.txt b/Documentation/cmd-flush-caches.txt
index aa9790d..4716f3b 100644
--- a/Documentation/cmd-flush-caches.txt
+++ b/Documentation/cmd-flush-caches.txt
@@ -4,10 +4,11 @@
 gerrit flush-caches - Flush some/all server caches from memory
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit flush-caches' --all
-'ssh' -p <port> <host> 'gerrit flush-caches' --list
-'ssh' -p <port> <host> 'gerrit flush-caches' --cache <NAME> ...
+_ssh_ -p <port> <host> _gerrit flush-caches_ --all
+_ssh_ -p <port> <host> _gerrit flush-caches_ --list
+_ssh_ -p <port> <host> _gerrit flush-caches_ --cache <NAME> ...
 --
 
 == DESCRIPTION
@@ -56,7 +57,7 @@
 == EXAMPLES
 List caches available for flushing:
 
-====
+----
 	$ ssh -p 29418 review.example.com gerrit flush-caches --list
 	accounts
 	accounts_byemail
@@ -67,32 +68,32 @@
 	projects
 	sshkeys
 	web_sessions
-====
+----
 
 Flush all caches known to the server, forcing them to recompute:
 
-====
+----
 	$ ssh -p 29418 review.example.com gerrit flush-caches --all
-====
+----
 
 or
 
-====
+----
 	$ ssh -p 29418 review.example.com gerrit flush-caches
-====
+----
 
 Flush only the "sshkeys" cache, after manually editing an SSH key
 for a user:
 
-====
+----
 	$ ssh -p 29418 review.example.com gerrit flush-caches --cache sshkeys
-====
+----
 
 Flush "web_sessions", forcing all users to sign-in again:
 
-====
+----
 	$ ssh -p 29418 review.example.com gerrit flush-caches --cache web_sessions
-====
+----
 
 == SEE ALSO
 
diff --git a/Documentation/cmd-gc.txt b/Documentation/cmd-gc.txt
index b7388a1..1d1cc00 100644
--- a/Documentation/cmd-gc.txt
+++ b/Documentation/cmd-gc.txt
@@ -4,8 +4,9 @@
 gerrit gc - Run the Git garbage collection
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit gc'
+_ssh_ -p <port> <host> _gerrit gc_
   [--all]
   [--show-progress]
   [--aggressive]
@@ -52,7 +53,7 @@
 
 Run the Git garbage collection for the projects 'myProject' and
 'yourProject':
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit gc myProject yourProject
 	collecting garbage for "myProject":
 	...
@@ -61,12 +62,12 @@
 	collecting garbage for "yourProject":
 	...
 	done.
-=====
+----
 
 Run the Git garbage collection for all projects:
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit gc --all
-=====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-gsql.txt b/Documentation/cmd-gsql.txt
index 411eb00..d2eb783 100644
--- a/Documentation/cmd-gsql.txt
+++ b/Documentation/cmd-gsql.txt
@@ -4,8 +4,9 @@
 gerrit gsql - Administrative interface to active database
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit gsql'
+_ssh_ -p <port> <host> _gerrit gsql_
   [--format {PRETTY | JSON | JSON_SINGLE}]
   [-c QUERY]
 --
@@ -40,7 +41,7 @@
 == EXAMPLES
 To manually correct a user's SSH user name:
 
-====
+----
 	$ ssh -p 29418 review.example.com gerrit gsql
 	Welcome to Gerrit Code Review v2.0.25
 	(PostgreSQL 8.3.8)
@@ -53,7 +54,7 @@
 	Bye
 
 	$ ssh -p 29418 review.example.com gerrit flush-caches --cache sshkeys --cache accounts
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-hook-commit-msg.txt b/Documentation/cmd-hook-commit-msg.txt
index e102186..ffdd5da 100644
--- a/Documentation/cmd-hook-commit-msg.txt
+++ b/Documentation/cmd-hook-commit-msg.txt
@@ -63,26 +63,26 @@
 
 You can use either of the below commands:
 
-====
+----
   $ scp -p -P 29418 <your username>@<your Gerrit review server>:hooks/commit-msg <local path to your git>/.git/hooks/
 
   $ curl -Lo <local path to your git>/.git/hooks/commit-msg <your Gerrit http URL>/tools/hooks/commit-msg
-====
+----
 
 A specific example of this might look something like this:
 
 .Example
-====
+----
   $ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg ~/duhproject/.git/hooks/
 
   $ curl -Lo ~/duhproject/.git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
-====
+----
 
 Make sure the hook file is executable:
 
-====
+----
   $ chmod u+x ~/duhproject/.git/hooks/commit-msg
-====
+----
 
 == SEE ALSO
 
diff --git a/Documentation/cmd-index-activate.txt b/Documentation/cmd-index-activate.txt
index 6cb7781..418e872 100644
--- a/Documentation/cmd-index-activate.txt
+++ b/Documentation/cmd-index-activate.txt
@@ -4,8 +4,9 @@
 gerrit index activate - Activate the latest index version available
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit index activate <index>'
+_ssh_ -p <port> <host> _gerrit index activate <INDEX>_
 --
 
 == DESCRIPTION
@@ -18,15 +19,26 @@
 This command allows to activate the latest index even if there were some
 failures.
 
-The <index> argument controls which secondary index is activated. Currently, the
-only supported value is "changes".
-
 == ACCESS
 Caller must be a member of the privileged 'Administrators' group.
 
 == SCRIPTING
 This command is intended to be used in scripts.
 
+== OPTIONS
+<INDEX>::
+  The index to activate.
+  Currently supported values:
+    * changes
+    * accounts
+
+== EXAMPLES
+Activate the latest change index:
+
+----
+  $ ssh -p 29418 review.example.com gerrit activate changes
+----
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-index-changes.txt b/Documentation/cmd-index-changes.txt
index 8566827..d38c51a 100644
--- a/Documentation/cmd-index-changes.txt
+++ b/Documentation/cmd-index-changes.txt
@@ -4,8 +4,9 @@
 gerrit index changes - Index one or more changes.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit index changes' <CHANGE> [<CHANGE> ...]
+_ssh_ -p <port> <host> _gerrit index changes_ <CHANGE> [<CHANGE> ...]
 --
 
 == DESCRIPTION
@@ -28,9 +29,9 @@
 == EXAMPLES
 Index changes with legacy ID numbers 1 and 2.
 
-====
+----
     $ ssh -p 29418 user@review.example.com gerrit index changes 1 2
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-index-start.txt b/Documentation/cmd-index-start.txt
index 0a481e5..fbe4f3f 100644
--- a/Documentation/cmd-index-start.txt
+++ b/Documentation/cmd-index-start.txt
@@ -4,8 +4,9 @@
 gerrit index start - Start the online indexer
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit index start <index>'
+_ssh_ -p <port> <host> _gerrit index start_ <INDEX> [--force]
 --
 
 == DESCRIPTION
@@ -19,15 +20,29 @@
 Gerrit. This command will not start the indexer if it is already running or if
 the active index is the latest.
 
-The <index> argument controls which secondary index is started. Currently, the
-only supported value is "changes".
-
 == ACCESS
 Caller must be a member of the privileged 'Administrators' group.
 
 == SCRIPTING
 This command is intended to be used in scripts.
 
+== OPTIONS
+<INDEX>::
+  Restart the online indexer on this secondary index.
+  Currently supported values:
+    * changes
+    * accounts
+
+--force::
+  Force an online re-index.
+
+== EXAMPLES
+Start the online indexer for the 'changes' index:
+
+----
+  $ ssh -p 29418 review.example.com gerrit index start changes
+----
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index e244228..61e7865 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -7,11 +7,13 @@
 
 To download a client command or hook, use scp or an http client:
 
+----
   $ scp -p -P 29418 john.doe@review.example.com:bin/gerrit-cherry-pick ~/bin/
   $ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg .git/hooks/
 
   $ curl -Lo ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
   $ curl -Lo .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
+----
 
 For more details on how to determine the correct SSH port number,
 see link:user-upload.html#test_ssh[Testing Your SSH Connection].
@@ -38,7 +40,9 @@
 not provide an interactive shell, the commands must be triggered
 from an ssh client, for example:
 
+----
   $ ssh -p 29418 review.example.com gerrit ls-projects
+----
 
 For more details on how to determine the correct SSH port number,
 see link:user-upload.html#test_ssh[Testing Your SSH Connection].
diff --git a/Documentation/cmd-kill.txt b/Documentation/cmd-kill.txt
index c64c537..ac8e802 100644
--- a/Documentation/cmd-kill.txt
+++ b/Documentation/cmd-kill.txt
@@ -4,8 +4,9 @@
 kill - Cancel or abort a background task
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'kill' <ID> ...
+_ssh_ -p <port> <host> _kill_ <ID> ...
 --
 
 == DESCRIPTION
diff --git a/Documentation/cmd-logging-ls-level.txt b/Documentation/cmd-logging-ls-level.txt
index c59dc3f..ee015bb 100644
--- a/Documentation/cmd-logging-ls-level.txt
+++ b/Documentation/cmd-logging-ls-level.txt
@@ -6,8 +6,9 @@
 gerrit logging ls - view the logging level
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit logging ls-level | ls'
+_ssh_ -p <port> <host> _gerrit logging ls-level_ | _ls_
   <NAME>
 --
 
@@ -25,15 +26,15 @@
 == Examples
 
 View the logging level of the loggers in the package com.google:
-=====
+----
     $ssh -p 29418 review.example.com gerrit logging ls-level \
      com.google.
-=====
+----
 
 View the logging level of every logger
-=====
+----
     $ssh -p 29418 review.example.com gerrit logging ls-level
-=====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-logging-set-level.txt b/Documentation/cmd-logging-set-level.txt
index 38062cb..5baa968 100644
--- a/Documentation/cmd-logging-set-level.txt
+++ b/Documentation/cmd-logging-set-level.txt
@@ -6,8 +6,9 @@
 gerrit logging set - set the logging level
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit logging set-level | set'
+_ssh_ -p <port> <host> _gerrit logging set-level_ | _set_
   <LEVEL>
   <NAME>
 --
@@ -32,16 +33,16 @@
 == Examples
 
 Change the logging level of the loggers in the package com.google to DEBUG.
-=====
+----
     $ssh -p 29418 review.example.com gerrit logging set-level \
      debug com.google.
-=====
+----
 
 Reset the logging level of every logger to what they were at deployment time.
-=====
+----
     $ssh -p 29418 review.example.com gerrit logging set-level \
      reset
-=====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-ls-groups.txt b/Documentation/cmd-ls-groups.txt
index 651cebe..d8eef8b 100644
--- a/Documentation/cmd-ls-groups.txt
+++ b/Documentation/cmd-ls-groups.txt
@@ -4,8 +4,9 @@
 gerrit ls-groups - List groups visible to caller
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit ls-groups'
+_ssh_ -p <port> <host> _gerrit ls-groups_
   [--project <NAME> | -p <NAME>]
   [--user <NAME> | -u <NAME>]
   [--owned]
@@ -86,55 +87,55 @@
 == EXAMPLES
 
 List visible groups:
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit ls-groups
 	Administrators
 	Anonymous Users
 	MyProject_Committers
 	Project Owners
 	Registered Users
-=====
+----
 
 List all groups for which any permission is set for the project
 "MyProject":
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit ls-groups --project MyProject
 	MyProject_Committers
 	Project Owners
 	Registered Users
-=====
+----
 
 List all groups which are owned by the calling user:
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit ls-groups --owned
 	MyProject_Committers
 	MyProject_Verifiers
-=====
+----
 
 Check if the calling user owns the group `MyProject_Committers`. If
 `MyProject_Committers` is returned the calling user owns this group.
 If the result is empty, the calling user doesn't own the group.
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit ls-groups --owned -q MyProject_Committers
 	MyProject_Committers
-=====
+----
 
 Extract the UUID of the 'Administrators' group:
 
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit ls-groups -v | awk '-F\t' '$1 == "Administrators" {print $2}'
 	ad463411db3eec4e1efb0d73f55183c1db2fd82a
-=====
+----
 
 Extract and expand the multi-line description of the 'Administrators'
 group:
 
-=====
+----
 	$ printf "$(ssh -p 29418 review.example.com gerrit ls-groups -v | awk '-F\t' '$1 == "Administrators" {print $3}')\n"
 	This is a
 	multi-line
 	description.
-=====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-ls-members.txt b/Documentation/cmd-ls-members.txt
index f8708d3..a6d492c 100644
--- a/Documentation/cmd-ls-members.txt
+++ b/Documentation/cmd-ls-members.txt
@@ -4,8 +4,9 @@
 gerrit ls-members - Show members of a given group
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit ls-members GROUPNAME'
+_ssh_ -p <port> <host> _gerrit ls-members_ GROUPNAME
   [--recursive]
 --
 
@@ -38,19 +39,19 @@
 == EXAMPLES
 
 List members of the Administrators group:
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit ls-members Administrators
 	id      username  full name    email
 	100000  jim     Jim Bob somebody@example.com
 	100001  johnny  John Smith      n/a
 	100002  mrnoname        n/a     someoneelse@example.com
-=====
+----
 
 List members of a non-existent group:
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit ls-members BadlySpelledGroup
 	Group not found or not visible
-=====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index 2a88915..e2e71ff 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -4,8 +4,9 @@
 gerrit ls-projects - List projects visible to caller
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit ls-projects'
+_ssh_ -p <port> <host> _gerrit ls-projects_
   [--show-branch <BRANCH> ...]
   [--description | -d]
   [--tree | -t]
@@ -113,7 +114,7 @@
 == EXAMPLES
 
 List visible projects:
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit ls-projects
 	platform/manifest
 	tools/gerrit
@@ -127,16 +128,16 @@
 	$ curl http://review.example.com/projects/tools/
 	tools/gerrit
 	tools/gwtorm
-=====
+----
 
 Clone any project visible to the user:
-====
+----
 	for p in `ssh -p 29418 review.example.com gerrit ls-projects`
 	do
 	  mkdir -p `dirname "$p"`
 	  git clone --bare "ssh://review.example.com:29418/$p.git" "$p.git"
 	done
-====
+----
 
 == SEE ALSO
 
diff --git a/Documentation/cmd-ls-user-refs.txt b/Documentation/cmd-ls-user-refs.txt
index 11781de..1a87fc9 100644
--- a/Documentation/cmd-ls-user-refs.txt
+++ b/Documentation/cmd-ls-user-refs.txt
@@ -4,8 +4,9 @@
 gerrit ls-user-refs - List refs visible to a specific user
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit ls-user-refs'
+_ssh_ -p <port> <host> _gerrit ls-user-refs_
   [--project PROJECT> | -p <PROJECT>]
   [--user <USER> | -u <USER>]
   [--only-refs-heads]
@@ -40,9 +41,9 @@
 == EXAMPLES
 
 List visible refs for the user "mr.developer" in project "gerrit"
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit ls-user-refs -p gerrit -u mr.developer
-=====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-plugin-enable.txt b/Documentation/cmd-plugin-enable.txt
index c8022ef..9b52736 100644
--- a/Documentation/cmd-plugin-enable.txt
+++ b/Documentation/cmd-plugin-enable.txt
@@ -4,8 +4,9 @@
 plugin enable - Enable plugins.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit plugin enable'
+_ssh_ -p <port> <host> _gerrit plugin enable_
   <NAME> ...
 --
 
@@ -30,9 +31,9 @@
 == EXAMPLES
 Enable a plugin:
 
-====
+----
 	ssh -p 29418 localhost gerrit plugin enable my-plugin
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-plugin-install.txt b/Documentation/cmd-plugin-install.txt
index 0ce6d7d..274b446 100644
--- a/Documentation/cmd-plugin-install.txt
+++ b/Documentation/cmd-plugin-install.txt
@@ -6,8 +6,9 @@
 plugin add - Install/Add a plugin.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit plugin install | add'
+_ssh_ -p <port> <host> _gerrit plugin install_ | _add_
   [--name <NAME> | -n <NAME>]
   - | <URL> | <PATH>
 --
@@ -44,31 +45,31 @@
 == EXAMPLES
 Install a plugin from an absolute file path on the server's host:
 
-====
+----
 	ssh -p 29418 localhost gerrit plugin install -n name.jar \
 	  $(pwd)/my-plugin.jar
-====
+----
 
 Install a WebUi plugin from an absolute file path on the server's host:
 
-====
+----
   ssh -p 29418 localhost gerrit plugin install -n name.js \
     $(pwd)/my-webui-plugin.js
-====
+----
 
 Install a plugin from an HTTP site:
 
-====
+----
 	ssh -p 29418 localhost gerrit plugin install -n name.jar \
 	  http://build-server/output/our-plugin
-====
+----
 
 Install a plugin from piped input:
 
-====
+----
 	ssh -p 29418 localhost gerrit plugin install -n name.jar \
 	  - <target/name-0.1.jar
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-plugin-ls.txt b/Documentation/cmd-plugin-ls.txt
index 234ce87..d329db5 100644
--- a/Documentation/cmd-plugin-ls.txt
+++ b/Documentation/cmd-plugin-ls.txt
@@ -4,8 +4,9 @@
 plugin ls - List the installed plugins.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit plugin ls'
+_ssh_ -p <port> <host> _gerrit plugin ls_
   [--all | -a]
   [--format {text | json | json_compact}]
 --
diff --git a/Documentation/cmd-plugin-reload.txt b/Documentation/cmd-plugin-reload.txt
index 88cb1f3..ad1e5e7 100644
--- a/Documentation/cmd-plugin-reload.txt
+++ b/Documentation/cmd-plugin-reload.txt
@@ -4,8 +4,9 @@
 plugin reload - Reload/Restart plugins.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit plugin reload'
+_ssh_ -p <port> <host> _gerrit plugin reload_
   <NAME> ...
 --
 
@@ -34,9 +35,9 @@
 == EXAMPLES
 Reload a plugin:
 
-====
+----
 	ssh -p 29418 localhost gerrit plugin reload my-plugin
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-plugin-remove.txt b/Documentation/cmd-plugin-remove.txt
index 770df85..805c7b4 100644
--- a/Documentation/cmd-plugin-remove.txt
+++ b/Documentation/cmd-plugin-remove.txt
@@ -6,8 +6,9 @@
 plugin rm - Disable plugins.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit plugin remove | rm'
+_ssh_ -p <port> <host> _gerrit plugin remove_ | _rm_
   <NAME> ...
 --
 
@@ -31,9 +32,9 @@
 == EXAMPLES
 Disable a plugin:
 
-====
+----
 	ssh -p 29418 localhost gerrit plugin remove my-plugin
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index 090781b..1faf1b0 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -4,8 +4,9 @@
 gerrit query - Query the change database
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit query'
+_ssh_ -p <port> <host> _gerrit query_
   [--format {TEXT | JSON}]
   [--current-patch-set]
   [--patch-sets | --all-approvals]
@@ -115,20 +116,20 @@
 == EXAMPLES
 
 Find the 2 most recent open changes in the tools/gerrit project:
-====
+----
   $ ssh -p 29418 review.example.com gerrit query --format=JSON status:open project:tools/gerrit limit:2
   {"project":"tools/gerrit", ...}
   {"project":"tools/gerrit", ...}
   {"type":"stats","rowCount":2,"runningTimeMilliseconds:15}
-====
+----
 
 Skip number of changes:
-====
+----
   $ ssh -p 29418 review.example.com gerrit query --format=JSON --start 42 status:open project:tools/gerrit limit:2
   {"project":"tools/gerrit", ...}
   {"project":"tools/gerrit", ...}
   {"type":"stats","rowCount":1,"runningTimeMilliseconds:15}
-====
+----
 
 
 == SCHEMA
diff --git a/Documentation/cmd-receive-pack.txt b/Documentation/cmd-receive-pack.txt
index f3b4f02..798f872 100644
--- a/Documentation/cmd-receive-pack.txt
+++ b/Documentation/cmd-receive-pack.txt
@@ -4,8 +4,9 @@
 git-receive-pack - Receive what is pushed into the repository
 
 == SYNOPSIS
+[verse]
 --
-'git receive-pack'
+_git receive-pack_
   [--reviewer <address> | --re <address>]
   [--cc <address>]
   <project>
@@ -41,25 +42,25 @@
 == EXAMPLES
 
 Send a review for a change on the master branch to charlie@example.com:
-=====
+----
 	git push ssh://review.example.com:29418/project HEAD:refs/for/master%r=charlie@example.com
-=====
+----
 
 Send reviews, but tagging them with the topic name 'bug42':
-=====
+----
 	git push ssh://review.example.com:29418/project HEAD:refs/for/master%r=charlie@example.com,topic=bug42
-=====
+----
 
 Also CC two other parties:
-=====
+----
 	git push ssh://review.example.com:29418/project HEAD:refs/for/master%r=charlie@example.com,cc=alice@example.com,cc=bob@example.com
-=====
+----
 
 Configure a push macro to perform the last action:
-====
+----
 	git config remote.charlie.url ssh://review.example.com:29418/project
 	git config remote.charlie.push HEAD:refs/for/master%r=charlie@example.com,cc=alice@example.com,cc=bob@example.com
-====
+----
 
 afterwards `.git/config` contains the following:
 ----
@@ -70,9 +71,9 @@
 
 and now sending a new change for review to charlie, CC'ing both
 alice and bob is much easier:
-====
+----
 	git push charlie
-====
+----
 
 == SEE ALSO
 
diff --git a/Documentation/cmd-rename-group.txt b/Documentation/cmd-rename-group.txt
index 9578458..a48014c 100644
--- a/Documentation/cmd-rename-group.txt
+++ b/Documentation/cmd-rename-group.txt
@@ -4,8 +4,9 @@
 gerrit rename-group - Rename an account group.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit rename-group'
+_ssh_ -p <port> <host> _gerrit rename-group_
   <GROUP>
   <NEWNAME>
 --
@@ -30,9 +31,9 @@
 == EXAMPLES
 Rename the group "MyGroup" to "MyCommitters".
 
-====
+----
 	$ ssh -p 29418 user@review.example.com gerrit rename-group MyGroup MyCommitters
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index c3d8651..668862b 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -1,12 +1,12 @@
-gerrit review
-==============
+= gerrit review
 
 == NAME
 gerrit review - Apply reviews to one or more patch sets
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit review'
+_ssh_ -p <port> <host> _gerrit review_
   [--project <PROJECT> | -p <PROJECT>]
   [--branch <BRANCH> | -b <BRANCH>]
   [--message <MESSAGE> | -m <MESSAGE>]
@@ -141,7 +141,7 @@
   can represent an external system like CI that does automated verification
   of the change. Comments with specific 'TAG' values can be filtered out in
   the web UI.
-  NOTE: To apply different tags on on different votes/comments multiple
+  Note that to apply different tags on on different votes/comments, multiple
   invocations of the SSH command are required.
 
 == ACCESS
@@ -153,37 +153,37 @@
 == EXAMPLES
 
 Approve the change with commit c0ff33 as "Verified +1"
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit review --verified +1 c0ff33
-=====
+----
 
 Vote on the project specific label "mylabel":
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit review --label mylabel=+1 c0ff33
-=====
+----
 
 Append the message "Build Successful". Notice two levels of quoting is
 required, one for the local shell, and another for the argument parser
 inside the Gerrit server:
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit review -m '"Build Successful"' c0ff33
-=====
+----
 
 Mark the unmerged commits both "Verified +1" and "Code-Review +2" and
 submit them for merging:
-====
+----
   $ ssh -p 29418 review.example.com gerrit review \
     --verified +1 \
     --code-review +2 \
     --submit \
     --project this/project \
     $(git rev-list origin/master..HEAD)
-====
+----
 
 Abandon an active change:
-====
+----
   $ ssh -p 29418 review.example.com gerrit review --abandon c0ff33
-====
+----
 
 == SEE ALSO
 
diff --git a/Documentation/cmd-set-account.txt b/Documentation/cmd-set-account.txt
index 8fb8e0d..884c8cc 100644
--- a/Documentation/cmd-set-account.txt
+++ b/Documentation/cmd-set-account.txt
@@ -4,14 +4,16 @@
 gerrit set-account - Change an account's settings.
 
 == SYNOPSIS
+[verse]
 --
-set-account [--full-name <FULLNAME>] [--active|--inactive] \
-            [--add-email <EMAIL>] [--delete-email <EMAIL> | ALL] \
-            [--preferred-email <EMAIL>] \
-            [--add-ssh-key - | <KEY>] \
-            [--delete-ssh-key - | <KEY> | ALL] \
-            [--http-password <PASSWORD>] \
-            [--clear-http-password] <USER>
+_ssh_ -p <port> <host> _gerrit set-account_
+  [--full-name <FULLNAME>] [--active|--inactive]
+  [--add-email <EMAIL>] [--delete-email <EMAIL> | ALL]
+  [--preferred-email <EMAIL>]
+  [--add-ssh-key - | <KEY>]
+  [--delete-ssh-key - | <KEY> | ALL]
+  [--http-password <PASSWORD>]
+  [--clear-http-password] <USER>
 --
 
 == DESCRIPTION
@@ -100,9 +102,9 @@
 == EXAMPLES
 Add an email and SSH key to `watcher`'s account:
 
-====
+----
     $ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit set-account --add-ssh-key - --add-email mail@example.com watcher
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-set-head.txt b/Documentation/cmd-set-head.txt
index d74caaa..f444173 100644
--- a/Documentation/cmd-set-head.txt
+++ b/Documentation/cmd-set-head.txt
@@ -4,8 +4,9 @@
 gerrit set-head - Change a project's HEAD.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit set-head' <NAME>
+_ssh_ -p <port> <host> _gerrit set-head_ <NAME>
   --new-head <REF>
 --
 
@@ -33,9 +34,9 @@
 == EXAMPLES
 Change HEAD of project `example` to `stable-2.11` branch:
 
-====
+----
     $ ssh -p 29418 review.example.com gerrit set-head example --new-head stable-2.11
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-set-members.txt b/Documentation/cmd-set-members.txt
index 174a25a..ae44843 100644
--- a/Documentation/cmd-set-members.txt
+++ b/Documentation/cmd-set-members.txt
@@ -4,8 +4,9 @@
 gerrit set-members - Set group members
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit set-members'
+_ssh_ -p <port> <host> _gerrit set-members_
   [--add USER ...]
   [--remove USER ...]
   [--include GROUP ...]
@@ -57,18 +58,18 @@
 
 Add alice and bob, but remove eve from the groups my-committers and
 my-verifiers.
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit set-members \
 	  -a alice@example.com -a bob@example.com \
 	  -r eve@example.com my-committers my-verifiers
-=====
+----
 
 Include the group my-friends into the group my-committers, but
 exclude the included group my-testers from the group my-committers.
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit set-members \
 	  -i my-friends -e my-testers my-committers
-=====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-set-project-parent.txt b/Documentation/cmd-set-project-parent.txt
index 70918b2..6e2328c 100644
--- a/Documentation/cmd-set-project-parent.txt
+++ b/Documentation/cmd-set-project-parent.txt
@@ -4,8 +4,9 @@
 gerrit set-project-parent - Change the project permissions are inherited from.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit set-project-parent'
+_ssh_ -p <port> <host> _gerrit set-project-parent_
   [--parent <NAME>]
   [--children-of <NAME>]
   [--exclude <NAME>]
@@ -45,16 +46,16 @@
 == EXAMPLES
 Configure `kernel/omap` to inherit permissions from `kernel/common`:
 
-====
+----
 	$ ssh -p 29418 review.example.com gerrit set-project-parent --parent kernel/common kernel/omap
-====
+----
 
 Reparent all children of `myParent` to `myOtherParent`:
 
-====
+----
 	$ ssh -p 29418 review.example.com gerrit set-project-parent \
 	  --children-of myParent --parent myOtherParent
-====
+----
 
 == SEE ALSO
 
diff --git a/Documentation/cmd-set-project.txt b/Documentation/cmd-set-project.txt
index 2b64d77..11cb74c 100644
--- a/Documentation/cmd-set-project.txt
+++ b/Documentation/cmd-set-project.txt
@@ -4,8 +4,9 @@
 gerrit set-project - Change a project's settings.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit set-project'
+_ssh_ -p <port> <host> _gerrit set-project_
   [--description <DESC> | -d <DESC>]
   [--submit-type <TYPE> | -t <TYPE>]
   [--contributor-agreements <true|false|inherit>]
@@ -102,10 +103,10 @@
 Change project `example` to be hidden, require change id, don't use content merge
 and use 'merge if necessary' as merge strategy:
 
-====
+----
     $ ssh -p 29418 review.example.com gerrit set-project example --submit-type MERGE_IF_NECESSARY\
     --change-id true --content-merge false --project-state HIDDEN
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-set-reviewers.txt b/Documentation/cmd-set-reviewers.txt
index d5b4908..3d53456 100644
--- a/Documentation/cmd-set-reviewers.txt
+++ b/Documentation/cmd-set-reviewers.txt
@@ -4,8 +4,9 @@
 gerrit set-reviewers - Add or remove reviewers to a change
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit set-reviewers'
+_ssh_ -p <port> <host> _gerrit set-reviewers_
   [--project <PROJECT> | -p <PROJECT>]
   [--add <REVIEWER> ... | -a <REVIEWER> ...]
   [--remove <REVIEWER> ... | -r <REVIEWER> ...]
@@ -54,27 +55,27 @@
 == EXAMPLES
 
 Add reviewers alice and bob, but remove eve from change Iac6b2ac2.
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit set-reviewers \
 	  -a alice@example.com -a bob@example.com \
 	  -r eve@example.com \
 	  Iac6b2ac2
-=====
+----
 
 Add reviewer elvis to old-style change id 1935 specifying that the change is in project "graceland"
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit set-reviewers \
 	  --project graceland \
 	  -a elvis@example.com \
 	  1935
-=====
+----
 
 Add all project owners as reviewers to change Iac6b2ac2.
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit set-reviewers \
 	  -a "'Project Owners'" \
 	  Iac6b2ac2
-=====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-show-caches.txt b/Documentation/cmd-show-caches.txt
index 5d6ab20..59abc1c 100644
--- a/Documentation/cmd-show-caches.txt
+++ b/Documentation/cmd-show-caches.txt
@@ -1,12 +1,14 @@
-gerrit show-caches
-===================
+= gerrit show-caches
 
 == NAME
 gerrit show-caches - Display current cache statistics
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit show-caches' [--gc] [--show-jvm]
+_ssh_ -p <port> <host> _gerrit show-caches_
+  [--gc]
+  [--show-jvm]
 --
 
 == DESCRIPTION
@@ -48,7 +50,7 @@
 
 == EXAMPLES
 
-====
+----
   $ ssh -p 29418 review.example.com gerrit show-caches
   Gerrit Code Review        2.9                       now   11:14:13   CEST
                                                    uptime    6 days 20 hrs
@@ -87,7 +89,7 @@
            107 open files
 
   Threads: 4 CPUs available, 371 threads
-====
+----
 
 == SEE ALSO
 
diff --git a/Documentation/cmd-show-connections.txt b/Documentation/cmd-show-connections.txt
index a694fb3..2f70e3c 100644
--- a/Documentation/cmd-show-connections.txt
+++ b/Documentation/cmd-show-connections.txt
@@ -4,8 +4,10 @@
 gerrit show-connections - Display active client SSH connections
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit show-connections' [--numeric | -n]
+_ssh_ -p <port> <host> _gerrit show-connections_
+  [--numeric | -n]
 --
 
 == DESCRIPTION
@@ -40,6 +42,7 @@
 
 Start::
 	Time (local to the server) that this connection started.
+	Only shown for MINA backend.
 
 Idle::
 	Time since the last data transfer on this connection.
@@ -47,6 +50,7 @@
 	connection keep-alive, but also an encrypted keep alive
 	higher up in the SSH protocol stack.  That higher keep
 	alive resets the idle timer, about once a minute.
+	Only shown for MINA backend.
 
 User::
 	The username of the account that is authenticated on this
@@ -60,22 +64,22 @@
 == EXAMPLES
 
 With reverse DNS lookup (default):
-====
+----
 	$ ssh -p 29418 review.example.com gerrit show-connections
 	Session     Start     Idle   User            Remote Host
 	--------------------------------------------------------------
 	3abf31e6 20:09:02 00:00:00  jdoe            jdoe-desktop.example.com
 	--
-====
+----
 
 Without reverse DNS lookup:
-====
+----
 	$ ssh -p 29418 review.example.com gerrit show-connections -n
 	Session     Start     Idle   User            Remote Host
 	--------------------------------------------------------------
 	3abf31e6 20:09:02 00:00:00  a/1001240       10.0.0.1
 	--
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-show-queue.txt b/Documentation/cmd-show-queue.txt
index e3f44ab..02f1c5b 100644
--- a/Documentation/cmd-show-queue.txt
+++ b/Documentation/cmd-show-queue.txt
@@ -4,9 +4,10 @@
 gerrit show-queue - Display the background work queues, including replication
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit show-queue'
-'ssh' -p <port> <host> 'ps'
+_ssh_ -p <port> <host> _gerrit show-queue_
+_ssh_ -p <port> <host> _ps_
 --
 
 == DESCRIPTION
@@ -38,6 +39,10 @@
 	Do not format the output to the terminal width (default of
 	80 columns).
 
+--by-queue::
+-q::
+	Group tasks by queue and print queue info.
+
 == DISPLAY
 
 Task::
@@ -69,7 +74,7 @@
 `tools/gerrit.git` project to two different remote systems, `dst1`
 and `dst2`:
 
-====
+----
 	$ ssh -p 29418 review.example.com gerrit show-queue
 	Task     State                 Command
 	------------------------------------------------------------------------------
@@ -77,7 +82,7 @@
 	9ad09d27 14:31:25.434          mirror dst2:/var/cache/tools/gerrit.git
 	------------------------------------------------------------------------------
 	  2 tasks
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index 028bd58..1cfb8b9 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -4,8 +4,9 @@
 gerrit stream-events - Monitor events occurring in real time
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit stream-events'
+_ssh_ -p <port> <host> _gerrit stream-events_
 --
 
 == DESCRIPTION
@@ -36,18 +37,18 @@
 
 == EXAMPLES
 
-====
+----
   $ ssh -p 29418 review.example.com gerrit stream-events
   {"type":"comment-added",change:{"project":"tools/gerrit", ...}, ...}
   {"type":"comment-added",change:{"project":"tools/gerrit", ...}, ...}
-====
+----
 
 Only subscribe to specific event types:
 
-====
+----
   $ ssh -p 29418 review.example.com gerrit stream-events \
       -s draft-published -s patchset-created -s ref-replicated
-====
+----
 
 == SCHEMA
 The JSON messages consist of nested objects referencing the *change*,
diff --git a/Documentation/cmd-suexec.txt b/Documentation/cmd-suexec.txt
index f6ee753..16338ba 100644
--- a/Documentation/cmd-suexec.txt
+++ b/Documentation/cmd-suexec.txt
@@ -4,11 +4,12 @@
 suexec - Execute a command as any registered user account
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port>
+_ssh_ -p <port>
   -i SITE_PATH/etc/ssh_host_rsa_key
-  '"Gerrit Code Review@localhost"'
-  'suexec'
+  "Gerrit Code Review@localhost"
+  _suexec_
   --as <EMAIL>
   [--from HOST:PORT]
   [--]
@@ -47,7 +48,7 @@
 == EXAMPLES
 
 Approve the change with commit c0ff33 as "Verified +1" as user bob@example.com
-=====
+----
   $ sudo -u gerrit ssh -p 29418 \
     -i site_path/etc/ssh_host_rsa_key \
     "Gerrit Code Review@localhost" \
@@ -55,7 +56,7 @@
     --as bob@example.com \
     -- \
     gerrit approve --verified +1 c0ff33
-=====
+----
 
 GERRIT
 ------
diff --git a/Documentation/cmd-test-submit-rule.txt b/Documentation/cmd-test-submit-rule.txt
index a9a1bc4..b8c4380 100644
--- a/Documentation/cmd-test-submit-rule.txt
+++ b/Documentation/cmd-test-submit-rule.txt
@@ -4,8 +4,9 @@
 gerrit test-submit rule - Test prolog submit rules with a chosen changeset.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit test-submit rule'
+_ssh_ -p <port> <host> _gerrit test-submit_ rule
   [-s]
   [--no-filters]
   CHANGE
@@ -27,7 +28,7 @@
 == EXAMPLES
 
 Test submit_rule from stdin and return the results as JSON.
-====
+----
  cat rules.pl | ssh -p 29418 review.example.com gerrit test-submit rule -s I78f2c6673db24e4e92ed32f604c960dc952437d9
  [
    {
@@ -37,10 +38,10 @@
      }
    }
  ]
-====
+----
 
 Test the active submit_rule from the refs/meta/config branch, ignoring filters in the project parents.
-====
+----
  $ ssh -p 29418 review.example.com gerrit test-submit rule I78f2c6673db24e4e92ed32f604c960dc952437d9 --no-filters
  [
    {
@@ -51,7 +52,7 @@
      }
    }
  ]
-====
+----
 
 == SCRIPTING
 Can be used either interactively for testing new prolog submit rules, or from a script to check the submit status of a change.
diff --git a/Documentation/cmd-test-submit-type.txt b/Documentation/cmd-test-submit-type.txt
index 658d43b..508684f 100644
--- a/Documentation/cmd-test-submit-type.txt
+++ b/Documentation/cmd-test-submit-type.txt
@@ -4,8 +4,9 @@
 gerrit test-submit type - Test prolog submit type with a chosen change.
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit test-submit type'
+_ssh_ -p <port> <host> _gerrit test-submit_ type
   [-s]
   [--no-filters]
   CHANGE
@@ -27,16 +28,16 @@
 == EXAMPLES
 
 Test submit_type from stdin and return the submit type.
-====
+----
  cat rules.pl | ssh -p 29418 review.example.com gerrit test-submit type -s I78f2c6673db24e4e92ed32f604c960dc952437d9
  "MERGE_IF_NECESSARY"
-====
+----
 
 Test the active submit_type from the refs/meta/config branch, ignoring filters in the project parents.
-====
+----
  $ ssh -p 29418 review.example.com gerrit test-submit type I78f2c6673db24e4e92ed32f604c960dc952437d9 --no-filters
  "MERGE_IF_NECESSARY"
-====
+----
 
 == SCRIPTING
 Can be used either interactively for testing new prolog submit type, or from a script to check the submit type of a change.
diff --git a/Documentation/cmd-version.txt b/Documentation/cmd-version.txt
index d5c2263..cc797cc 100644
--- a/Documentation/cmd-version.txt
+++ b/Documentation/cmd-version.txt
@@ -4,8 +4,9 @@
 gerrit version - Show the version of the currently executing Gerrit server
 
 == SYNOPSIS
+[verse]
 --
-'ssh' -p <port> <host> 'gerrit version'
+_ssh_ -p <port> <host> _gerrit version_
 --
 
 == DESCRIPTION
@@ -32,10 +33,10 @@
 
 == EXAMPLES
 
-=====
+----
 	$ ssh -p 29418 review.example.com gerrit version
 	gerrit version 2.4.2
-=====
+----
 
 GERRIT
 ------
diff --git a/Documentation/config-cla.txt b/Documentation/config-cla.txt
index 4c8d04a..2234808 100644
--- a/Documentation/config-cla.txt
+++ b/Documentation/config-cla.txt
@@ -12,39 +12,44 @@
 
 To retrieve the `project.config` file, initialize a temporary Git
 repository to edit the configuration:
-====
+----
   mkdir cfg_dir
   cd cfg_dir
   git init
-====
+----
 
 Download the existing configuration from Gerrit:
-====
+----
   git fetch ssh://localhost:29418/All-Projects refs/meta/config
   git checkout FETCH_HEAD
-====
+----
 
 Contributor agreements are defined as contributor-agreement sections in
 `project.config`:
-====
+----
   [contributor-agreement "Individual"]
     description = If you are going to be contributing code on your own, this is the one you want. You can sign this one online.
     agreementUrl = static/cla_individual.html
     autoVerify = group CLA Accepted - Individual
     accepted = group CLA Accepted - Individual
-====
+----
 
 Each `contributor-agreement` section within the `project.config` file must
 have a unique name. The section name will appear in the web UI.
 
-If not already present, add the UUID of the groups used in the
-`autoVerify` and `accepted` variables in the groups file.
+If not already present, add the group(s) used in the `autoVerify` and
+`accepted` variables in the `groups` file:
+----
+    # UUID                                  	Group Name
+    #
+    3dedb32915ecdbef5fced9f0a2587d164cd614d4	CLA Accepted - Individual
+----
 
 Commit the configuration change, and push it back:
-====
+----
   git commit -a -m "Add Individual contributor agreement"
   git push ssh://localhost:29418/All-Projects HEAD:refs/meta/config
-====
+----
 
 [[contributor-agreement.name.description]]contributor-agreement.<name>.description::
 +
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 810a690..ed49276 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -539,24 +539,31 @@
 
 [[cache.h2CacheSize]]cache.h2CacheSize::
 +
-The size of the in-memory cache for each opened H2 database, in bytes.
+The size of the in-memory cache for each opened H2 cache database, in bytes.
 +
+Some caches of Gerrit are persistent and are backed by an H2 database.
 H2 uses memory to cache its database content. The parameter `h2CacheSize`
 allows to limit the memory used by H2 and thus prevent out-of-memory
 caused by the H2 database using too much memory.
 +
-Technically the H2 cache size is configured using the CACHE_SIZE parameter in
-the H2 JDBC connection URL, as described
-link:http://www.h2database.com/html/features.html#cache_settings[here]
+See <<database.h2.cachesize,database.h2.cachesize>> for a detailed discussion.
 +
-Gerrit uses H2 for storing reviewed flags on changes and for persistent
-caches. The configured cache size is used for each of these local H2
-databases.
-+
-Default is unset, no cache size limit.
+Default is unset, using up to half of the available memory.
+
+H2 will persist this value in the database, so to unset explicitly specify 0.
 +
 Common unit suffixes of 'k', 'm', or 'g' are supported.
 
+[[cache.h2AutoServer]]cache.h2AutoServer::
++
+If set to true, enable H2 autoserver mode for the H2-backed persistent cache
+databases.
++
+See link:http://www.h2database.com/html/features.html#auto_mixed_mode[here]
+for detail.
++
+Default is false.
+
 [[cache.name.maxAge]]cache.<name>.maxAge::
 +
 Maximum age to keep an entry in the cache. Entries are removed from
@@ -885,6 +892,33 @@
 +
 Default is the number of CPUs.
 
+
+[[capability]]
+=== Section capability
+
+[[capability.administrateServer]]capability.administrateServer::
++
+Names of groups of users that are allowed to exercise the
+administrateServer capability, in addition to those listed in
+All-Projects. Configuring this option can be a useful fail-safe
+to recover a server in the event an administrator removed all
+groups from the administrateServer capability, or to ensure that
+specific groups always have administration capabilities.
++
+----
+[capability]
+  administrateServer = group Fail Safe Admins
+----
++
+The configuration file uses group names, not UUIDs.  If a group is
+renamed the gerrit.config file must be updated to reflect the new
+name. If a group cannot be found for the configured name a warning
+is logged and the server will continue normal startup.
++
+If not specified (default), only the groups listed by All-Projects
+may use the administrateServer capability.
+
+
 [[change]]
 === Section change
 
@@ -1542,6 +1576,42 @@
 classpath, e. g. in `$gerrit_site/lib` directory. Example implementation of
 SQL monitoring can be found in javamelody-plugin.
 
+[[database.h2]]database.h2::
++
+The settings in this section are used for the reviewdb if the
+<<database.type,database.type>> is H2.
++
+Additionally gerrit uses H2 for storing reviewed flags on changes.
+
+[[database.h2.cacheSize]]database.h2.cacheSize::
++
+The size of the H2 internal database cache, in bytes. The H2 internal cache for
+persistent H2-backed caches is controlled by
+<<cache.h2CacheSize,cache.h2CacheSize>>.
++
+H2 uses memory to cache its database content. The parameter `cacheSize`
+allows to limit the memory used by H2 and thus prevent out-of-memory
+caused by the H2 database using too much memory.
++
+Technically the H2 cache size is configured using the CACHE_SIZE parameter in
+the H2 JDBC connection URL, as described
+link:http://www.h2database.com/html/features.html#cache_settings[here]
++
+Default is unset, using up to half of the available memory.
+
+H2 will persist this value in the database, so to unset explicitly specify 0.
++
+Common unit suffixes of 'k', 'm', or 'g' are supported.
+
+[[database.h2.autoServer]]database.h2.autoServer::
++
+If `true` enable the automatic mixed mode
+(see link:http://www.h2database.com/html/features.html#auto_mixed_mode[Automatic Mixed Mode]).
+This enables concurrent access to the embedded H2 database from command line
+utils (e.g. RebuildNoteDb).
++
+Default is `false`.
+
 [[download]]
 === Section download
 
@@ -1774,8 +1844,8 @@
 +
 The default URL for Gerrit to be accessed through.
 +
-Typically this would be set to "http://review.example.com/" or
-"http://example.com/gerrit/" so Gerrit can output links that point
+Typically this would be set to something like "http://review.example.com/"
+or "http://example.com:8080/gerrit/" so Gerrit can output links that point
 back to itself.
 +
 Setting this is highly recommended, as its necessary for the upload
@@ -1870,6 +1940,13 @@
 +
 If not specified, the default no-op implementation is used.
 
+[[gerrit.canLoadInIFrame]]gerrit.canLoadInIFrame::
++
+For security reasons Gerrit will always jump out of iframe.
+Setting this option to true will prevent this behavior.
++
+By default false.
+
 [[gitweb]]
 === Section gitweb
 
@@ -2002,85 +2079,6 @@
 +
 By default, false.
 
-[[hooks]]
-=== Section hooks
-
-See also link:config-hooks.html[Hooks].
-
-[[hooks.path]]hooks.path::
-+
-Optional path to hooks, if not specified then `'$site_path'/hooks` will be used.
-
-[[hooks.syncHookTimeout]]hooks.syncHookTimeout::
-+
-Optional timeout value in seconds for synchronous hooks, if not specified
-then 30 seconds will be used.
-
-[[hooks.changeAbandonedHook]]hooks.changeAbandonedHook::
-+
-Optional filename for the change abandoned hook, if not specified then
-`change-abandoned` will be used.
-
-[[hooks.changeMergedHook]]hooks.changeMergedHook::
-+
-Optional filename for the change merged hook, if not specified then
-`change-merged` will be used.
-
-[[hooks.changeRestoredHook]]hooks.changeRestoredHook::
-+
-Optional filename for the change restored hook, if not specified then
-`change-restored` will be used.
-
-[[hooks.claSignedHook]]hooks.claSignedHook::
-+
-Optional filename for the CLA signed hook, if not specified then
-`cla-signed` will be used.
-
-[[hooks.commentAddedHook]]hooks.commentAddedHook::
-+
-Optional filename for the comment added hook, if not specified then
-`comment-added` will be used.
-
-[[hooks.draftPublishedHook]]hooks.draftPublishedHook::
-+
-Optional filename for the draft published hook, if not specified then
-`draft-published` will be used.
-
-[[hooks.hashtagsChangedHook]]hooks.hashtagsChangedHook::
-+
-Optional filename for the hashtags changed hook, if not specified then
-`hashtags-changed` will be used.
-
-[[hooks.projectCreatedHook]]hooks.projectCreatedHook::
-+
-Optional filename for the project created hook, if not specified then
-`project-created` will be used.
-
-[[hooks.patchsetCreatedHook]]hooks.patchsetCreatedHook::
-+
-Optional filename for the patchset created hook, if not specified then
-`patchset-created` will be used.
-
-[[hooks.refUpdateHook]]hooks.refUpdateHook::
-+
-Optional filename for the ref update hook, if not specified then
-`ref-update` will be used.
-
-[[hooks.refUpdatedHook]]hooks.refUpdatedHook::
-+
-Optional filename for the ref updated hook, if not specified then
-`ref-updated` will be used.
-
-[[hooks.reviewerAddedHook]]hooks.reviewerAddedHook::
-+
-Optional filename for the reviewer added hook, if not specified then
-`reviewer-added` will be used.
-
-[[hooks.topicChangedHook]]hooks.topicChangedHook::
-+
-Optional filename for the topic changed hook, if not specified then
-`topic-changed` will be used.
-
 [[http]]
 === Section http
 
@@ -2679,7 +2677,10 @@
 example `${userPrincipalName.localPart}` would provide only 'user'.
 +
 If set, users will be unable to modify their SSH username field, as
-Gerrit will populate it only from the LDAP data.
+Gerrit will populate it only from the LDAP data. Note that once the
+username has been set it cannot be changed, therefore it is
+recommended not to make changes to this setting that would cause the
+value to differ, as this will prevent users from logging in.
 +
 Default is `uid` for RFC 2307 servers,
 and `${sAMAccountName.toLowerCase}` for Active Directory.
@@ -2862,9 +2863,11 @@
 
 [[lfs.plugin]]lfs.plugin::
 +
-The name of a plugin which serves the LFS protocol on the
-`<project-name>/info/lfs/objects/batch` endpoint. When not configured Gerrit
-will respond with `501 Not Implemented` on LFS protocol requests.
+The name of a plugin which serves the
+link:https://github.com/github/git-lfs/blob/master/docs/api/v1/http-v1-batch.md[
+LFS protocol] on the `<project-name>/info/lfs/objects/batch` endpoint. When
+not configured Gerrit will respond with `501 Not Implemented` on LFS protocol
+requests.
 +
 By default unset.
 
@@ -3077,7 +3080,7 @@
 +
 When a client pushes with `git push --signed`, this ensures that the
 push certificate is valid and signed with a valid public key stored in
-the `refs/gpg-keys` branch of `All-Users`.
+the `refs/meta/gpg-keys` branch of `All-Users`.
 +
 Defaults to false.
 
@@ -3187,7 +3190,8 @@
   defaultSubmitType = CHERRY_PICK
 ----
 
-[NOTE] All properties are used from the matching repository configuration. In
+[NOTE]
+All properties are used from the matching repository configuration. In
 the previous example, all properties will be used from `project/plugins/\*`
 section and no properties will be inherited nor overridden from `project/*`.
 
@@ -3325,7 +3329,9 @@
 Full Name and Preferred Email.  This may cause messages to be
 classified as spam if the user's domain has SPF or DKIM enabled
 and <<sendemail.smtpServer,sendemail.smtpServer>> is not a trusted
-relay for that domain.
+relay for that domain. You can specify
+<<sendemail.allowedDomain,sendemail.allowedDomain>> to instruct Gerrit to only
+send as USER if USER is from those domains.
 +
 * `MIXED`
 +
@@ -3351,6 +3357,16 @@
 +
 By default, MIXED.
 
+[[sendemail.allowedDomain]]sendemail.allowedDomain::
++
+Only used when `sendemail.from` is set to `USER`.
+List of allowed domains. If user's email matches one of the domains, emails will
+be sent as USER, otherwise as MIXED mode. Wildcards may be specified by
+including `*` to match any number of characters, for example `*.example.com`
+matches any subdomain of `example.com`.
++
+By default, `*`.
+
 [[sendemail.smtpServer]]sendemail.smtpServer::
 +
 Hostname (or IP address) of a SMTP server that will relay
@@ -3487,12 +3503,12 @@
 Specifies the local addresses the internal SSHD should listen
 for connections on.  The following forms may be used to specify
 an address.  In any form, `:'port'` may be omitted to use the
-default of 29418.
+default of `29418`.
 +
-* 'hostname':'port' (for example `review.example.com:29418`)
-* 'IPv4':'port' (for example `10.0.0.1:29418`)
-* ['IPv6']:'port' (for example `[ff02::1]:29418`)
-* +*:'port'+ (for example `+*:29418+`)
+* `'hostname':'port'` (for example `review.example.com:29418`)
+* `'IPv4':'port'` (for example `10.0.0.1:29418`)
+* `['IPv6']:'port'` (for example `[ff02::1]:29418`)
+* `+*:'port'+` (for example `+*:29418+`)
 
 +
 --
@@ -3501,7 +3517,7 @@
 
 To disable the internal SSHD, set listenAddress to `off`.
 
-By default, *:29418.
+By default, `*:29418`.
 --
 
 [[sshd.advertisedAddress]]sshd.advertisedAddress::
@@ -3512,16 +3528,16 @@
 22. The following forms may be used to specify an address.  In any
 form, `:'port'` may be omitted to use the default SSH port of 22.
 
-* 'hostname':'port' (for example `review.example.com:22`)
-* 'IPv4':'port' (for example `10.0.0.1:29418`)
-* ['IPv6']:'port' (for example `[ff02::1]:29418`)
+* `'hostname':'port'` (for example `review.example.com:22`)
+* `'IPv4':'port'` (for example `10.0.0.1:29418`)
+* `['IPv6']:'port'` (for example `[ff02::1]:29418`)
 
 +
 --
 If multiple values are supplied, the daemon will advertise all
 of them.
 
-By default, sshd.listenAddress.
+By default uses the value of `sshd.listenAddress`.
 --
 
 [[sshd.tcpKeepAlive]]sshd.tcpKeepAlive::
@@ -3531,7 +3547,7 @@
 +
 Only effective when `sshd.backend` is set to `MINA`.
 +
-By default, true.
+By default, `true`.
 
 [[sshd.threads]]sshd.threads::
 +
@@ -3632,8 +3648,8 @@
 to the default ciphers, cipher names starting with `-` are removed
 from the default cipher set.
 +
-Supported ciphers: aes128-cbc, aes128-cbc, aes256-cbc, blowfish-cbc,
-3des-cbc, none.
+Supported ciphers: `aes128-cbc`, `aes128-cbc`, `aes256-cbc`, `blowfish-cbc`,
+`3des-cbc`, `none`.
 +
 By default, all supported ciphers except `none` are available.
 
@@ -3645,8 +3661,8 @@
 are enabled in addition to the default MACs, MAC names starting with
 `-` are removed from the default MACs.
 +
-Supported MACs: hmac-md5, hmac-md5-96, hmac-sha1, hmac-sha1-96,
-hmac-sha2-256, hmac-sha2-512.
+Supported MACs: `hmac-md5`, `hmac-md5-96`, `hmac-sha1`, `hmac-sha1-96`,
+`hmac-sha2-256`, `hmac-sha2-512`.
 +
 By default, all supported MACs are available.
 
@@ -3720,7 +3736,7 @@
 `log4j.appender` with the name `sshd_log` can be configured to overwrite
 programmatic configuration.
 +
-By default, true.
+By default, `true`.
 
 [[sshd.rekeyBytesLimit]]sshd.rekeyBytesLimit::
 +
@@ -3729,7 +3745,7 @@
 +
 By default, 1073741824 (bytes, 1GB).
 +
-The rekeyBytesLimit cannot be set to lower than 32.
+The `rekeyBytesLimit` cannot be set to lower than 32.
 
 [[sshd.rekeyTimeLimit]]sshd.rekeyTimeLimit::
 +
@@ -3749,12 +3765,6 @@
 +
 By default 10.
 
-[[suggest.fullTextSearch]]suggest.fullTextSearch::
-+
-If `true` the reviewer completion suggestions will be based on a full text search.
-+
-By default `false`.
-
 [[suggest.from]]suggest.from::
 +
 The number of characters that a user must have typed before suggestions
@@ -3762,18 +3772,6 @@
 +
 By default 0.
 
-[[suggest.fullTextSearchMaxMatches]]suggest.fullTextSearchMaxMatches::
-+
-The maximum number of matches evaluated for change access when using full text search.
-+
-By default 100.
-
-[[suggest.fullTextSearchRefresh]]suggest.fullTextSearchRefresh::
-+
-Refresh interval for the in-memory account search index.
-+
-By default 1 hour.
-
 
 [[theme]]
 === Section theme
@@ -4017,10 +4015,16 @@
 [[submodule.verbosesuperprojectupdate]]submodule.verboseSuperprojectUpdate::
 +
 When using link:user-submodules.html#automatic_update[automatic superproject updates]
-this option will determine if the submodule commit messages are included into
+this option will determine how the submodule commit messages are included into
 the commit message of the superproject update.
 +
-By default this is true.
+If `FALSE`, will not include any commit messages for the gitlink update.
++
+If `SUBJECT_ONLY`, will include only the commit subjects.
++
+If `TRUE`, will include full commit messages.
++
+By default this is `TRUE`.
 
 [[submodule.enableSuperProjectSubscriptions]]submodule.enableSuperProjectSubscriptions::
 +
diff --git a/Documentation/config-gitweb.txt b/Documentation/config-gitweb.txt
index 63eaffd..fcfd0e1 100644
--- a/Documentation/config-gitweb.txt
+++ b/Documentation/config-gitweb.txt
@@ -16,10 +16,10 @@
 which is a common installation path for the 'gitweb' package on
 Linux distributions.
 
-====
+----
   git config --file $site_path/etc/gerrit.config gitweb.cgi /usr/lib/cgi-bin/gitweb.cgi
   git config --file $site_path/etc/gerrit.config --unset gitweb.url
-====
+----
 
 Alternatively, if Gerrit is served behind reverse proxy, it can
 generate different URLs for gitweb's links (they need to be
@@ -27,10 +27,10 @@
 for serving gitweb under a different URL than the Gerrit instance.
 To enable this feature, set both: `gitweb.cgi` and `gitweb.url`.
 
-====
+----
   git config --file $site_path/etc/gerrit.config gitweb.cgi /usr/lib/cgi-bin/gitweb.cgi
   git config --file $site_path/etc/gerrit.config gitweb.url /pretty/path/to/gitweb
-====
+----
 
 After updating `'$site_path'/etc/gerrit.config`, the Gerrit server must
 be restarted and clients must reload the host page to see the change.
@@ -76,15 +76,15 @@
 
 On Ubuntu:
 
-====
-  sudo apt-get install gitweb
-====
+----
+  $ sudo apt-get install gitweb
+----
 
 With Yum:
 
-====
+----
   $ yum install gitweb
-====
+----
 
 ===== Configure Gitweb
 
@@ -124,16 +124,16 @@
 
 Link gitweb to `/var/www/gitweb`, check `/etc/gitweb.conf` if unsure of paths:
 
-====
+----
   $ sudo ln -s /usr/share/gitweb /var/www/gitweb
-====
+----
 
 Add the gitweb directory to the Apache configuration by creating a "gitweb"
 file inside the Apache conf.d directory:
 
-====
+----
   $ touch /etc/apache/conf.d/gitweb
-====
+----
 
 Add the following to /etc/apache/conf.d/gitweb:
 
@@ -145,14 +145,15 @@
 AllowOverride None
 ----
 
-*NOTE* This may have already been added by yum/apt-get. If that's the case, leave as
+[NOTE]
+This may have already been added by yum/apt-get. If that's the case, leave as
 is.
 
 ===== Restart the Apache Web Server
 
-====
-$ sudo /etc/init.d/apache2 restart
-====
+----
+  $ sudo /etc/init.d/apache2 restart
+----
 
 Now you should be able to view your repository projects online:
 
@@ -182,9 +183,9 @@
 verify by checking for perl modules. From an msys console, execute the
 following to check:
 
-====
+----
 $ perl -mCGI -mEncode -mFcntl -mFile::Find -mFile::Basename -e ""
-====
+----
 
 You may encounter the following exception:
 
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index cde7b39..a71595f 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -1,192 +1,9 @@
 = Gerrit Code Review - Hooks
 
-Gerrit does not run any of the standard git hooks in the
-repositories it works with, but it does have its own hook mechanism
-included. Gerrit looks in `'$site_path'/hooks` for executables with
-names listed below.
-
-The environment will have GIT_DIR set to the full path of the
-affected git repository so that git commands can be easily run.
-
-Make sure your hook scripts are executable if running on *nix.
-
-With the exception of the ref-update hook, hooks are run in the background
-after the relevant change has taken place so are unable to affect
-the outcome of any given change. Because of the fact the hooks are
-run in the background after the activity, a hook might not be notified
-about an event if the server is shutdown before the hook can be invoked.
-
-== Supported Hooks
-
-=== ref-update
-
-This is called when a push request is received by Gerrit. It allows
-a push to be rejected before it is committed to the Gerrit repository.
-If the script exits with non-zero return code the push will be rejected.
-Any output from the script will be returned to the user, regardless of the
-return code.
-
-This hook is called synchronously so it is recommended that
-it not block.  A default timeout on the hook is set to 30 seconds to avoid
-"runaway" hooks using up server threads.  See link:config-gerrit.html#hooks.syncHookTimeout[hooks.syncHookTimeout]
-for configuration details.
-
-====
-  ref-update --project <project name> --refname <refname> --uploader <uploader> --oldrev <sha1> --newrev <sha1>
-====
-
-=== patchset-created
-
-This is called whenever a patchset is created (this includes new
-changes and drafts).
-
-====
-  patchset-created --change <change id> --is-draft <boolean> --kind <change kind> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --uploader <uploader> --commit <sha1> --patchset <patchset id>
-====
-
-kind:: change kind represents the kind of change uploaded, also represented in link:json.html#patchSet[patchSet]
-
-  REWORK;; Nontrivial content changes.
-
-  TRIVIAL_REBASE;; Conflict-free merge between the new parent and the prior patch set.
-
-  MERGE_FIRST_PARENT_UPDATE;; Conflict-free change of first (left) parent of a merge commit.
-
-  NO_CODE_CHANGE;; No code changed; same tree and same parent tree.
-
-  NO_CHANGE;; No changes; same commit message, same tree and same parent tree.
-
-=== draft-published
-
-This is called whenever a draft change is published.
-
-====
-  draft-published --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --uploader <uploader> --commit <sha1> --patchset <patchset id>
-====
-
-=== comment-added
-
-This is called whenever a comment is added to a change.
-
-====
-  comment-added --change <change id> --is-draft <boolean> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --author <comment author> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> --<approval category id>-oldValue <score> ...]
-====
-
-=== change-merged
-
-Called whenever a change has been merged.
-
-====
-  change-merged --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1> --newrev <sha1>
-====
-
-=== change-abandoned
-
-Called whenever a change has been abandoned.
-
-====
-  change-abandoned --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --abandoner <abandoner> --commit <sha1> --reason <reason>
-====
-
-=== change-restored
-
-Called whenever a change has been restored.
-
-====
-  change-restored --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --restorer <restorer> --commit <sha1> --reason <reason>
-====
-
-=== ref-updated
-
-Called whenever a ref has been updated.
-
-====
-  ref-updated --oldrev <old rev> --newrev <new rev> --refname <ref name> --project <project name> --submitter <submitter>
-====
-
-=== project-created
-
-Called whenever a project has been created.
-
-====
-  project-created --project <project name> --head <head name>
-====
-
-=== reviewer-added
-
-Called whenever a reviewer is added to a change.
-
-====
-  reviewer-added --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --reviewer <reviewer>
-====
-
-=== reviewer-deleted
-
-Called whenever a reviewer (with a vote) is removed from a change.
-
-====
-  reviewer-deleted --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --reviewer <reviewer> [--<approval category id> <score> --<approval category id> <score> ...]
-====
-
-=== topic-changed
-
-Called whenever a change's topic is changed from the Web UI or via the REST API.
-
-====
-  topic-changed --change <change id> --change-owner <change owner> --project <project name> --branch <branch> --changer <changer> --old-topic <old topic> --new-topic <new topic>
-====
-
-=== hashtags-changed
-
-Called whenever hashtags are added to or removed from a change from the Web UI
-or via the REST API.
-
-====
-  hashtags-changed --change <change id>  --change-owner <change owner> --project <project name> --branch <branch> --editor <editor> --added <hashtag> --removed <hashtag> --hashtag <hashtag>
-====
-
-The `--added` parameter may be passed multiple times, once for each
-hashtag that was added to the change.
-
-The `--removed` parameter may be passed multiple times, once for each
-hashtag that was removed from the change.
-
-The `--hashtag` parameter may be passed multiple times, once for each
-hashtag remaining on the change after the add or remove operation has
-been performed.
-
-=== cla-signed
-
-Called whenever a user signs a contributor license agreement.
-
-====
-  cla-signed --submitter <submitter> --user-id <user_id> --cla-id <cla_id>
-====
-
-
-== Configuration Settings
-
-It is possible to change where Gerrit looks for hooks, and what
-filenames it looks for, by adding a [hooks] section in gerrit.config.
-
-Gerrit will use the value of hooks.path for the hooks directory.
-
-For the hook filenames, Gerrit will use the values of hooks.patchsetCreatedHook,
-hooks.draftPublishedHook, hooks.commentAddedHook, hooks.changeMergedHook,
-hooks.changeAbandonedHook, hooks.changeRestoredHook, hooks.refUpdatedHook,
-hooks.refUpdateHook, hooks.reviewerAddedHook and hooks.claSignedHook.
-
-== Missing Change URLs
-
-If link:config-gerrit.html#gerrit.canonicalWebUrl[gerrit.canonicalWebUrl]
-is not set in `gerrit.config` the `--change-url` flag may not be
-passed to all hooks.  Hooks started out of an SSH context (for example
-the patchset-created hook) don't know the server's web URL, unless
-this variable is configured.
-
-== SEE ALSO
-
-* link:config-gerrit.html#hooks[Section hooks]
+Gerrit does not run any of the standard git hooks in the repositories
+it works with, but it does have its own hook mechanism included via
+the link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/hooks[
+hooks plugin].
 
 GERRIT
 ------
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index a192360..1f9dd33 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -90,13 +90,13 @@
 Administrators can install the Verified label by adding the following
 text to `project.config`:
 
-====
+----
   [label "Verified"]
       function = MaxWithBlock
       value = -1 Fails
       value =  0 No score
       value = +1 Verified
-====
+----
 
 The range of values is:
 
@@ -323,16 +323,17 @@
 E.g. create a label `Video-Qualify` on parent project and configure
 the `branch` as:
 
-====
+----
   [label "Video-Qualify"]
       branch = refs/heads/video-1.0/*
       branch = refs/heads/video-1.1/Kino
-====
+----
 
 Then *only* changes in above branch scope of parent project and child
 projects will be affected by `Video-Qualify`.
 
-NOTE: The `branch` is independent from the branch scope defined in `access`
+[NOTE]
+The `branch` is independent from the branch scope defined in `access`
 parts in `project.config` file. That means from the UI a user can always
 assign permissions for that label on a branch, but this permission is then
 ignored if the label doesn't apply for that branch.
@@ -343,13 +344,13 @@
 To define a new 3-valued category that behaves exactly like `Verified`,
 but has different names/labels:
 
-====
+----
   [label "Copyright-Check"]
       function = MaxWithBlock
       value = -1 Do not have copyright
       value =  0 No score
       value = +1 Copyright clear
-====
+----
 
 The new column will appear at the end of the table, and `-1 Do not have
 copyright` will block submit, while `+1 Copyright clear` is required to
@@ -360,7 +361,7 @@
 This example attempts to describe how a label default value works with the
 user permissions.  Assume the configuration below.
 
-====
+----
   [access "refs/heads/*"]
       label-Snarky-Review = -3..+3 group Administrators
       label-Snarky-Review = -2..+2 group Project Owners
@@ -374,7 +375,7 @@
       value = +2 Hmm, this is pretty nice
       value = +3 Ohh, hell yes!
       defaultValue = -3
-====
+----
 
 Upon clicking the Reply button:
 
@@ -386,7 +387,7 @@
 
 This example shows how a label can be configured to have a standard patch set lock.
 
-====
+----
   [access "refs/heads/*"]
       label-Patch-Set-Lock = +0..+1 group Administrators
   [label "Patch-Set-Lock"]
@@ -394,7 +395,7 @@
       value =  0 Patch Set Unlocked
       value = +1 Patch Set Locked
       defaultValue = 0
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/config-login-register.txt b/Documentation/config-login-register.txt
index 76f47ed..ffeae62 100644
--- a/Documentation/config-login-register.txt
+++ b/Documentation/config-login-register.txt
@@ -89,7 +89,8 @@
 
 * Real name (visible name in Gerrit)
 * Register your email (it must be confirmed later)
-* Select a username with which to communicate with Gerrit over ssh+git
+* Select a username with which to communicate with Gerrit over ssh+git. Note
+that once saved, the username cannot be changed.
 
 * The server will ask you for an RSA public key.
 That's the key we generated above, and it's time to make sure that Gerrit knows
@@ -105,7 +106,8 @@
   user@host:~$
 ----
 
-IMPORTANT: Please take note of the extra line-breaks introduced in the key above
+[IMPORTANT]
+Please take note of the extra line-breaks introduced in the key above
 for formatting purposes. Please be sure to copy and paste your key without
 line-breaks.
 
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index 5dec21d..34072d7 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -76,6 +76,18 @@
 link:https://gerrit.googlesource.com/plugins/download-commands/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[hooks]]
+=== hooks
+
+This plugin runs server-side hooks on events.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/hooks[
+Project] |
+link:https://gerrit.googlesource.com/plugins/hooks/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/hooks/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
 [[replication]]
 === replication
 
@@ -387,6 +399,7 @@
 Configuration]
 
 [[metrics-reporter-elasticsearch]]
+=== metrics-reporter-elasticsearch
 
 This plugin reports Gerrit metrics to Elasticsearch.
 
@@ -394,6 +407,7 @@
 Project].
 
 [[metrics-reporter-graphite]]
+=== metrics-reporter-graphite
 
 This plugin reports Gerrit metrics to Graphite.
 
@@ -401,6 +415,7 @@
 Project].
 
 [[metrics-reporter-jmx]]
+=== metrics-reporter-jmx
 
 This plugin reports Gerrit metrics to JMX.
 
@@ -568,7 +583,7 @@
 where Gerrit's config files are stored is difficult or impossible to
 get.
 
-link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/server-config[
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/server-config[
 Project]
 
 [[serviceuser]]
@@ -581,7 +596,7 @@
 Plugin in Jenkins. A service user is not able to login into the Gerrit
 WebUI and it cannot push commits or tags.
 
-link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/serviceuser[
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/serviceuser[
 Project] |
 link:https://gerrit.googlesource.com/plugins/serviceuser/+doc/master/src/main/resources/Documentation/about.md[
 Documentation] |
@@ -597,7 +612,7 @@
 and a maximum allowed path length. Pushes of commits that violate these
 settings are rejected by Gerrit.
 
-link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/uploadvalidator[
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/uploadvalidator[
 Project] |
 link:https://gerrit.googlesource.com/plugins/uploadvalidator/+doc/master/src/main/resources/Documentation/about.md[
 Documentation] |
@@ -612,7 +627,7 @@
 amongst multiple Gerrit servers, making it useful for multi-master
 Gerrit installations.
 
-link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/websession-flatfile[
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/websession-flatfile[
 Project] |
 link:https://gerrit.googlesource.com/plugins/websession-flatfile/+doc/master/src/main/resources/Documentation/about.md[
 Documentation] |
@@ -630,7 +645,7 @@
 Requests. Pushing a new patchset will reset the change to Review In
 Progress.
 
-link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/wip[
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/wip[
 Project] |
 link:https://gerrit.googlesource.com/plugins/wip/+doc/master/src/main/resources/Documentation/about.md[
 Documentation] |
@@ -642,7 +657,7 @@
 
 This plugin serves project documentation as HTML pages.
 
-link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/x-docs[
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/x-docs[
 Project] |
 link:https://gerrit.googlesource.com/plugins/x-docs/+doc/master/src/main/resources/Documentation/about.md[
 Documentation] |
diff --git a/Documentation/config-sso.txt b/Documentation/config-sso.txt
index 561309b..5f35d73 100644
--- a/Documentation/config-sso.txt
+++ b/Documentation/config-sso.txt
@@ -10,9 +10,9 @@
 user authentication services.  To enable OpenID, the auth.type
 setting should be `OpenID`:
 
-====
+----
   git config --file $site_path/etc/gerrit.config auth.type OpenID
-====
+----
 
 As this is the default setting there is nothing required from the
 site administrator to make use of the OpenID authentication services.
@@ -24,9 +24,9 @@
 Add the following to `$JETTY_HOME/etc/jetty.xml` under
 `org.mortbay.jetty.nio.SelectChannelConnector`:
 
-====
+----
   <Set name="headerBufferSize">16384</Set>
-====
+----
 
 In order to use permissions beyond those granted to the
 `Anonymous Users` and `Registered Users` groups, an account
@@ -44,9 +44,9 @@
 * `https://` -- trust all OpenID providers using the HTTPS protocol
 
 To trust only Yahoo!:
-====
+----
   git config --file $site_path/etc/gerrit.config auth.trustedOpenID https://me.yahoo.com
-====
+----
 
 === Database Schema
 
@@ -100,11 +100,11 @@
 
 To enable this form of authentication:
 
-====
+----
   git config --file $site_path/etc/gerrit.config auth.type HTTP
   git config --file $site_path/etc/gerrit.config --unset auth.httpHeader
   git config --file $site_path/etc/gerrit.config auth.emailFormat '{0}@example.com'
-====
+----
 
 The auth.type must always be HTTP, indicating the user identity
 will be obtained from the HTTP authorization data.
@@ -124,14 +124,14 @@
 such as the following is recommended to ensure Apache performs the
 authentication at the proper time:
 
-====
+----
   <Location "/login/">
     AuthType Basic
     AuthName "Gerrit Code Review"
     Require valid-user
     ...
   </Location>
-====
+----
 
 === Database Schema
 
@@ -161,11 +161,11 @@
 
 To enable this form of authentication:
 
-====
+----
   git config --file $site_path/etc/gerrit.config auth.type HTTP
   git config --file $site_path/etc/gerrit.config auth.httpHeader SM_USER
   git config --file $site_path/etc/gerrit.config auth.emailFormat '{0}@example.com'
-====
+----
 
 The auth.type must always be HTTP, indicating the user identity
 will be obtained from the HTTP authorization data.
@@ -186,9 +186,9 @@
 Add the following to `$JETTY_HOME/etc/jetty.xml` under
 `org.mortbay.jetty.nio.SelectChannelConnector`:
 
-====
+----
   <Set name="headerBufferSize">16384</Set>
-====
+----
 
 
 === Database Schema
diff --git a/Documentation/config-themes.txt b/Documentation/config-themes.txt
index b165e37..dcfd711 100644
--- a/Documentation/config-themes.txt
+++ b/Documentation/config-themes.txt
@@ -88,7 +88,7 @@
 To connect Gerrit to Google Analytics add the following to your
 `GerritSiteFooter.html`:
 
-====
+----
   <div>
   <!-- standard analytics code -->
     <script type="text/javascript">
@@ -110,7 +110,7 @@
     };
   </script>
   </div>
-====
+----
 
 Please consult the Google Analytics documentation for the correct
 setup code (the first two script tags).  The above is shown only
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index 27b39eb..2707e5c 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -21,16 +21,18 @@
 Out of the box, Gerrit includes a plugin that checks the length of the
 subject and body lines of commit messages on uploaded commits.
 
-[[ref-operation-validation]]
-== Ref operation validation
+[[user-ref-operations-validation]]
+== User ref operations validation
 
 
 Plugins implementing the `RefOperationValidationListener` interface can
-perform additional validation checks against ref creation/deletion operation
-before it is applied to the git repository.
+perform additional validation checks against user ref operations (resulting
+from either push or corresponding Gerrit REST/SSH endpoints call e.g.
+create branch etc.). Namely including ref creation, deletion and update
+(also non-fast-forward) before they are applied to the git repository.
 
-If the ref operation fails the validation, the plugin can throw an exception
-which will cause the operation to fail.
+The plugin can throw an exception which will cause the operation to fail,
+and prevent the ref update from being applied.
 
 [[pre-merge-validation]]
 == Pre-merge validation
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index 53f5c32..73d2e98 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -105,7 +105,8 @@
   buck build gerrit
 ----
 
-*Note: PolyGerrit UI may require additional tools (such as npm). Please read
+[NOTE]
+PolyGerrit UI may require additional tools (such as npm). Please read
 the polygerrit-ui/README.md for more info.
 
 The output executable WAR will be placed in:
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index b8d01e8..4fa542d 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -24,6 +24,12 @@
   Could not write generated class ... javax.annotation.processing.FilerException: Source file already created
 ----
 
+and
+
+----
+  AutoAnnotation_Commands_named cannot be resolved to a type
+----
+
 In Eclipse, choose 'Import existing project' and select the `gerrit` project
 from the current working directory.
 
diff --git a/Documentation/dev-inspector.txt b/Documentation/dev-inspector.txt
index 7c13a7d..2134f2f 100644
--- a/Documentation/dev-inspector.txt
+++ b/Documentation/dev-inspector.txt
@@ -4,14 +4,15 @@
 Gerrit Inspector - Interactive Jython environment for Gerrit
 
 == SYNOPSIS
+[verse]
 --
-'java' -jar gerrit.war 'daemon'
-	-d <SITE_PATH>
-	[\--enable-httpd | \--disable-httpd]
-	[\--enable-sshd | \--disable-sshd]
-	[\--console-log]
-	[\--slave]
-	-s
+_java_ -jar gerrit.war _daemon_
+  -d <SITE_PATH>
+  [--enable-httpd | --disable-httpd]
+  [--enable-sshd | --disable-sshd]
+  [--console-log]
+  [--slave]
+  -s
 --
 
 == DESCRIPTION
@@ -283,7 +284,8 @@
 == KNOWN ISSUES
 The Inspector does not yet recognize Google Guice bindings.
 
-IMPORTANT: Using the Inspector may void your warranty.
+[IMPORTANT]
+Using the Inspector may void your warranty.
 
 GERRIT
 ------
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 0b275c1..57e6d01 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -36,7 +36,7 @@
 ----
 mvn archetype:generate -DarchetypeGroupId=com.google.gerrit \
     -DarchetypeArtifactId=gerrit-plugin-archetype \
-    -DarchetypeVersion=2.13-SNAPSHOT \
+    -DarchetypeVersion=2.14-SNAPSHOT \
     -DgroupId=com.googlesource.gerrit.plugins.testplugin \
     -DartifactId=testplugin
 ----
@@ -90,11 +90,11 @@
 Plugins may provide optional description information with standard
 manifest fields:
 
-====
+----
   Implementation-Title: Example plugin showing examples
   Implementation-Version: 1.0
   Implementation-Vendor: Example, Inc.
-====
+----
 
 === ApiType
 
@@ -104,9 +104,9 @@
 API will be assumed. This may cause ClassNotFoundExceptions when
 loading a plugin that needs the plugin API.
 
-====
+----
   Gerrit-ApiType: plugin
-====
+----
 
 === Explicit Registration
 
@@ -119,20 +119,20 @@
 will be performed by scanning all classes in the plugin JAR for
 `@Listen` and `@Export("")` annotations.
 
-====
+----
   Gerrit-Module:     tld.example.project.CoreModuleClassName
   Gerrit-SshModule:  tld.example.project.SshModuleClassName
   Gerrit-HttpModule: tld.example.project.HttpModuleClassName
-====
+----
 
 [[plugin_name]]
 === Plugin Name
 
 A plugin can optionally provide its own plugin name.
 
-====
+----
   Gerrit-PluginName: replication
-====
+----
 
 This is useful for plugins that contribute plugin-owned capabilities that
 are stored in the `project.config` file. Another use case is to be able to put
@@ -216,9 +216,9 @@
 be used, as it enables the server to hot-patch an updated plugin
 with no down time.
 
-====
+----
   Gerrit-ReloadMode: restart
-====
+----
 
 In either mode ('restart' or 'reload') any plugin or extension can
 be updated without restarting the Gerrit server. The difference is
@@ -259,9 +259,9 @@
 contribute their own "init step" to allow configuring the Jira URL,
 credentials and possibly verify connectivity to validate them.
 
-====
+----
   Gerrit-InitStep: tld.example.project.MyInitStep
-====
+----
 
 MyInitStep needs to follow the standard Gerrit InitStep syntax
 and behavior: writing to the console using the injected ConsoleUI
@@ -425,10 +425,35 @@
 Gerrit's `stream-events` ssh command will receive them.
 
 To send an event, the plugin must invoke one of the `postEvent`
-methods in the `ChangeHookRunner` class, passing an instance of
+methods in the `EventDispatcher` interface, passing an instance of
 its own custom event class derived from
 `com.google.gerrit.server.events.Event`.
 
+[source,java]
+----
+import com.google.gerrit.common.EventDispatcher;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+class MyPlugin {
+  private final DynamicItem<EventDispatcher> eventDispatcher;
+
+  @Inject
+  myPlugin(DynamicItem<EventDispatcher> eventDispatcher) {
+    this.eventDispatcher = eventDispatcher;
+  }
+
+  private void postEvent(MyPluginEvent event) {
+    try {
+      eventDispatcher.get().postEvent(event);
+    } catch (OrmException e) {
+      // error handling
+    }
+  }
+}
+----
+
 Plugins which define new Events should register them via the
 `com.google.gerrit.server.events.EventTypes.registerClass()`
 method. This will make the EventType known to the system.
@@ -473,6 +498,12 @@
 for those plugins which would like to monitor usage in Git
 repositories.
 
+[[post-upload-hook]]
+== Post Upload-Pack Hooks
+
+Plugins may register PostUploadHook instances in order to get notified after
+JGit is done uploading a pack.
+
 [[ssh]]
 == SSH Commands
 
@@ -712,6 +743,18 @@
   reviewer = My Info Developers
 ----
 
+Plugins that have sensitive configuration settings can store those settings in
+an own secure configuration file. The plugin's secure configuration file must be
+named after the plugin and must be located in the `etc` folder of the review
+site. For example a secure configuration file for a `default-reviewer` plugin
+could look like this:
+
+.$site_path/etc/default-reviewer.secure.config
+----
+[auth]
+  password = secret
+----
+
 Via the `com.google.gerrit.server.config.PluginConfigFactory` class a
 plugin can easily access its configuration:
 
@@ -724,6 +767,8 @@
 
 String[] reviewers = cfg.getGlobalPluginConfig("default-reviewer")
                         .getStringList("branch", "refs/heads/master", "reviewer");
+String password = cfg.getGlobalPluginConfig("default-reviewer")
+                     .getString("auth", null, "password");
 ----
 
 
@@ -1055,10 +1100,18 @@
 Panel will be shown in the header bar on the right side of the pop down
 buttons.
 
+** `GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK`:
++
+Panel will be shown below the commit info block.
+
 ** `GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK`:
 +
 Panel will be shown below the change info block.
 
+** `GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_RELATED_INFO_BLOCK`:
++
+Panel will be shown below the related info block.
+
 ** The following parameters are provided:
 *** `GerritUiExtensionPoint.Key.CHANGE_INFO`:
 +
@@ -1355,13 +1408,13 @@
 Every `UiAction` exposes a REST API endpoint. The endpoint from the example above
 can be accessed from any REST client, i. e.:
 
-====
+----
   curl -X POST -H "Content-Type: application/json" \
     -d '{message: "François", french: true}' \
     --digest --user joe:secret \
     http://host:port/a/changes/1/revisions/1/cookbook~say-hello
   "Bonjour François from change 1, patch set 1!"
-====
+----
 
 A special case is to bind an endpoint without a view name.  This is
 particularly useful for `DELETE` requests:
@@ -2070,12 +2123,13 @@
 [[lfs-extension]]
 == LFS Storage Plugins
 
-Gerrit provides an extension point that enables development of LFS (Large File
-Storage) storage plugins. Gerrit core exposes the default LFS protocol endpoint
-`<project-name>/info/lfs/objects/batch` and forwards the requests to the configured
-link:config-gerrit.html#lfs[lfs.plugin] plugin which implements the LFS protocol.
-By exposing the default LFS endpoint, the git-lfs client can be used without
-any configuration.
+Gerrit provides an extension point that enables development of
+link:https://github.com/github/git-lfs/blob/master/docs/api/v1/http-v1-batch.md[
+LFS (Large File Storage)] storage plugins. Gerrit core exposes the default LFS
+protocol endpoint `<project-name>/info/lfs/objects/batch` and forwards the requests
+to the configured link:config-gerrit.html#lfs[lfs.plugin] plugin which implements
+the LFS protocol. By exposing the default LFS endpoint, the git-lfs client can be
+used without any configuration.
 
 [source, java]
 ----
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index 6f11158..921244f 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -142,7 +142,8 @@
   </distributionManagement>
 ----
 
-NOTE: In case of JGit the `pom.xml` already contains a distributionManagement
+[NOTE]
+In case of JGit the `pom.xml` already contains a distributionManagement
 section.  Replace the existing distributionManagement section with this snippet
 in order to deploy the artifacts only in the gerrit-maven repository.
 
@@ -150,10 +151,12 @@
 * Add these two snippets to the `pom.xml` to enable the wagon provider:
 
 ----
-  <pluginRepository>
-    <id>google-storage</id>
-    <url>https://console.developers.google.com/storage/browser/gerrit-maven/</url>
-  </pluginRepository>
+  <pluginRepositories>
+    <pluginRepository>
+      <id>gerrit-maven</id>
+      <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
+    </pluginRepository>
+  </pluginRepositories>
 ----
 
 ----
diff --git a/Documentation/dev-release-subproject.txt b/Documentation/dev-release-subproject.txt
index 9571edb..fcafea5 100644
--- a/Documentation/dev-release-subproject.txt
+++ b/Documentation/dev-release-subproject.txt
@@ -6,9 +6,9 @@
 * Build the latest snapshot and install it into the local Maven
 repository:
 +
-====
+----
   mvn clean install
-====
+----
 
 * Test Gerrit with this snapshot locally
 
@@ -27,9 +27,9 @@
 
 * Deploy the new snapshot:
 +
-====
+----
   mvn deploy
-====
+----
 
 * Change the `id`, `bin_sha1`, and `src_sha1` values in the `maven_jar`
 for the subproject in `/lib/BUCK` to the `SNAPSHOT` version.
@@ -50,15 +50,15 @@
 
 * Create the Release Tag
 +
-====
- git tag -a -m "prolog-cafe 1.3" v1.3
-====
+----
+  git tag -a -m "prolog-cafe 1.3" v1.3
+----
 
 * Build and install into local Maven repository:
 +
-====
+----
   mvn clean install
-====
+----
 
 
 [[publish-release]]
@@ -72,18 +72,18 @@
 
 * Deploy the new release:
 +
-====
+----
   mvn deploy
-====
+----
 
 * Push the pom change(s) to the project's repository
 `refs/for/<master|stable>`
 
 * Push the Release Tag
 +
-====
+----
   git push gerrit-review refs/tags/v1.3:refs/tags/v1.3
-====
+----
 
 
 GERRIT
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 5ede117..7190a43 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -1,12 +1,10 @@
 = Making a Gerrit Release
 
 [NOTE]
-========================================================================
 This document is meant primarily for Gerrit maintainers
 who have been given approval and submit status to the Gerrit
 projects.  Additionally, maintainers should be given owner
 status to the Gerrit web site.
-========================================================================
 
 To make a Gerrit release involves a great deal of complex
 tasks and it is easy to miss a step so this document should
@@ -34,16 +32,12 @@
 * If needed create a Gerrit `RC1`
 
 [NOTE]
-========================================================================
 You may let in a few features to this release
-========================================================================
 
 * If needed create a Gerrit `RC2`
 
 [NOTE]
-========================================================================
 There should be no new features in this release, only bug fixes
-========================================================================
 
 * Finally create the `stable` release (no `RC`)
 
@@ -358,20 +352,15 @@
 [[update-issues]]
 ==== Update the Issues
 
-====
- How do the issues get updated?  Do you run a script to do
- this?  When do you do it, after the final 2.5 is released?
-====
-
-By hand.
+Update the issues by hand. There is no script for this.
 
 Our current process is an issue should be updated to say `Status =
 Submitted, FixedIn-2.5` once the change is submitted, but before the
 release.
 
 After the release is actually made, you can search in Google Code for
-``Status=Submitted FixedIn=2.5'' and then batch update these changes
-to say `Status=Released`. Make sure the pulldown says ``All Issues''
+`Status=Submitted FixedIn=2.5` and then batch update these changes
+to say `Status=Released`. Make sure the pulldown says `All Issues`
 because `Status=Submitted` is considered a closed issue.
 
 
diff --git a/Documentation/error-has-duplicates.txt b/Documentation/error-has-duplicates.txt
index 8294c12..a520f5d 100644
--- a/Documentation/error-has-duplicates.txt
+++ b/Documentation/error-has-duplicates.txt
@@ -1,4 +1,4 @@
-= \... has duplicates
+= ... has duplicates
 
 With this error message Gerrit rejects to push a commit if its commit
 message contains a Change-Id for which multiple changes can be found
diff --git a/Documentation/error-upload-denied.txt b/Documentation/error-upload-denied.txt
index 6de94b4..30c5f2d 100644
--- a/Documentation/error-upload-denied.txt
+++ b/Documentation/error-upload-denied.txt
@@ -1,5 +1,4 @@
-Upload denied for project \'...'
-=================================
+= Upload denied for project ...
 
 With this error message Gerrit rejects to push a commit if the
 pushing user has no upload permissions for the project to which the
diff --git a/Documentation/gen_licenses.py b/Documentation/gen_licenses.py
index bc2d657..15f470c 100755
--- a/Documentation/gen_licenses.py
+++ b/Documentation/gen_licenses.py
@@ -95,16 +95,14 @@
 
 if args.asciidoc:
   print("""\
-Gerrit Code Review - Licenses
-=============================
+= Gerrit Code Review - Licenses
 
 Gerrit open source software is licensed under the <<Apache2_0,Apache
 License 2.0>>.  Executable distributions also include other software
 components that are provided under additional licenses.
 
 [[cryptography]]
-Cryptography Notice
--------------------
+== Cryptography Notice
 
 This distribution includes cryptographic software.  The country
 in which you currently reside may have restrictions on the import,
@@ -139,8 +137,7 @@
 link:http://www.bouncycastle.org/java.html[Bouncy Castle Crypto API]
 to be installed by the end-user.
 
-Licenses
---------
+== Licenses
 """)
 
 for n in used:
@@ -149,13 +146,13 @@
   if args.asciidoc:
     print()
     print('[[%s]]' % name.replace('.', '_'))
-    print(name)
-    print('~' * len(name))
+    print("=== " + name)
     print()
   else:
     print()
     print(name)
-    print('--')
+    print()
+    print('----')
   for d in libs:
     if d.startswith('//lib:') or d.startswith('//lib/'):
       p = d[len('//lib:'):]
@@ -166,12 +163,12 @@
     print('* ' + p)
   if args.asciidoc:
     print()
-    print('[[license]]')
-    print('[verse]')
-    print('--')
+    print('[[%s_license]]' % name.replace('.', '_'))
+    print('----')
   with open(n[2:].replace(':', '/')) as fd:
     copyfileobj(fd, stdout)
-  print('--')
+  print()
+  print('----')
 
 if args.asciidoc:
   print("""
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 93352d9..594c028 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -34,7 +34,6 @@
 . Prolog rules
 .. link:prolog-cookbook.html[Prolog Cookbook]
 .. link:prolog-change-facts.html[Prolog Facts for Gerrit Changes]
-. link:user-submodules.html[Subscribing to Git Submodules]
 . link:intro-project-owner.html#project-deletion[Project deletion]
 
 == Customization and Integration
diff --git a/Documentation/install.txt b/Documentation/install.txt
index 3f7d1c1..e3fb28d 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -17,7 +17,8 @@
 Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files_
 from Oracle and installing them into your JRE.
 
-NOTE: Installing JCE extensions is optional and export restrictions may apply.
+[NOTE]
+Installing JCE extensions is optional and export restrictions may apply.
 
 . Download the unlimited strength JCE policy files.
 +
@@ -82,7 +83,8 @@
   java -jar gerrit.war init -d /path/to/your/gerrit_application_directory
 ----
 
-'Please note:' If you choose a location where your new user doesn't
+[NOTE]
+If you choose a location where your new user doesn't
 have any privileges, you may have to manually create the directory first and
 then give ownership of that location to the `'gerrit2'` user.
 
@@ -137,11 +139,11 @@
 To control the Gerrit Code Review daemon that is running in the
 background, use the rc.d style start script created by 'init':
 
-====
+----
   review_site/bin/gerrit.sh start
   review_site/bin/gerrit.sh stop
   review_site/bin/gerrit.sh restart
-====
+----
 
 ('Optional') Configure the daemon to automatically start and stop
 with the operating system.
@@ -149,18 +151,18 @@
 Uncomment the following 3 lines in the `'$site_path/bin/gerrit.sh'`
 script:
 
-====
+----
  chkconfig: 3 99 99
  description: Gerrit Code Review
  processname: gerrit
-====
+----
 
 Then link the `gerrit.sh` script into `rc3.d`:
 
-====
+----
   sudo ln -snf `pwd`/review_site/bin/gerrit.sh /etc/init.d/gerrit
   sudo ln -snf /etc/init.d/gerrit /etc/rc3.d/S90gerrit
-====
+----
 
 ('Optional') To enable autocompletion of the gerrit.sh commands, install
 autocompletion from the `/contrib/bash_completion` script.  Refer to the
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index dfffe57..f982212 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -69,11 +69,11 @@
 cloned the repository you can do this by executing the following
 commands:
 
-====
+----
   $ git fetch origin refs/meta/config:config
   $ git checkout config
   $ git log project.config
-====
+----
 
 Non project owners may still edit the access rights and propose the
 modifications to the project owners by clicking on the `Save for
@@ -519,11 +519,11 @@
 to an issue in an issue tracker system. For example, to link the ID
 from the `Bug` footer to Jira the following configuration can be used:
 
-====
+----
   [commentlink "myjira"]
     match = ([Bb][Uu][Gg]:\\s+)(\\S+)
     link =  https://myjira/browse/$2
-====
+----
 
 [[reviewers]]
 == Reviewers
@@ -594,11 +594,11 @@
 The project-specific download commands must be configured in the
 `project.config` file in the `refs/meta/config` branch of the project:
 +
-====
+----
   [plugin "project-download-commands"]
     Build = git fetch ${url} ${ref} && git checkout FETCH_HEAD && buck build ${project}
     Update = git fetch ${url} ${ref} && git checkout FETCH_HEAD && git submodule update
-====
+----
 +
 Project-specific download commands that are defined on a parent project
 are inherited by the child projects. A child project can overwrite an
@@ -705,9 +705,9 @@
 commits (the author information that records who was writing the code
 stays intact; signed tags will lose their signature):
 
-====
+----
   $ git filter-branch --tag-name-filter cat --env-filter 'GIT_COMMITTER_NAME="John Doe"; GIT_COMMITTER_EMAIL="john.doe@example.com";' -- --all
-====
+----
 
 If a link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
 limit] is configured on the server you may need to remove large objects
@@ -715,20 +715,22 @@
 the history of your project you can use the `reposize.sh` script which
 you can download from Gerrit:
 
+----
   $ curl -Lo reposize.sh http://review.example.com:8080/tools/scripts/reposize.sh
 
 or
 
   $ scp -p -P 29418 john.doe@review.example.com:scripts/reposize.sh .
+----
 
 You can then use the
 link:https://www.kernel.org/pub/software/scm/git/docs/git-filter-branch.html[
 git filter-branch] command to remove the large objects from the history
 of all branches:
 
-====
+----
   $ git filter-branch -f --index-filter 'git rm --cached --ignore-unmatch path/to/large-file.jar' -- --all
-====
+----
 
 Since this command rewrites all commits in the repository it's a good
 idea to create a fresh clone from this rewritten repository before
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 9b283b4..535248d 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -644,9 +644,9 @@
 Whether the Side-by-Side diff view or the Unified diff view should be
 shown when clicking on a file path in the change screen.
 
-- [[show-site-header]]`Show Site Header`:
+- [[show-site-header]]`Show Site Header / Footer`:
 +
-Whether the site header should be shown.
+Whether the site header and footer should be shown.
 
 - [[relative-dates]]`Show Relative Dates In Changes Table`:
 +
diff --git a/Documentation/pgm-LocalUsernamesToLowerCase.txt b/Documentation/pgm-LocalUsernamesToLowerCase.txt
index 09651c2..1136ced 100644
--- a/Documentation/pgm-LocalUsernamesToLowerCase.txt
+++ b/Documentation/pgm-LocalUsernamesToLowerCase.txt
@@ -5,8 +5,11 @@
 account to lower case
 
 == SYNOPSIS
+[verse]
 --
-'java' -jar gerrit.war 'LocalUsernamesToLowerCase' -d <SITE_PATH>
+_java_ -jar gerrit.war _LocalUsernamesToLowerCase
+  -d <SITE_PATH>
+  [--threads]
 --
 
 == DESCRIPTION
@@ -48,9 +51,9 @@
 == EXAMPLES
 To convert the local username of every account to lower case:
 
-====
+----
 	$ java -jar gerrit.war LocalUsernamesToLowerCase -d site_path
-====
+----
 
 == SEE ALSO
 
diff --git a/Documentation/pgm-SwitchSecureStore.txt b/Documentation/pgm-SwitchSecureStore.txt
index f9b2aa4..47de1be 100644
--- a/Documentation/pgm-SwitchSecureStore.txt
+++ b/Documentation/pgm-SwitchSecureStore.txt
@@ -4,8 +4,10 @@
 SwitchSecureStore - Changes the currently used SecureStore implementation
 
 == SYNOPSIS
+[verse]
 --
-'java' -jar gerrit.war 'SwitchSecureStore' [<OPTIONS>]
+_java_ -jar gerrit.war _SwitchSecureStore_
+  [--new-secure-store-lib]
 --
 
 == DESCRIPTION
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index bcf2b1b..76a26e1 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -4,16 +4,17 @@
 daemon - Gerrit network server
 
 == SYNOPSIS
+[verse]
 --
-'java' -jar gerrit.war 'daemon'
-	-d <SITE_PATH>
-	[--enable-httpd | --disable-httpd]
-	[--enable-sshd | --disable-sshd]
-	[--console-log]
-	[--slave]
-	[--headless]
-	[--init]
-	[-s]
+_java_ -jar gerrit.war _daemon_
+  -d <SITE_PATH>
+  [--enable-httpd | --disable-httpd]
+  [--enable-sshd | --disable-sshd]
+  [--console-log]
+  [--slave]
+  [--headless]
+  [--init]
+  [-s]
 --
 
 == DESCRIPTION
diff --git a/Documentation/pgm-gsql.txt b/Documentation/pgm-gsql.txt
index ba40b26..4986522 100644
--- a/Documentation/pgm-gsql.txt
+++ b/Documentation/pgm-gsql.txt
@@ -4,8 +4,10 @@
 gsql - Administrative interface to idle database
 
 == SYNOPSIS
+[verse]
 --
-'java' -jar gerrit.war 'gsql' -d <SITE_PATH>
+_java_ -jar gerrit.war _gsql_
+  -d <SITE_PATH>
 --
 
 == DESCRIPTION
@@ -32,7 +34,7 @@
 == EXAMPLES
 To manually correct a user's SSH user name:
 
-====
+----
 	$ java -jar gerrit.war gsql
 	Welcome to Gerrit Code Review v2.0.25
 	(PostgreSQL 8.3.8)
@@ -43,7 +45,7 @@
 	UPDATE 1; 1 ms
 	gerrit> \q
 	Bye
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index 22f9109..40c2b30 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -4,19 +4,20 @@
 init - Initialize/Upgrade a Gerrit server installation
 
 == SYNOPSIS
+[verse]
 --
-'java' -jar gerrit.war 'init'
-	-d <SITE_PATH>
-	[--batch]
-	[--no-auto-start]
-	[--skip-plugins]
-	[--list-plugins]
-	[--install-plugin=<PLUGIN_NAME>]
-	[--install-all-plugins]
-	[--secure-store-lib]
-	[--dev]
-	[--skip-all-downloads]
-	[--skip-download=<LIBRARY_NAME>]
+_java_ -jar gerrit.war _init_
+  -d <SITE_PATH>
+  [--batch]
+  [--no-auto-start]
+  [--skip-plugins]
+  [--list-plugins]
+  [--install-plugin=<PLUGIN_NAME>]
+  [--install-all-plugins]
+  [--secure-store-lib]
+  [--dev]
+  [--skip-all-downloads]
+  [--skip-download=<LIBRARY_NAME>]
 --
 
 == DESCRIPTION
diff --git a/Documentation/pgm-prolog-shell.txt b/Documentation/pgm-prolog-shell.txt
index 9861310..aee5799 100644
--- a/Documentation/pgm-prolog-shell.txt
+++ b/Documentation/pgm-prolog-shell.txt
@@ -4,8 +4,10 @@
 prolog-shell - Simple interactive Prolog interpreter
 
 == SYNOPSIS
+[verse]
 --
-'java' -jar gerrit.war 'prolog-shell' [-s FILE.pl ...]
+_java_ -jar gerrit.war _prolog-shell_
+  [-s FILE.pl ...]
 --
 
 == DESCRIPTION
@@ -22,7 +24,7 @@
 == EXAMPLES
 Define a simple predicate and test it:
 
-====
+----
 	$ cat >simple.pl
 	food(apple).
 	food(orange).
@@ -45,7 +47,7 @@
 
 	no
 	| ?-
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/pgm-reindex.txt b/Documentation/pgm-reindex.txt
index f081843..e13d518 100644
--- a/Documentation/pgm-reindex.txt
+++ b/Documentation/pgm-reindex.txt
@@ -4,8 +4,14 @@
 reindex - Rebuild the secondary index
 
 == SYNOPSIS
+[verse]
 --
-'java' -jar gerrit.war 'reindex' [<OPTIONS>]
+_java_ -jar gerrit.war _reindex_
+  [--threads]
+  [--changes-schema-version]
+  [--verbose]
+  [--list]
+  [--index]
 --
 
 == DESCRIPTION
@@ -15,15 +21,12 @@
 --threads::
 	Number of threads to use for indexing.
 
---schema-version::
+--changes-schema-version::
 	Schema version to reindex; default is most recent version.
 
 --verbose::
 	Output debug information for each change.
 
---dry-run::
-	Dry run.  Don't write anything to index.
-
 --list::
 	List available index names.
 
diff --git a/Documentation/pgm-rulec.txt b/Documentation/pgm-rulec.txt
index 3236c38..1b50812 100644
--- a/Documentation/pgm-rulec.txt
+++ b/Documentation/pgm-rulec.txt
@@ -4,8 +4,12 @@
 rulec - Compile project-specific Prolog rules to JARs
 
 == SYNOPSIS
+[verse]
 --
-'java' -jar gerrit.war 'rulec' -d <SITE_PATH> [--all | <PROJECT>...]
+_java_ -jar gerrit.war _rulec_
+  -d <SITE_PATH>
+  [--quiet]
+  [--all | <PROJECT>...]
 --
 
 == DESCRIPTION
@@ -39,9 +43,9 @@
 == EXAMPLES
 To compile a rule JAR file for test/project:
 
-====
+----
 	$ java -jar gerrit.war rulec -d site_path test/project
-====
+----
 
 GERRIT
 ------
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index 2b0db70b..d71d19a 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -22,9 +22,9 @@
 
 . Create a Git repository under `gerrit.basePath`:
 +
-====
+----
   git --git-dir=$base_path/new/project.git init
-====
+----
 +
 [TIP]
 By tradition the repository directory name should have a `.git`
@@ -33,17 +33,17 @@
 To also make this repository available over the anonymous git://
 protocol, don't forget to create a `git-daemon-export-ok` file:
 +
-====
+----
   touch $base_path/new/project.git/git-daemon-export-ok
-====
+----
 
 . Register Project
 +
 Either restart the server, or flush the `project_list` cache:
 +
-====
+----
   ssh -p 29418 localhost gerrit flush-caches --cache project_list
-====
+----
 
 [[project_options]]
 == Project Options
@@ -262,9 +262,9 @@
   REST endpoint
 - by using a git client to force push nothing to an existing branch
 +
-====
+----
   $ git push --force origin :refs/heads/<branch-to-delete>
-====
+----
 
 To be able to delete branches, the user must have the
 link:access-control.html#category_push[Push] access right with the
diff --git a/Documentation/prolog-change-facts.txt b/Documentation/prolog-change-facts.txt
index 826f56c..cbd4070 100644
--- a/Documentation/prolog-change-facts.txt
+++ b/Documentation/prolog-change-facts.txt
@@ -4,7 +4,8 @@
 the Prolog engine with a set of facts (current data) about this change.
 The following table provides an overview of the provided facts.
 
-IMPORTANT: All the terms listed below are defined in the `gerrit` package. To use any
+[IMPORTANT]
+All the terms listed below are defined in the `gerrit` package. To use any
 of them we must use a qualified name like `gerrit:change_branch(X)`.
 
 .Prolog facts about the current change
@@ -97,7 +98,8 @@
 
 |=============================================================================
 
-NOTE: for a complete list of built-in helpers read the `gerrit_common.pl` and
+[NOTE]
+For a complete list of built-in helpers read the `gerrit_common.pl` and
 all Java classes whose name matches `PRED_*.java` from Gerrit's source code.
 
 GERRIT
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 22c2b21..6754eea 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -17,7 +17,8 @@
 submittable. For a change that is not submittable, the set of needed criteria
 is displayed in the Gerrit UI.
 
-NOTE: Loading and executing Prolog submit rules may be disabled by setting
+[NOTE]
+Loading and executing Prolog submit rules may be disabled by setting
 `rules.enable=false` in the Gerrit config file (see
 link:config-gerrit.html#_a_id_rules_a_section_rules[rules section])
 
@@ -73,7 +74,8 @@
 link:pgm-prolog-shell.html[prolog-shell] program which opens an interactive
 Prolog interpreter shell.
 
-NOTE: The interactive shell is just a prolog shell, it does not load
+[NOTE]
+The interactive shell is just a prolog shell, it does not load
 a gerrit server environment and thus is not intended for
 xref:TestingSubmitRules[testing submit rules].
 
@@ -92,14 +94,14 @@
 checkout the `refs/meta/config` branch in order to create or edit the `rules.pl`
 file:
 
-====
+----
   $ git fetch origin refs/meta/config:config
   $ git checkout config
   ... edit or create the rules.pl file
   $ git add rules.pl
   $ git commit -m "My submit rules"
   $ git push origin HEAD:refs/meta/config
-====
+----
 
 [[HowToWriteSubmitRules]]
 == How to write submit rules
@@ -113,14 +115,14 @@
 `C` on top of the `rules.pl` file and then consults it. The set of facts about
 the change `C` will look like:
 
-====
+----
   :- package gerrit.                                                   <1>
 
   commit_author(user(1000000), 'John Doe', 'john.doe@example.com').    <2>
   commit_committer(user(1000000), 'John Doe', 'john.doe@example.com'). <3>
   commit_message('Add plugin support to Gerrit').                      <4>
   ...
-====
+----
 
 <1> Gerrit will provide its facts in a package named `gerrit`. This means we
 have to use qualified names when writing our code and referencing these facts.
@@ -139,31 +141,33 @@
 an expectation on the format and value of the result of the `submit_rule`
 predicate which is expected to be a `submit` term of the following format:
 
-====
+----
   submit(label(label-name, status) [, label(label-name, status)]*)
-====
+----
 
 where `label-name` is usually `'Code-Review'` or `'Verified'` but could also
 be any other string (see examples below). The `status` is one of:
 
-* `ok(user(ID))` or just `ok(_)` if user info is not important. This status is
-  used to tell that this label/category has been met.
+* `ok(user(ID))`. This status is used to tell that this label/category has been
+  met.
 * `need(_)` is used to tell that this label/category is needed for the change to
-   become submittable.
-* `reject(user(ID))` or just `reject(_)`. This status is used to tell that this
-   label/category is blocking submission of the change.
+  become submittable.
+* `reject(user(ID))`. This status is used to tell that this label/category is
+  blocking submission of the change.
 * `impossible(_)` is used when the logic knows that the change cannot be submitted
-   as-is. This is meant for cases where the logic requires members of a specific
-   group to apply a specific label on a change, but no users are in that group.
-   This is usually caused by misconfiguration of permissions.
+  as-is. This is meant for cases where the logic requires members of a specific
+  group to apply a specific label on a change, but no users are in that group.
+  This is usually caused by misconfiguration of permissions.
 * `may(_)` allows expression of approval categories that are optional, i.e.
   could either be set or unset without ever influencing whether the change
   could be submitted.
 
-NOTE: For a change to be submittable all `label` terms contained in the returned
+[NOTE]
+For a change to be submittable all `label` terms contained in the returned
 `submit` term must have either `ok` or `may` status.
 
-IMPORTANT: Gerrit will let the Prolog engine continue searching for solutions of
+[IMPORTANT]
+Gerrit will let the Prolog engine continue searching for solutions of
 the `submit_rule(X)` query until it finds the first one where all labels in the
 return result have either status `ok` or `may` or there are no more solutions.
 If a solution where all labels have status `ok` is found then all previously
@@ -173,11 +177,12 @@
 
 Here some examples of possible return values from the `submit_rule` predicate:
 
-====
-  submit(label('Code-Review', ok(_)))                               <1>
-  submit(label('Code-Review', ok(_)), label('Verified', reject(_))) <2>
+----
+  submit(label('Code-Review', ok(user(ID))))                        <1>
+  submit(label('Code-Review', ok(user(ID))),
+      label('Verified', reject(user(ID))))                          <2>
   submit(label('Author-is-John-Doe', need(_))                       <3>
-====
+----
 
 <1> label `'Code-Review'` is met. As there are no other labels in the
     return result, the change is submittable.
@@ -185,7 +190,7 @@
 <3> label `'Author-is-John-Doe'` is needed for the change to become submittable.
     Note that this tells nothing about how this criteria will be met. It is up
     to the implementer of the `submit_rule` to return
-    `label('Author-is-John-Doe', ok(_))` when this criteria is met.  Most
+    `label('Author-is-John-Doe', ok(user(ID)))` when this criteria is met. Most
     likely, it will have to match against `gerrit:commit_author` in order to
     check if this criteria is met. This will become clear through the examples
     below.
@@ -217,10 +222,9 @@
 of the `submit_rule`. Therefore, the `submit_filter` predicate has two
 parameters:
 
-====
+----
   submit_filter(In, Out) :- ...
-====
-
+----
 Gerrit will invoke `submit_filter` with the `In` parameter containing a `submit`
 structure produced by the `submit_rule` and will take the value of the `Out`
 parameter as the result.
@@ -230,7 +234,8 @@
 of the top-most `submit_filter` is the final result of the submit rule that
 is used to decide whether a change is submittable or not.
 
-IMPORTANT: `submit_filter` is a mechanism for Gerrit administrators to implement
+[IMPORTANT]
+`submit_filter` is a mechanism for Gerrit administrators to implement
 and enforce submit rules that would apply to all projects while `submit_rule` is
 a mechanism for project owners to implement project specific submit rules.
 However, project owners who own several projects could also make use of
@@ -240,7 +245,7 @@
 
 The following "drawing" illustrates the order of the invocation and the chaining
 of the results of the `submit_rule` and `submit_filter` predicates.
-====
+----
   All-Projects
   ^   submit_filter(B, S) :- ...  <4>
   |
@@ -255,7 +260,7 @@
   |
   MyProject
       submit_rule(X) :- ...       <1>
-====
+----
 
 <1> The `submit_rule` of `MyProject` is invoked first.
 <2> The result `X` is filtered through the `submit_filter` from the `Parent-1`
@@ -267,7 +272,8 @@
 `submit_filter` in the `All-Projects` project. The value in `S` is the final
 value of the submit rule evaluation.
 
-NOTE: If `MyProject` doesn't define its own `submit_rule` Gerrit will invoke the
+[NOTE]
+If `MyProject` doesn't define its own `submit_rule` Gerrit will invoke the
 default implementation of submit rule that is named `gerrit:default_submit` and
 its result will be filtered as described above.
 
@@ -289,9 +295,9 @@
 Submit type filter works the same way as the xref:SubmitFilter[Submit Filter]
 where the name of the filter predicate is `submit_type_filter`.
 
-====
+----
   submit_type_filter(In, Out).
-====
+----
 
 Gerrit will invoke `submit_type_filter` with the `In` parameter containing a
 result of the `submit_type` and will take the value of the `Out` parameter as
@@ -306,9 +312,9 @@
 and executes the `submit_rule`. It optionally reads the rule from from `stdin`
 to facilitate easy testing.
 
-====
+----
   $ cat rules.pl | ssh gerrit_srv gerrit test-submit rule I45e080b105a50a625cc8e1fb5b357c0bfabe6d68 -s
-====
+----
 
 == Prolog vs Gerrit plugin for project specific submit rules
 Since version 2.5 Gerrit supports plugins and extension points. A plugin or an
@@ -346,7 +352,7 @@
 [source,prolog]
 ----
 submit_rule(submit(W)) :-
-    W = label('Any-Label-Name', ok(_)).
+    W = label('Any-Label-Name', ok(user(1000000))).
 ----
 
 In this case we make no use of facts about the change. We don't need it as we
@@ -355,6 +361,14 @@
 `'Verified'` categories as labels with these names are not part of the return
 result. The `'Any-Label-Name'` could really be any string.
 
+The `user(1000000)` represents the user whose account ID is `1000000`.
+
+[NOTE]
+Instead of the account ID `1000000` we could have used any other account ID.
+The following examples will use `user(ID)` instead of `user(1000000)` because
+it is easier to read and doesn't suggest that there is anything special with
+the account ID `1000000`.
+
 === Example 2: Every change submittable and voting in the standard categories possible
 This is continuation of the previous example where, in addition, to making
 every change submittable we want to enable voting in the standard
@@ -364,8 +378,8 @@
 [source,prolog]
 ----
 submit_rule(submit(CR, V)) :-
-    CR = label('Code-Review', ok(_)),
-    V = label('Verified', ok(_)).
+    CR = label('Code-Review', ok(user(ID))),
+    V = label('Verified', ok(user(ID))).
 ----
 
 Since for every change all label statuses are `'ok'` every change will be
@@ -380,7 +394,7 @@
 [source,prolog]
 ----
 submit_rule(submit(R)) :-
-    R = label('Any-Label-Name', reject(_)).
+    R = label('Any-Label-Name', reject(user(ID))).
 ----
 
 Since for any change we return only one label with status `reject`, no change
@@ -434,7 +448,7 @@
     N = label('Some-Condition', need(_)).
 
 submit_rule(submit(OK)) :-
-    OK = label('Another-Condition', ok(_)).
+    OK = label('Another-Condition', ok(user(ID))).
 ----
 
 The `'Need Some-Condition'` will not be shown in the UI because of the result of
@@ -446,7 +460,7 @@
 [source,prolog]
 ----
 submit_rule(submit(OK)) :-
-    OK = label('Another-Condition', ok(_)).
+    OK = label('Another-Condition', ok(user(ID))).
 
 submit_rule(submit(N)) :-
     N = label('Some-Condition', need(_)).
@@ -482,8 +496,8 @@
     Author = label('Author-is-John-Doe', need(_)).
 
 submit_rule(submit(Author)) :-
-    gerrit:commit_author(_, 'John Doe', _),
-    Author = label('Author-is-John-Doe', ok(_)).
+    gerrit:commit_author(A, 'John Doe', _),
+    Author = label('Author-is-John-Doe', ok(A)).
 ----
 
 In the second rule we return `ok` status for the `'Author-is-John-Doe'` label
@@ -503,8 +517,8 @@
     Author = label('Author-is-John-Doe', need(_)).
 
 submit_rule(submit(Author)) :-
-    gerrit:commit_author(_, _, 'john.doe@example.com'),
-    Author = label('Author-is-John-Doe', ok(_)).
+    gerrit:commit_author(A, _, 'john.doe@example.com'),
+    Author = label('Author-is-John-Doe', ok(A)).
 ----
 
 or by user id (assuming it is `1000000`):
@@ -516,8 +530,9 @@
     Author = label('Author-is-John-Doe', need(_)).
 
 submit_rule(submit(Author)) :-
-    gerrit:commit_author(user(1000000), _, _),
-    Author = label('Author-is-John-Doe', ok(_)).
+    U = user(1000000),
+    gerrit:commit_author(U, _, _),
+    Author = label('Author-is-John-Doe', ok(U)).
 ----
 
 or by a combination of these 3 attributes:
@@ -529,8 +544,8 @@
     Author = label('Author-is-John-Doe', need(_)).
 
 submit_rule(submit(Author)) :-
-    gerrit:commit_author(_, 'John Doe', 'john.doe@example.com'),
-    Author = label('Author-is-John-Doe', ok(_)).
+    gerrit:commit_author(A, 'John Doe', 'john.doe@example.com'),
+    Author = label('Author-is-John-Doe', ok(A)).
 ----
 
 === Example 7: Make change submittable if commit message starts with "Fix "
@@ -556,13 +571,15 @@
 
 submit_rule(submit(Fix)) :-
     gerrit:commit_message(M), name(M, L), starts_with(L, "Fix "),
-    Fix = label('Commit-Message-starts-with-Fix', ok(_)).
+    gerrit:commit_author(A),
+    Fix = label('Commit-Message-starts-with-Fix', ok(A)).
 
 starts_with(L, []).
 starts_with([H|T1], [H|T2]) :- starts_with(T1, T2).
 ----
 
-NOTE: The `name/2` embedded predicate is used to convert a string symbol into a
+[NOTE]
+The `name/2` embedded predicate is used to convert a string symbol into a
 list of characters. A string `abc` is converted into a list of characters `[97,
 98, 99]`.  A double quoted string in Prolog is just a shortcut for creating a
 list of characters. `"abc"` is a shortcut for `[97, 98, 99]`. This is why we use
@@ -580,7 +597,8 @@
 
 submit_rule(submit(Fix)) :-
     gerrit:commit_message_matches('^Fix '),
-    Fix = label('Commit-Message-starts-with-Fix', ok(_)).
+    gerrit:commit_author(A),
+    Fix = label('Commit-Message-starts-with-Fix', ok(A)).
 ----
 
 The previous example could also be written so that it first checks if the commit
@@ -592,7 +610,8 @@
 ----
 submit_rule(submit(Fix)) :-
     gerrit:commit_message_matches('^Fix '),
-    Fix = label('Commit-Message-starts-with-Fix', ok(_)),
+    gerrit:commit_author(A),
+    Fix = label('Commit-Message-starts-with-Fix', ok(A)),
     !.
 
 % Message does not start with 'Fix ' so Fix is needed to submit
@@ -689,8 +708,8 @@
 
 This example uses the `univ` operator `=..` to "unpack" the result of the
 default_submit, which is a structure of the form `submit(label('Code-Review',
-ok(_)), label('Verified', need(_)), ...)` into a list like `[submit,
-label('Code-Review', ok(_)), label('Verified', need(_)), ...]`.  Then we
+ok(user(ID))), label('Verified', need(_)), ...)` into a list like `[submit,
+label('Code-Review', ok(user(ID))), label('Verified', need(_)), ...]`.  Then we
 process the tail of the list (the list of labels) as a Prolog list, which is
 much easier than processing a structure. In the end we use the same `univ`
 operator to convert the resulting list of labels back into a `submit` structure
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index f2a2bb2..2b44855 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -61,13 +61,18 @@
 for each account.
 --
 
+[[all-emails]]
+--
+* `ALL_EMAILS`: Includes all registered emails.
+--
+
 [[suggest-account]]
 To get account suggestions set the parameter `suggest` and provide the
 typed substring as query `q`. If a result limit `n` is not specified,
 then the default 10 is used.
 
-For account suggestions link:#details[account details] are always
-returned.
+For account suggestions link:#details[account details] and
+link:#all-emails[all emails] are always returned.
 
 .Request
 ----
@@ -1648,25 +1653,9 @@
   ]
 ----
 
-As result the watched projects of the user are returned as a list of
-link:#project-watch-info[ProjectWatchInfo] entities.
-The result is sorted by project name in ascending order.
-
 .Response
 ----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  [
-    {
-      "project": "Test Project 2",
-      "notify_new_changes": true,
-      "notify_new_patch_sets": true,
-      "notify_all_comments": true,
-    }
-  ]
+  HTTP/1.1 204 No Content
 ----
 
 [[default-star-endpoints]]
@@ -1885,6 +1874,71 @@
   ]
 ----
 
+[[list-contributor-agreements]]
+=== List Contributor Agreements
+--
+'GET /accounts/link:#account-id[\{account-id\}]/agreements'
+--
+
+Gets a list of the user's signed contributor agreements.
+
+.Request
+----
+  GET /a/accounts/self/agreements HTTP/1.0
+----
+
+As response the user's signed agreements are returned as a list
+of link:#contributor-agreement-info[ContributorAgreementInfo] entities.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "name": "Individual",
+      "description": "If you are going to be contributing code on your own, this is the one you want. You can sign this one online.",
+      "url": "static/cla_individual.html"
+    }
+  ]
+----
+
+[[sign-contributor-agreement]]
+=== Sign Contributor Agreement
+--
+'PUT /accounts/link:#account-id[\{account-id\}]/agreements'
+--
+
+Signs a contributor agreement.
+
+The contributor agreement must be provided in the request body as
+a link:#contributor-agreement-input[ContributorAgreementInput].
+
+.Request
+----
+  PUT /accounts/self/agreements HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "name": "Individual"
+  }
+----
+
+As response the contributor agreement name is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  "Individual"
+----
+
 [[ids]]
 == IDs
 
@@ -1950,29 +2004,34 @@
 The `AccountInfo` entity contains information about an account.
 
 [options="header",cols="1,^1,5"]
-|=============================
-|Field Name      ||Description
-|`_account_id`   ||The numeric ID of the account.
-|`name`          |optional|The full name of the user. +
+|===============================
+|Field Name        ||Description
+|`_account_id`     ||The numeric ID of the account.
+|`name`            |optional|The full name of the user. +
 Only set if detailed account information is requested. +
 See option link:rest-api-changes.html#detailed-accounts[
 DETAILED_ACCOUNTS] for change queries +
 and option link:#details[DETAILS] for account queries.
-|`email`         |optional|
+|`email`           |optional|
 The email address the user prefers to be contacted through. +
 Only set if detailed account information is requested. +
 See option link:rest-api-changes.html#detailed-accounts[
 DETAILED_ACCOUNTS] for change queries +
-and option link:#details[DETAILS] for account queries.
-|`username`      |optional|The username of the user. +
+and options link:#details[DETAILS] and link:#all-emails[
+ALL_EMAILS] for account queries.
+|`secondary_emails`|optional|
+A list of the secondary email addresses of the user. +
+Only set for account queries when the link:#all-emails[ALL_EMAILS]
+option is set.
+|`username`        |optional|The username of the user. +
 Only set if detailed account information is requested. +
 See option link:rest-api-changes.html#detailed-accounts[
 DETAILED_ACCOUNTS] for change queries +
 and option link:#details[DETAILS] for account queries.
-|`_more_accounts`|optional, not set if `false`|
+|`_more_accounts`  |optional, not set if `false`|
 Whether the query would deliver more results if not limited. +
 Only set on the last account that is returned.
-|=============================
+|===============================
 
 [[account-input]]
 === AccountInput
@@ -2066,6 +2125,31 @@
 link:access-control.html#capability_viewQueue[View Queue] capability.
 |=================================
 
+[[contributor-agreement-info]]
+=== ContributorAgreementInfo
+
+The `ContributorAgreementInfo` entity contains information about a
+contributor agreement.
+
+[options="header",cols="1,6"]
+|=================================
+|Field Name                 |Description
+|`name`                     |The name of the agreement.
+|`description`              |The description of the agreement.
+|`url`                      |The URL of the agreement.
+|=================================
+
+[[contributor-agreement-input]]
+=== ContributorAgreementInput
+The `ContributorAgreementInput` entity contains information about a
+new contributor agreement.
+
+[options="header",cols="1,6"]
+|=================================
+|Field Name                 |Description
+|`name`                     |The name of the agreement.
+|=================================
+
 [[diff-preferences-info]]
 === DiffPreferencesInfo
 The `DiffPreferencesInfo` entity contains information about the diff
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index aad6113..ec1335b 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -192,6 +192,7 @@
 get::/changes/?q=is:open+owner:self&q=is:open+reviewer:self+-owner:self&q=is:closed+owner:self+limit:5&o=LABELS
 ****
 
+[[query-options]]
 Additional fields can be obtained by adding `o` parameters, each
 option requires more database lookups and slows down the query
 response time to the client so they are generally disabled by
@@ -264,6 +265,12 @@
   fields when referencing accounts.
 --
 
+[[reviewer-updates]]
+--
+* `REVIEWER_UPDATES`: include updates to reviewers set as
+  link:#review-update-info[ReviewerUpdateInfo] entities.
+--
+
 [[messages]]
 --
 * `MESSAGES`: include messages associated with the change.
@@ -511,8 +518,8 @@
 --
 
 Retrieves a change with link:#labels[labels], link:#detailed-labels[
-detailed labels], link:#detailed-accounts[detailed accounts], and
-link:#messages[messages].
+detailed labels], link:#detailed-accounts[detailed accounts],
+link:#reviewer-updates[reviewer updates], and link:#messages[messages].
 
 Additional fields can be obtained by adding `o` parameters, each
 option requires more database lookups and slows down the query
@@ -656,6 +663,56 @@
         }
       ]
     },
+    "reviewer_updates": [
+      {
+        "state": "REVIEWER",
+        "reviewer": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com",
+          "username": "jdoe"
+        },
+        "updated_by": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com",
+          "username": "jdoe"
+        },
+        "updated": "2016-07-21 20:12:39.000000000"
+      },
+      {
+        "state": "REMOVED",
+        "reviewer": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com",
+          "username": "jdoe"
+        },
+        "updated_by": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com",
+          "username": "jdoe"
+        },
+        "updated": "2016-07-21 20:12:33.000000000"
+      },
+      {
+        "state": "CC",
+        "reviewer": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com",
+          "username": "jdoe"
+        },
+        "updated_by": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com",
+          "username": "jdoe"
+        },
+        "updated": "2016-03-23 21:34:02.419000000",
+      },
+    ],
     "messages": [
       {
         "id": "YH-egE",
@@ -753,9 +810,6 @@
 
 Deletes the topic of a change.
 
-The request body does not need to include a link:#topic-input[
-TopicInput] entity if no review comment is added.
-
 Please note that some proxies prohibit request bodies for DELETE
 requests. In this case, if you want to specify a commit message, use
 link:#set-topic[PUT] to delete the topic.
@@ -2212,13 +2266,15 @@
         "_account_id": 1000097,
         "name": "Jane Roe",
         "email": "jane.roe@example.com"
-      }
+      },
+      "count": 1
     },
     {
       "group": {
         "id": "4fd581c0657268f2bdcc26699fbf9ddb76e3a279",
         "name": "Joiner"
-      }
+      },
+      "count": 5
     }
   ]
 ----
@@ -2291,6 +2347,7 @@
   {
     "reviewers": [
       {
+        "input": "john.doe@example.com",
         "approvals": {
           "Verified": " 0",
           "Code-Review": " 0"
@@ -2328,6 +2385,7 @@
 
   )]}'
   {
+    "input": "MyProjectVerifiers",
     "error": "The group My Group has 15 members. Do you want to add them all as reviewers?",
     "confirm": true
   }
@@ -2342,7 +2400,7 @@
   Content-Type: application/json; charset=UTF-8
 
   {
-    "reviewer": "MyProjectVerifiers",
+    "input": "MyProjectVerifiers",
     "confirmed": true
   }
 ----
@@ -2375,7 +2433,7 @@
 
 .Request
 ----
-  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/ HTTP/1.0
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/votes/ HTTP/1.0
 ----
 
 As result a map is returned that maps the label name to the label value.
@@ -2818,6 +2876,92 @@
 A review cannot be set on a change edit. Trying to post a review for a
 change edit fails with `409 Conflict`.
 
+It is also possible to add one or more reviewers to a change simultaneously
+with a review.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/review HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "message": "Looks good to me, but Jane and John should also take a look.",
+    "labels": {
+      "Code-Review": 1
+    },
+    "reviewers": [
+      {
+        "reviewer": "jane.roe@example.com"
+      },
+      {
+        "reviewer": "john.doe@example.com"
+      }
+    ]
+  }
+----
+
+Each element of the `reviewers` list is an instance of
+link:#reviewer-input[ReviewerInput]. The corresponding result of
+adding each reviewer will be returned in a list of
+link:#add-reviewer-result[AddReviewerResult].
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "labels": {
+      "Code-Review": 1
+    },
+    "reviewers": [
+      {
+        "input": "jane.roe@example.com",
+        "approvals": {
+          "Verified": " 0",
+          "Code-Review": " 0"
+        },
+        "_account_id": 1000097,
+        "name": "Jane Roe",
+        "email": "jane.roe@example.com"
+      },
+      {
+        "input": "john.doe@example.com",
+        "approvals": {
+          "Verified": " 0",
+          "Code-Review": " 0"
+        },
+        "_account_id": 1000096,
+        "name": "John Doe",
+        "email": "john.doe@example.com"
+      }
+    ]
+  }
+----
+
+If there are any errors returned for reviewers, the entire review request will
+be rejected with `400 Bad Request`.
+
+.Error Response
+----
+  HTTP/1.1 400 Bad Request
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "reviewers": {
+      "MyProjectVerifiers": {
+        "input": "MyProjectVerifiers",
+        "error": "The group My Group has 15 members. Do you want to add them all as reviewers?",
+        "confirm": true
+      }
+    }
+  }
+----
+
 [[rebase-revision]]
 === Rebase Revision
 --
@@ -3057,7 +3201,8 @@
   )]}'
   {
     submit_type: "MERGE_IF_NECESSARY",
-    mergeable: true,
+    strategy: "recursive",
+    mergeable: true
   }
 ----
 
@@ -3515,6 +3660,11 @@
 in the path name. This is useful to implement suggestion services
 finding a file by partial name.
 
+The integer-valued request parameter `parent` changes the response to return a
+list of the files which are different in this commit compared to the given
+parent commit. This is useful for supporting review of merge commits.  The value
+is the 1-based index of the parent's position in the commit object.
+
 .Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/?reviewed HTTP/1.0
@@ -3760,6 +3910,11 @@
 The `base` parameter can be specified to control the base patch set from which the diff should
 be generated.
 
+The integer-valued request parameter `parent` can be specified to control the
+parent commit number against which the diff should be generated.  This is useful
+for supporting review of merge commits.  The value is the 1-based index of the
+parent's position in the commit object.
+
 [[weblinks-only]]
 If the `weblinks-only` parameter is specified, only the diff web links are returned.
 
@@ -4015,6 +4170,11 @@
 |`message`     |optional|
 Message to be added as review comment to the change when abandoning the
 change.
+|`notify`      |optional|
+Notify handling that defines to whom email notifications should be sent after
+the change is abandoned. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `ALL`.
 |===========================
 
 [[action-info]]
@@ -4051,9 +4211,17 @@
 [options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
+|`input`    ||
+Value of the `reviewer` field from link:#reviewer-input[ReviewerInput]
+set while adding the reviewer.
 |`reviewers`   |optional|
 The newly added reviewers as a list of link:#reviewer-info[
 ReviewerInfo] entities.
+|`ccs`         |optional|
+The newly CCed accounts as a list of link:#reviewer-info[
+ReviewerInfo] entities. This field will only appear if the requested
+`state` for the reviewer was `CC` *and* NoteDb is enabled on the
+server.
 |`error`       |optional|
 Error message explaining why the reviewer could not be added. +
 If a group was specified in the input and an error is returned, it
@@ -4205,6 +4373,11 @@
 `REMOVED`: Users that were previously reviewers on the change, but have
 been removed. +
 Only set if link:#detailed-labels[detailed labels] are requested.
+|`reviewer_updates`|optional|
+Updates to reviewers set for the change as
+link:#review-update-info[ReviewerUpdateInfo] entities.
+Only set if link:#reviewer-updates[reviewer updates] are requested and
+if NoteDb is enabled.
 |`messages`|optional|
 Messages associated with the change as a list of
 link:#change-message-info[ChangeMessageInfo] entities. +
@@ -4248,6 +4421,8 @@
 change operation.
 |`new_branch`         |optional, default to `false`|
 Allow creating a new branch when set to `true`.
+|`merge`              |optional|
+The detail of a merge commit as a link:#merge-input[MergeInput] entity.
 |==================================
 
 [[change-message-info]]
@@ -4304,6 +4479,9 @@
 The side on which the comment was added. +
 Allowed values are `REVISION` and `PARENT`. +
 If not set, the default is `REVISION`.
+|`parent`      |optional|
+The 1-based parent number. Used only for merge commits when `side == PARENT`.
+When not set the comment is for the auto-merge tree.
 |`line`        |optional|
 The number of the line for which the comment was done. +
 If range is set, this equals the end line of the range. +
@@ -4733,12 +4911,37 @@
 Submit type used for this change, can be `MERGE_IF_NECESSARY`,
 `FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
 `CHERRY_PICK`.
+|`strategy`     |optional|
+The strategy of the merge, can be `recursive`, `resolve`,
+`simple-two-way-in-core`, `ours` or `theirs`.
 |`mergeable`     ||
 `true` if this change is cleanly mergeable, `false` otherwise
+|`commit_merged`     |optional|
+`true` if this change is already merged, `false` otherwise
+|`content_merged`     |optional|
+`true` if the content of this change is already merged, `false` otherwise
+|`conflicts`|optional|
+A list of paths with conflicts
 |`mergeable_into`|optional|
 A list of other branch names where this change could merge cleanly
 |============================
 
+[[merge-input]]
+=== MergeInput
+The `MergeInput` entity contains information about the merge
+
+[options="header",cols="1,^1,5"]
+|============================
+|Field Name      ||Description
+|`source`   ||
+The source to merge from, e.g. a complete or abbreviated commit SHA-1,
+a complete reference name, a short reference name under refs/heads, refs/tags,
+or refs/remotes namespace, etc.
+|`strategy`     |optional|
+The strategy of the merge, can be `recursive`, `resolve`,
+`simple-two-way-in-core`, `ours` or `theirs`, default will use project settings.
+|============================
+
 [[move-input]]
 === MoveInput
 The `MoveInput` entity contains information for moving a change to a new branch.
@@ -4882,6 +5085,26 @@
 voting values.
 |===========================
 
+[[review-update-info]]
+=== ReviewerUpdateInfo
+The `ReviewerUpdateInfo` entity contains information about updates to
+change's reviewers set.
+
+[options="header",cols="1,6"]
+|===========================
+|Field Name     |Description
+|`updated`|
+Timestamp of the update.
+|`updated_by`|
+The account which modified state of the reviewer in question as
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`reviewer`|
+The reviewer account added or removed from the change as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`state`|
+The reviewer state, one of `REVIEWER`, `CC` or `REMOVED`.
+|===========================
+
 [[review-input]]
 === ReviewInput
 The `ReviewInput` entity contains information for adding a review to a
@@ -4963,6 +5186,9 @@
 ID] of one group for which all members should be added as reviewers. +
 If an ID identifies both an account and a group, only the account is
 added as reviewer to the change.
+|`state`       |optional|
+Add reviewer in this state. Possible reviewer states are `REVIEWER`
+and `CC`. If not given, defaults to `REVIEWER`.
 |`confirmed`   |optional|
 Whether adding the reviewer is confirmed. +
 The Gerrit server may be configured to
@@ -5146,6 +5372,25 @@
 the `group` field that contains the
 link:rest-api-changes.html#group-base-info[GroupBaseInfo] entity.
 
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`account`     |optional|
+An link:rest-api-accounts.html#account-info[AccountInfo] entity, if the
+suggestion is an account.
+|`group`       |optional|
+A link:rest-api-changes.html#group-base-info[GroupBaseInfo] entity, if the
+suggestion is a group.
+|`count`       ||
+The total number of accounts in the suggestion. This is `1` if `account` is
+present. If `group` is present, the total number of accounts that are
+members of the group is returned (this count includes members of nested
+groups).
+|`confirm`     |optional|
+True if `group` is present and `count` is above the threshold where the
+`confirmed` flag must be passed to add the group as a reviewer.
+|===========================
+
 [[topic-input]]
 === TopicInput
 The `TopicInput` entity contains information for setting a topic.
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 8dbf91e..5d08613 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -54,6 +54,14 @@
   {
     "auth": {
       "auth_type": "LDAP",
+      "use_contributor_agreements": true,
+      "contributor_agreements": [
+        {
+          "name": "Individual",
+          "description": "If you are going to be contributing code on your own, this is the one you want. You can sign this one online.",
+          "url": "static/cla_individual.html"
+        }
+      ],
       "editable_account_fields": [
         "FULL_NAME",
         "REGISTER_NEW_EMAIL"
@@ -936,6 +944,155 @@
   ]
 ----
 
+[[get-user-preferences]]
+=== Get Default User Preferences
+--
+'GET /config/server/preferences'
+--
+
+Returns the default user preferences for the server.
+
+.Request
+----
+  GET /a/config/server/preferences HTTP/1.0
+----
+
+As response a link:rest-api-accounts.html#preferences-info[
+PreferencesInfo] is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "changes_per_page": 25,
+    "show_site_header": true,
+    "use_flash_clipboard": true,
+    "download_command": "CHECKOUT",
+    "date_format": "STD",
+    "time_format": "HHMM_12",
+    "diff_view": "SIDE_BY_SIDE",
+    "size_bar_in_change_table": true,
+    "review_category_strategy": "NONE",
+    "mute_common_path_prefixes": true,
+    "my": [
+      {
+        "url": "#/dashboard/self",
+        "name": "Changes"
+      },
+      {
+        "url": "#/q/owner:self+is:draft",
+        "name": "Drafts"
+      },
+      {
+        "url": "#/q/has:draft",
+        "name": "Draft Comments"
+      },
+      {
+        "url": "#/q/has:edit",
+        "name": "Edits"
+      },
+      {
+        "url": "#/q/is:watched+is:open",
+        "name": "Watched Changes"
+      },
+      {
+        "url": "#/q/is:starred",
+        "name": "Starred Changes"
+      },
+      {
+        "url": "#/groups/self",
+        "name": "Groups"
+      }
+    ],
+    "email_strategy": "ENABLED"
+  }
+----
+
+[[set-user-preferences]]
+=== Set Default User Preferences
+
+--
+'PUT /config/server/preferences'
+--
+
+Sets the default user preferences for the server.
+
+The new user preferences must be provided in the request body as a
+link:rest-api-accounts.html#preferences-input[PreferencesInput]
+entity.
+
+To be allowed to set default preferences, a user must be a member of
+a group that is granted the
+link:access-control.html#capability_administrateServer[
+Administrate Server] capability.
+
+.Request
+----
+  PUT /a/config/server/preferences HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "changes_per_page": 50
+  }
+----
+
+As response a link:rest-api-accounts.html#preferences-info[
+PreferencesInfo] is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "changes_per_page": 50,
+    "show_site_header": true,
+    "use_flash_clipboard": true,
+    "download_command": "CHECKOUT",
+    "date_format": "STD",
+    "time_format": "HHMM_12",
+    "diff_view": "SIDE_BY_SIDE",
+    "size_bar_in_change_table": true,
+    "review_category_strategy": "NONE",
+    "mute_common_path_prefixes": true,
+    "my": [
+      {
+        "url": "#/dashboard/self",
+        "name": "Changes"
+      },
+      {
+        "url": "#/q/owner:self+is:draft",
+        "name": "Drafts"
+      },
+      {
+        "url": "#/q/has:draft",
+        "name": "Draft Comments"
+      },
+      {
+        "url": "#/q/has:edit",
+        "name": "Edits"
+      },
+      {
+        "url": "#/q/is:watched+is:open",
+        "name": "Watched Changes"
+      },
+      {
+        "url": "#/q/is:starred",
+        "name": "Starred Changes"
+      },
+      {
+        "url": "#/groups/self",
+        "name": "Groups"
+      }
+    ],
+    "email_strategy": "ENABLED"
+  }
+----
+
 [[get-diff-preferences]]
 === Get Default Diff Preferences
 
@@ -1077,6 +1234,9 @@
 |`use_contributor_agreements` |not set if `false`|
 Whether link:config-gerrit.html#auth.contributorAgreements[contributor
 agreements] are required.
+|`contributor_agreements`     |not set if `use_contributor_agreements` is `false`|
+List of contributor agreements as link:rest-api-accounts.html#contributor-agreement-info[
+ContributorAgreementInfo] entities.
 |`editable_account_fields`    ||
 List of account fields that are editable. Possible values are
 `FULL_NAME`, `USER_NAME` and `REGISTER_NEW_EMAIL`.
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index 336c7ca..23d4c5b 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -1314,10 +1314,12 @@
 |`visible_to_all`|optional|
 Whether the group is visible to all registered users. +
 `false` if not set.
-|`owner_id`|optional|The URL encoded ID of the owner group. +
+|`owner_id`      |optional|The URL encoded ID of the owner group. +
 This can be a group UUID, a legacy numeric group ID or a unique group
 name. +
 If not set, the new group will be self-owned.
+|`members`       |optional|The initial members in a list of +
+link:#account-id[account ids].
 |===========================
 
 [[group-options-info]]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 7d4b92f..2dc203d 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1354,6 +1354,101 @@
   Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
 ----
 
+
+[[get-mergeable-info]]
+=== Get Mergeable Information
+--
+'GET /projects/link:#project-name[\{project-name\}]/branches/link:#branch-id[\{branch-id\}]/mergeable'
+--
+
+Gets whether the source is mergeable with the target branch.
+
+The `source` query parameter is required, which can be anything that could be
+resolved to a commit, see examples of the `source` attribute in
+link:rest-api-changes.html#merge-input[MergeInput].
+
+Also takes an optional parameter `strategy`, which can be `recursive`, `resolve`,
+`simple-two-way-in-core`, `ours` or `theirs`, default will use project settings.
+
+.Request
+----
+  GET /projects/test/branches/master/mergeable?source=testbranch&strategy=recursive HTTP/1.0
+----
+
+As response a link:rest-api-changes.html#mergeable-info[MergeableInfo] entity is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "submit_type": "MERGE_IF_NECESSARY",
+    "strategy": "recursive",
+    "mergeable": true,
+    "commit_merged": false,
+    "content_merged": false
+  }
+----
+
+or when there were conflicts.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "submit_type": "MERGE_IF_NECESSARY",
+    "strategy": "recursive",
+    "mergeable": false,
+    "conflicts": [
+      "common.txt",
+      "shared.txt"
+    ]
+  }
+----
+
+or when the 'testbranch' has been already merged.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "submit_type": "MERGE_IF_NECESSARY",
+    "strategy": "recursive",
+    "mergeable": true,
+    "commit_merged": true,
+    "content_merged": true
+  }
+----
+
+or when only the content of 'testbranch' has been merged.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "submit_type": "MERGE_IF_NECESSARY",
+    "strategy": "recursive",
+    "mergeable": true,
+    "commit_merged": false,
+    "content_merged": true
+  }
+----
+
 [[get-reflog]]
 === Get Reflog
 --
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index a87f5b6..7f7e62e 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -97,6 +97,9 @@
 in the link:http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html[
 HTTP specification].
 
+In most cases, the response body of an error response will be a
+plaintext, human-readable error message.
+
 Here are examples that show how HTTP status codes are used in the
 context of the Gerrit REST API.
 
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index 9dffa51..4dc4880 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -45,24 +45,24 @@
 details on how access permissions work.
 
 Initialize a temporary Git repository to edit the configuration:
-====
+----
   mkdir cfg_dir
   cd cfg_dir
   git init
-====
+----
 
 Download the existing configuration from Gerrit:
-====
+----
   git fetch ssh://localhost:29418/project refs/meta/config
   git checkout FETCH_HEAD
-====
+----
 
 Enable notifications to an email address by adding to
 `project.config`, this can be done using the `git config` command:
-====
+----
   git config -f project.config --add notify.team.email team-address@example.com
   git config -f project.config --add notify.team.email paranoid-manager@example.com
-====
+----
 
 Examining the project.config file with any text editor should show
 a new notify section describing the email addresses to deliver to:
@@ -79,10 +79,10 @@
 if different filters are needed.
 
 Commit the configuration change, and push it back:
-====
+----
   git commit -a -m "Notify team-address@example.com of changes"
   git push ssh://localhost:29418/project HEAD:refs/meta/config
-====
+----
 
 [[notify.name.email]]notify.<name>.email::
 +
@@ -132,11 +132,11 @@
 security filtering by adding the `visibleto:groupname` predicate to
 the filter expression, for example:
 
-====
+----
   [notify "Developers"]
   	email = team-address@example.com
   	filter = visibleto:Developers
-====
+----
 
 When sending email to an internal group, the internal group's read
 access is automatically checked by Gerrit and therefore does not
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index af2b344..7b5f0a6 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -46,9 +46,9 @@
 With this feature, one could attach 'sub' inside of 'super' repository
 at path 'sub' by executing the following command when being inside
 'super':
-====
+----
 git submodule add ssh://server/sub sub
-====
+----
 
 Still considering the above example, after its execution notice that
 inside the local repository 'super' the 'sub' folder is considered a
@@ -56,9 +56,9 @@
 .gitmodules is created (it is a configuration file containing the
 subscription of 'sub'). To provide the SHA-1 each gitlink points to in
 the external repository, one should use the command:
-====
+----
 git submodule status
-====
+----
 
 In the example provided, if 'sub' is updated and 'super' is supposed
 to see the latest SHA-1 (considering here 'sub' has only the master
@@ -78,34 +78,34 @@
 the submodule needs to be configured to enable the superproject subscription.
 In a submodule client, checkout the refs/meta/config branch and edit
 the subscribe capabilities in the 'project.config' file:
-====
+----
     git fetch <remote> refs/meta/config:refs/meta/config
     git checkout refs/meta/config
     $EDITOR project.config
-====
+----
 and add the following lines:
-====
+----
   [allowSuperproject "<superproject>"]
-    refs = <refspec>
-====
+    matching = <refspec>
+----
 where the 'superproject' should be the exact project name of the superproject.
 The refspec defines which branches of the submodule are allowed to be
 subscribed to which branches of the superproject. See below for
 link:#acl_refspec[details]. Push the configuration for review and
 submit the change:
-====
+----
   git add project.config
   git commit -m "Allow <superproject> to subscribe"
   git push <remote> HEAD:refs/for/refs/meta/config
-====
+----
 After the change is integrated a superproject subscription is possible.
 
 The configuration is inherited from parent projects, such that you can have
 a configuration in the "All-Projects" project like:
-====
+----
     [allowSuperproject "my-only-superproject"]
-        refs = refs/heads/*:refs/heads/*
-====
+        matching = refs/heads/*:refs/heads/*
+----
 and then you don't have to worry about configuring the individual projects
 any more. Child projects cannot negate the parent's configuration.
 
@@ -147,30 +147,43 @@
 
 [[acl_refspec]]
 === The RefSpec in the allowSuperproject section
-The RefSpec for defining the branch level access for subscriptions look similar
-to Git style RefSpecs used for pushing in Git. Regular expressions
-as found in the ACL configuration are not supported. The most restrictive
-RefSpec is allowing one specific branch of the submodule to be subscribed
-to one specific branch of the superproject via:
-====
+There are two options for specifying which branches can be subscribed
+to. The most common is to set `allowSuperproject.<superproject>.matching`
+to a Git-style refspec, which has the same syntax as the refspecs used
+for pushing in Git. Regular expressions as found in the ACL configuration
+are not supported.
+
+The most restrictive refspec is allowing one specific branch of the
+submodule to be subscribed to one specific branch of the superproject:
+----
   [allowSuperproject "<superproject>"]
-    refs = refs/heads/<submodule-branch>:refs/heads/<superproject-branch>
-====
+    matching = refs/heads/<submodule-branch>:refs/heads/<superproject-branch>
+----
 
 If you want to allow for a 1:1 mapping, i.e. 'master' maps to 'master',
 'stable' maps to 'stable', but not allowing 'master' to be subscribed to
 'stable':
-====
+----
   [allowSuperproject "<superproject>"]
-    refs = refs/heads/*:refs/heads/*
-====
+    matching = refs/heads/*:refs/heads/*
+----
 
-If you want to enable a branch to be subscribed to any other branch of
-the superproject, omit the second part of the RefSpec:
-====
+To allow all refs matching one pattern to subscribe to all refs
+matching another pattern, set `allowSuperproject.<superproject>.all`
+to the patterns concatenated with a colon. For example, to make a
+single branch available for subscription from all branches of the
+superproject:
+----
   [allowSuperproject "<superproject>"]
-    refs = refs/heads/<submodule-branch>
-====
+     all = refs/heads/<submodule-branch>:refs/heads/*
+----
+
+To make all branches available for subscription from all branches of
+the superproject:
+----
+  [allowSuperproject "<superproject>"]
+     all = refs/heads/*:refs/heads/*
+----
 
 === Subscription Limitations
 
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 9295b32..676826d 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -54,16 +54,16 @@
 If you don't have any keys yet, you can create a new one and protect
 it with a passphrase:
 
-====
+----
   ssh-keygen -t rsa
-====
+----
 
 Then copy the content of the public key file onto your clipboard,
 and paste it into Gerrit's web interface:
 
-====
+----
   cat ~/.ssh/id_rsa.pub
-====
+----
 
 [TIP]
 Users who frequently upload changes will also want to consider
@@ -80,8 +80,7 @@
 to connect to Gerrit's SSHD port.  By default Gerrit runs on
 port 29418, using the same hostname as the web server:
 
-====
-..................................................................
+----
   $ ssh -p 29418 sshusername@hostname
 
     ****    Welcome to Gerrit Code Review    ****
@@ -94,8 +93,7 @@
     git clone ssh://sshusername@hostname:29418/REPOSITORY_NAME.git
 
   Connection to hostname closed.
-..................................................................
-====
+----
 
 In the command above, `sshusername` was configured as `Username` on
 the `Profile` tab of the `Settings` screen.  If it is not set,
@@ -105,10 +103,10 @@
 information URL `http://'hostname'/ssh_info`, and copy the port
 number from the second field:
 
-====
+----
   $ curl http://hostname/ssh_info
   hostname 29418
-====
+----
 
 If you are developing an automated tool to perform uploads to Gerrit,
 let the user supply the hostname or the web address for Gerrit,
@@ -125,17 +123,17 @@
 To create new changes for review, simply push to the project's
 magical `refs/for/'branch'` ref using any Git client tool:
 
-====
+----
   git push ssh://sshusername@hostname:29418/projectname HEAD:refs/for/branch
-====
+----
 
 E.g. `john.doe` can use git push to upload new changes for the
 `experimental` branch of project `kernel/common`, hosted at the
 `git.example.com` Gerrit server:
 
-====
+----
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental
-====
+----
 
 Each new commit uploaded by the `git push` client will be
 converted into a change record on the server.  The remote ref
@@ -163,9 +161,9 @@
 
 By default all email notifications are sent.
 
-====
+----
   git push ssh://bot@git.example.com:29418/kernel/common HEAD:refs/for/master%notify=NONE
-====
+----
 
 [[topic]]
 To include a short tag associated with all of the changes in the
@@ -174,38 +172,36 @@
 'driver/i42' will be saved on each change this push creates or
 updates:
 
-====
+----
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%topic=driver/i42
-====
+----
 
 [[message]]
 A comment message can be applied to the change by using the `message` (or `m`)
 option:
 
-====
+----
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%m=This_is_a_rebase_on_master
-====
+----
 
-.Note
-****
+[NOTE]
 git push refs parameter does not allow spaces.  Use the '_' character instead,
 it will then be applied as "This is a rebase on master".
-****
 
 [[review_labels]]
 Review labels can be applied to the change by using the `label` (or `l`)
 option in the reference:
 
-====
+----
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%l=Verified+1
-====
+----
 
 The `l='label[score]'` option may be specified more than once to
 apply multiple review labels.
 
-====
+----
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%l=Code-Review+1,l=Verified+1
-====
+----
 
 The value is optional.  If not specified, it defaults to +1 (if
 the label range allows it).
@@ -214,9 +210,9 @@
 A change edit can be pushed by specifying the `edit` (or `e`) option on
 the reference:
 
-====
+----
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%edit
-====
+----
 
 There is at most one change edit per user and change. In order to push
 a change edit the change must already exist.
@@ -232,7 +228,7 @@
 your username, hostname and port number.  This permits the use of
 shorter URLs on the command line, such as:
 
-====
+----
   $ cat ~/.ssh/config
   ...
   Host tr
@@ -241,15 +237,15 @@
     User john.doe
 
   $ git push tr:kernel/common HEAD:refs/for/experimental
-====
+----
 
 Specific reviewers can be requested and/or additional 'carbon
 copies' of the notification message may be sent by including the
 `reviewer` (or `r`) and `cc` options in the reference:
 
-====
+----
   git push tr:kernel/common HEAD:refs/for/experimental%r=a@a.com,cc=b@o.com
-====
+----
 
 The `r='email'` and `cc='email'` options may be specified as many
 times as necessary to cover all interested parties. Gerrit will
@@ -261,7 +257,7 @@
 branches, consider adding a custom remote block to your project's
 `.git/config` file:
 
-====
+----
   $ cat .git/config
   ...
   [remote "exp"]
@@ -269,7 +265,7 @@
     push = HEAD:refs/for/experimental%r=a@a.com,cc=b@o.com
 
   $ git push exp
-====
+----
 
 
 [[push_replace]]
@@ -293,8 +289,8 @@
 [[manual_replacement_mapping]]
 ==== Manual Replacement Mapping
 
-.Note
-****
+[NOTE]
+--
 The remainder of this section describes a manual method of replacing
 changes by matching each commit name to an existing change number.
 End-users should instead prefer to use Change-Id lines in their
@@ -302,7 +298,7 @@
 during normal uploads.
 
 See above for the preferred technique of replacing changes.
-****
+--
 
 To add an additional patch set to a change, replacing it with an
 updated version of the same logical modification, send the new
@@ -310,9 +306,9 @@
 SHA-1 starts with `c0ffee` as a new patch set for change number
 `1979`, use the push refspec `c0ffee:refs/changes/1979` as below:
 
-====
+----
   git push ssh://sshusername@hostname:29418/projectname c0ffee:refs/changes/1979
-====
+----
 
 This form can be combined together with `refs/for/'branchname'`
 (above) to simultaneously create new changes and replace changes
@@ -320,7 +316,7 @@
 
 For example, consider the following sequence of events:
 
-====
+----
   $ git commit -m A                    ; # create 3 commits
   $ git commit -m B
   $ git commit -m C
@@ -337,7 +333,7 @@
       HEAD~3:refs/changes/1500
       HEAD~1:refs/changes/1501
       HEAD~0:refs/changes/1502         ; # upload replacements
-====
+----
 
 At the final step during the push Gerrit will attach A' as a new
 patch set on change 1500; B' as a new patch set on change 1501; C'
@@ -404,9 +400,9 @@
 submit strategies to handle contention on busy branches.  Using
 `%submit` creates a change and submits it immediately:
 
-====
+----
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%submit
-====
+----
 
 On auto-merge of a change neither labels nor submit rules are checked.
 If the merge fails the change stays open, but when pushing a new patch
@@ -426,18 +422,18 @@
 may override that behavior and force new changes to be created
 by setting the merge base SHA-1 using the '%base' argument:
 
-====
+----
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%base=$(git rev-parse origin/master)
-====
+----
 
 It is also possible to specify more than one '%base' argument.
 This may be useful when pushing a merge commit. Note that the '%'
 character has only to be provided once, for the first '%base'
 argument:
 
-====
+----
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%base=commit-id1,base=commit-id2
-====
+----
 
 
 == repo upload
diff --git a/ReleaseNotes/ReleaseNotes-2.12.3.txt b/ReleaseNotes/ReleaseNotes-2.12.3.txt
new file mode 100644
index 0000000..f51d739
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.12.3.txt
@@ -0,0 +1,113 @@
+= Release notes for Gerrit 2.12.3
+
+Gerrit 2.12.3 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.3.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.12.3.war]
+
+Gerrit 2.12.3 includes the bug fixes done with
+link:ReleaseNotes-2.11.8.html[Gerrit 2.11.8] and
+link:ReleaseNotes-2.11.9.html[Gerrit 2.11.9]. These bug fixes are *not*
+listed in these release notes.
+
+== Schema Upgrade
+
+*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.2.html[
+2.12.2] but a manual schema upgrade is necessary when upgrading from 2.12.
+
+When upgrading a site that is already running version 2.12, the `patch_sets`
+table must be manually migrated using the `gerrit gsql` SSH command or the
+`gqsl` site program.
+
+For the default H2 database, execute the command:
+
+----
+  alter table patch_sets modify push_certficate clob;
+----
+
+For MySQL, execute the command:
+
+----
+  alter table patch_sets modify push_certficate text;
+----
+
+For PostgreSQL, execute the command:
+
+----
+  alter table patch_sets alter column push_certficate type text;
+----
+
+For other database types, execute the appropriate equivalent command.
+
+Note that the misspelled `push_certficate` is the actual name of the
+column.
+
+When upgrading from a version earlier than 2.12, or from 2.12.1 or 2.12.2
+having already done the migration, this manual step is not necessary and
+should be omitted.
+
+
+== Bug Fixes
+
+* Fix SSL security issue in the SMTP email relay.
++
+The hostname of the SSL socket was not verified. This made the read
+from the socket insecure since without verifying the hostname it may
+be link:https://www.cs.utexas.edu/~shmat/shmat_ccs12.pdf[vulnerable
+to a man-in-the-middle attack].
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3895[Issue 3895]:
+Fix failure to submit with 'Rebase if Necessary' after changes were reordered
+with interactive rebase.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4052[Issue 4052]:
+Fix failure to start server after upgrade from version 2.9.4.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3891[Issue 3891]:
+Fix query with `label:` operator and zero value.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4112[Issue 4112]:
+Fix failure to submit changes caused by empty user edit ref.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4087[Issue 4087]:
+Fix failure to submit change when a branch is created on the change ref.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4155[Issue 4155]:
+Fix tags REST API to correctly return all tags.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4154[Issue 4154]:
+Add support for `.team` and several more TLDs in email address validation.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4163[Issue 4163]:
+Prevent removal of non-voting reviewers on submit of change.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=2647[Issue 2647]:
+Fix usage of `CTRL-C` on change screen.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4236[Issue 4236]:
+Fix internal error when pushing an amended commit with the `%edit` option.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3426[Issue 3426]:
+Fix pushing changes with `%base` option or `newChangeForAllNotInTarget` option.
+
+* Show 'Submitted Together' tab for changes with same topic.
+
+* Improve submit button tooltip messages shown when change is not submittable.
+
+* Fix firing of the `topic-changed` hook.
+
+* Remove `--dry-run` option from the `Reindex` site program.
++
+The implementation of the option was removed, but the option was mistakenly
+added back to the command and did not actually work.
+
+* Print proper task names in the output of the `show-queues` command.
+
+* Replication plugin: Double check if a ref is missing locally before deleting
+from remote.
+
+* Show an error message when trying to add a non-existent group to an ACL.
+
+== Updates
+
+* Update commons-validator to 1.5.1.
diff --git a/ReleaseNotes/ReleaseNotes-2.12.4.txt b/ReleaseNotes/ReleaseNotes-2.12.4.txt
new file mode 100644
index 0000000..ce8651c
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.12.4.txt
@@ -0,0 +1,104 @@
+= Release notes for Gerrit 2.12.4
+
+Gerrit 2.12.4 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.4.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.12.4.war]
+
+== Schema Upgrade
+
+*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.3.html[
+2.12.3] but a manual schema upgrade is necessary when upgrading from 2.12.
+
+When upgrading a site that is already running version 2.12, the `patch_sets`
+table must be manually migrated using the `gerrit gsql` SSH command or the
+`gqsl` site program.
+
+For the default H2 database, execute the command:
+
+----
+  alter table patch_sets modify push_certficate clob;
+----
+
+For MySQL, execute the command:
+
+----
+  alter table patch_sets modify push_certficate text;
+----
+
+For PostgreSQL, execute the command:
+
+----
+  alter table patch_sets alter column push_certficate type text;
+----
+
+For other database types, execute the appropriate equivalent command.
+
+Note that the misspelled `push_certficate` is the actual name of the
+column.
+
+When upgrading from a version earlier than 2.12, or from 2.12.1 or 2.12.2
+having already done the migration, this manual step is not necessary and
+should be omitted.
+
+== Known Issues
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4323[Issue 4323]:
+'value too long for type character varying(255)' in patch_sets table when
+migrating to schema version 108.
++
+This error may occur under some circumstances when running the schema
+migration from an earlier version of Gerrit.
++
+On sites where this occurs, it can be fixed with a manual schema update
+according to the comments in the issue.
+
+== Bug Fixes
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4249[Issue 4249]:
+Fix 'Duplicate stages not allowed' error during indexing.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4238[Issue 4238]:
+Fix 'not found' error when browsing tree in gitweb.
++
+The `refs/heads/` prefix was incorrectly being added to `HEAD`, causing a
+'404 Not Found' error.
+
+* Allow to read repositories that do not end with `.git`.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4262[Issue 4262]:
+Fix GPG push certificate for first patch set of new changes.
++
+The GPG certificate was not being set for the first patch set of new
+changes.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4296[Issue 4296]:
+Fix internal error when a query does not contain any token.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4241[Issue 4241]:
+Fix 'Cannot format velocity template' error when sending notification emails.
+
+* Fix `sshd.idleTimeout` setting being ignored.
++
+Ths `sshd.idleTimeout` setting was not being correctly set on the SSHD
+backend, causing idle sessions to not time out.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4324[Issue 4324]:
+Set the correct uploader on new patch sets created via the inline editor.
+
+* Log a warning instead of failing when invalid commentlinks are configured.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4136[Issue 4136]:
+Fix support for `HEAD` requests in the REST API.
++
+Sending a `HEAD` request failed with '404 Not Found'.
+
+* Return proper error response when trying to confirm an email that is already
+used by another user.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4318[Issue 4318]
+Fix 'Rebase if Necessary' merge strategy to prevent introducing a duplicate
+commit when submitting a merge commit.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4332[Issue 4332]:
+Allow `local` as a valid TLD for outgoing emails.
diff --git a/ReleaseNotes/ReleaseNotes-2.12.txt b/ReleaseNotes/ReleaseNotes-2.12.txt
index 65a4484..84644e8 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.txt
@@ -79,7 +79,7 @@
 +
 When a client pushes with `git push --signed`, Gerrit ensures that the push
 certificate is valid and signed with a valid public key stored in the
-`refs/gpg-keys` branch of the `All-Users` repository.
+`refs/meta/gpg-keys` branch of the `All-Users` repository.
 
 * When signed push is enabled, and
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#gerrit.editGpgKeys[
diff --git a/ReleaseNotes/ReleaseNotes-2.13.txt b/ReleaseNotes/ReleaseNotes-2.13.txt
index 07ceb4d..8a1e583 100644
--- a/ReleaseNotes/ReleaseNotes-2.13.txt
+++ b/ReleaseNotes/ReleaseNotes-2.13.txt
@@ -9,16 +9,59 @@
 
 == Important Notes
 
-TODO
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* To use online reindexing for `changes` secondary index when upgrading
+to 2.13.x, the server must first be upgraded to 2.8 (or 2.9) and then through
+2.10, 2.11 and 2.12. Skipping a version will prevent online reindexer from
+working.
+
+Since 2.13 introduces a new secondary index for accounts, it must be indexed
+offline before starting Gerrit:
+----
+  java -jar gerrit.war reindex --index accounts -d site_path
+----
+If reindexing will be done offline, you may ignore these warnings and upgrade
+directly to 2.13.x using the following command that will reindex both `changes`
+and `accounts` secondary indexes:
+----
+  java -jar gerrit.war reindex -d site_path
+----
+
+*WARNING:* The server side hooks functionality is moved to a core plugin. Sites
+that make use of server side hooks must install this plugin during site init.
 
 
 == Release Highlights
 
+* Support for Large File Storage (LFS).
+
 * Metrics interface.
 
+* Hooks plugin.
+
+* Secondary index for accounts.
+
+* File annotations (blame) in side-by-side diff.
 
 == New Features
 
+=== Large File Storage (LFS)
+
+Gerrit provides an
+link:https://gerrit-review.googlesource.com/Documentation/dev-plugins.html#lfs-extension[
+extension point] that enables development of plugins implementing the
+link:https://github.com/github/git-lfs/blob/master/docs/api/v1/http-v1-batch.md[
+LFS protocol].
+
+By setting
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#lfs.plugin[
+`lfs.plugin`] the administrator can configure the name of the plugin
+which handles LFS requests.
+
 === Metrics
 
 Metrics about Gerrit's internal state can be sent to external
@@ -45,21 +88,110 @@
 report metrics to different monitoring systems. The following
 plugins are available:
 
-* JMX
+* link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-jmx[
+JMX]
 
-* Graphite
+* link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-graphite[
+Graphite]
 
-* Elasticsearch
+* link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-elasticsearch[
+Elasticsearch]
 
 
 Plugins can also provide metrics.  The following metrics are provided
-by core plugins:
+by the replication plugin:
 
-* Replication
+* Replication latency
 
-** Replication time
+* Replication delay
 
-* TODO add more
+* Replication retry
+
+=== Hooks
+
+Server side hooks are moved to the
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/hooks[
+hooks plugin]. Sites that make use of server side hooks should install this
+plugin. After installing the plugin, no additional configuration is needed.
+The plugin uses the same configuration settings in `gerrit.config`.
+
+=== Secondary Index
+
+* The secondary index now supports indexing of accounts.
++
+The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/pgm-reindex.html[
+reindex program] by default reindexes all changes and accounts. A new
+option allows to explicitly specify whether to reindex changes or accounts.
++
+The `suggest.fullTextSearch`, `suggest.fullTextSearchMaxMatches` and
+`suggest.fullTextSearchRefresh` configuration options are removed. Full text
+search is supported by default with the account secondary index.
+
+* New ssh command to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/cmd-index-changes.html[
+reindex changes].
+
+
+=== UI
+
+* The UI can now be loaded in an iFrame by enabling
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#gerrit.canLoadInIFrame[
+gerrit.canLoadInIFrame] in the site configuration.
+
+==== Change Screen
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=106[Issue 106]:
+Allow to select parent for diff base in change screen.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3035[Issue 3035]:
+Allow to remove specific votes from a change, while leaving the reviewer on the
+change.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3487[Issue 3487]:
+Use 'Ctrl-Alt-e' instead of 'e' to open edit mode.
+
+==== Diff Screens
+
+* Add all syntax highlighting available in CodeMirror.
+
+* Improve search experience in diff screen
++
+Ctrl-F, Ctrl-G and Shift-Ctrl-G now bind to the search dialog box provided by
+CodeMirror's search add-on. Enter and Shift-Enter navigate among the search
+results from the CodeMirror search, just like they do in a normal browser
+search. Esc now clears the search result.
++
+If the user sets `Render` to `Slow` in the diff preferences and the file is less
+than 4000 lines (huge), then Ctrl-F, Ctrl-G and Shift-Ctrl-G fall back to the
+browser search.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=2968[Issue 2968]:
+Allow to go back to change list by keyboard shortcut from diff screens.
+
+==== Side-By-Side Diff Screen
+
+* Blame annotations
++
+By enabling
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#change.allowBlame[
+`change.allowBlame`], blame annotations can be shown in the side-by-side diff
+screen gutter. Clicking the annotation opens the relevant change.
+
+==== User Preferences
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=989[Issue 989]:
+New option to control email notifications.
++
+Users can now choose between 'Enabled', 'Disabled' and 'CC Me on Comments I Write'.
+
+* New option to control adding 'Signed-off-by' footer in commit message of new changes
+created online.
+
+* New option to control auto-indent width in inline editor.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=890[Issue 890]:
+New diff option to control whether to skip unchanged files when navigating to
+the previous or the next file.
 
 === Changes
 
@@ -67,33 +199,207 @@
 batch, submit type rules may not be used to mix submit types on a single branch,
 and trying to submit such a batch will fail.
 
+=== REST API
+
+==== Accounts
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3766[Issue 3766]:
+Allow users with the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/access-control.html#capability_modifyAccount[
+'ModifyAccount' capability] to get the preferences for other users via the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#get-user-preferences[
+Get User Preferences] endpoint.
+
+* Rename 'Suggest Account' to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#query-account[
+'Query Account'] and add support for arbitrary account queries.
++
+The `_more_accounts` flag is set on the last result when there are more results
+than the limit. The `DETAILS` and `ALL_EMAILS` options may be set to control
+whether the results should include details (full name, email, username, avatars)
+and all emails, respectively.
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#get-watched-projects[
+Get Watched Projects].
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#set-watched-projects[
+Set Watched Projects].
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#delete-watched-projects[
+Delete Watched Projects].
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#get-stars[
+Get Star Labels from Change].
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#set-stars[
+Update Star Labels on Change].
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#get-oauth-token[
+Get OAuth Access Token].
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#list-contributor-agreements[
+List Contributor Agreements].
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#sign-contributor-agreement[
+Sign Contributor Agreement].
+
+==== Changes
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3579[Issue 3579]:
+Append submitted info to ChangeInfo.
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-changes.html#move-change[
+Move Change].
+
+==== Groups
+
+* Add `-s` as an alias for `--suggest` on the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-groups.html#suggest-group[
+Suggest Group] endpoint.
+
+==== Projects
+
+* Add `async` option to the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#run-gc[
+Run GC] endpoint to allow garbage collection to run asynchronously.
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#get-access[
+List Access Rights].
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#set-access[
+Add, Update and Delete Access Rights].
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#create-tag[
+Create Tag].
+
+* New endpoint:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#get-mergeable-info[
+Get Mergeable Information].
+
+=== Plugins
+
+Plugins may now store secure settings in `etc/$PLUGIN.secure.config` where they
+will be decoded by the Secure Store implementation.
+
+=== Misc
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4015[Issue 4015]:
+Allow setting a comment message when uploading a change.
+
+* Support ACLs for superproject subscriptions.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3220[Issue 3220]:
+Append approval info to every comment-added stream event and hook.
+
+* The `administrateServer` capability can be assigned to groups by setting
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#capability.administrateServer[
+capability.administrateServer] in the site configuration.
++
+Configuring this option can be a useful fail-safe to recover a server in the
+event an administrator removed all groups from the `administrateServer`
+capability, or to ensure that specific groups always have administration
+capabilities.
+
 == Bug Fixes
 
-TODO
+* Don't add the same SSH key multiple times.
 
+* Make Lucene index more stable when being interrupted.
 
-== Upgrades
+* Don't show the `start` and `idle` columns in the `show-connections`
+output when the ssh backend is NIO2.
++
+The NIO2 backend doesn't provide the start and idle times, and the
+values being displayed were just dummy values. Now these values are
+only displayed for the MINA backend.
 
-* Upgrade CodeMirror to 5.14.2
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4150[Issue 4150]:
+Deleting a draft inline comment no longer causes the change's `Updated` field to
+be bumped.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4099[Issue 4099]:
+Fix SubmitWholeTopic does not update subscriptions.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3603[Issue 3603]:
+Fix editing a submodule via inline edit.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4069[Issue 4069]:
+Fix highlights in scrollbar overview ruler not moved when extending the
+displayed area.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3446[Issue 3446]:
+Respect the `Skip Deleted` diff preference.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3445[Issue 3445]:
+Respect the `Skip Uncommented` diff preference.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4051[Issue 4051]:
+Fix empty `From` email header.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3423[Issue 3423]:
+Fix intraline diff for added spaces.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=1867[Issue 1867]:
+Remove `no changes made` error case when the only difference between a new
+commit and the previous patch set of the change is the committer.
+
+Remove "no changes made" error case
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3831[Issue 3831]:
+Prevent creating groups with the same name as a system group.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3754[Issue 3754]:
+Fix `View All Accounts` permission to allow accounts rest endpoint to access
+email info.
+
+== Dependency updates
+
+* Add dependency on blame-cache 0.1-9
+
+* Add dependency on guava-retrying 2.0.0
+
+* Add dependency on jsr305 3.0.1
+
+* Add dependency on metrics-core 3.1.2
+
+* Upgrade auto-value to 1.3-rc1
+
+* Upgrade commons-net to 3.5
+
+* Upgrade CodeMirror to 5.17.0
 
 * Upgrade Guava to 19.0
 
 * Upgrade Gson to 2.6.2
 
-* Upgrade gwtjsonrpc to version 1.8
+* Upgrade gwtjsonrpc to 1.8
 
 * Upgrade gwtorm to 1.15
 
-* Upgrade javassist.jar to 3.18.1
+* Upgrade javassist to 3.20.0-GA
 
 * Upgrade Jetty to 9.2.14.v20151106
 
-* Upgrade JGit to 4.3.0.201604071810-r
+* Upgrade JGit to 4.4.1.201607150455-r
 
-* Upgrade Lucene to 5.4.1
+* Upgrade joda-convert to 1.8.1
 
-* Upgrade mina to 2.10
+* Upgrade joda-time to 2.9.4
+
+* Upgrade Lucene to 5.5.0
+
+* Upgrade mina to 2.0.10
 
 * Upgrade sshd-core to 1.2.0
 
-* Upgrade Truth to 0.28
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 49491ac..5534713 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -6,6 +6,8 @@
 
 [[s2_12]]
 == Version 2.12.x
+* link:ReleaseNotes-2.12.4.html[2.12.4]
+* link:ReleaseNotes-2.12.3.html[2.12.3]
 * link:ReleaseNotes-2.12.2.html[2.12.2]
 * link:ReleaseNotes-2.12.1.html[2.12.1]
 * link:ReleaseNotes-2.12.html[2.12]
diff --git a/VERSION b/VERSION
index 573f909..3035c93 100644
--- a/VERSION
+++ b/VERSION
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = '2.13-SNAPSHOT'
+GERRIT_VERSION = '2.14-SNAPSHOT'
diff --git a/WORKSPACE b/WORKSPACE
index b5d9eb6..4911f44 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -82,27 +82,27 @@
   sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e',
 )
 
-JGIT_VERS = '4.3.0.201604071810-r.23-gc9b0028'
+JGIT_VERS = '4.4.1.201607150455-r.118-g1096652'
 
 maven_jar(
   name = 'jgit',
   repository = 'http://gerrit-maven.storage.googleapis.com/',
   artifact = 'org.eclipse.jgit:org.eclipse.jgit:' + JGIT_VERS,
-  sha1 = 'dc4464c876cbf3815fd6cf6cb9d29d375566d6b1',
+  sha1 = 'cd142b9030910babd119702f1c4eeae13ee90018',
 )
 
 maven_jar(
   name = 'jgit_servlet',
   repository = 'http://gerrit-maven.storage.googleapis.com/',
   artifact = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + JGIT_VERS,
-  sha1 = 'bb01841b74a48abe506c2e44f238e107188e6c8f',
+  sha1 = 'fa67bf925001cfc663bf98772f37d5c5c1abd756',
 )
 
 # TODO(davido): Remove this hack when maven_jar supports pulling sources
 # https://github.com/bazelbuild/bazel/issues/308
 http_file(
   name = 'jgit_src',
-  sha256 = '881906cb1e6743cb78df6dd3788cab7e974308fbb98cab4915e6591a62aa9374',
+  sha256 = '1a0b2d637359b1b51eba4d094491ef39877a6fc192e2fc1da0422a9adf04f0b8',
   url = 'http://gerrit-maven.storage.googleapis.com/org/eclipse/jgit/org.eclipse.jgit/' +
       '%s/org.eclipse.jgit-%s-sources.jar' % (JGIT_VERS, JGIT_VERS),
 )
@@ -117,14 +117,14 @@
   name = 'jgit_archive',
   repository = 'http://gerrit-maven.storage.googleapis.com/',
   artifact = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + JGIT_VERS,
-  sha1 = 'c612e5bd40ebf6226032cb32c14b396d7ebfe036',
+  sha1 = '3f45cd199e40a7c68ee07a1743c06d1c3d07308a',
 )
 
 maven_jar(
   name = 'jgit_junit',
   repository = 'http://gerrit-maven.storage.googleapis.com/',
   artifact = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + JGIT_VERS,
-  sha1 = '62dddedccdcd67b622d0d35a4bfb15c7eab8e171',
+  sha1 = 'dc7edb9c3060655c7fb93ab9b9349e815bab266f',
 )
 
 maven_jar(
@@ -299,8 +299,8 @@
 
 maven_jar(
   name = 'commons_validator',
-  artifact = 'commons-validator:commons-validator:1.4.1',
-  sha1 = '2231238e391057a53f92bde5bbc588622c1956c3',
+  artifact = 'commons-validator:commons-validator:1.5.1',
+  sha1 = '86d05a46e8f064b300657f751b5a98c62807e2a0',
 )
 
 maven_jar(
diff --git a/gerrit-acceptance-framework/BUCK b/gerrit-acceptance-framework/BUCK
index ba68fa3..7ef71c4 100644
--- a/gerrit-acceptance-framework/BUCK
+++ b/gerrit-acceptance-framework/BUCK
@@ -37,10 +37,20 @@
 
 java_binary(
   name = 'acceptance-framework',
+  merge_manifests = False,
+  manifest_file = ':manifest',
   deps = [':lib'],
   visibility = ['PUBLIC'],
 )
 
+genrule(
+  name = 'manifest',
+  cmd = 'echo "Manifest-Version: 1.0" >$OUT;' +
+    'echo "Implementation-Title: Gerrit Acceptance Test Framework" >>$OUT;' +
+    'echo "Implementation-Vendor: Gerrit Code Review Project" >>$OUT',
+  out = 'manifest.txt',
+)
+
 java_library(
   name = 'lib',
   srcs = SRCS,
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
index b5156c0..d9d701c 100644
--- a/gerrit-acceptance-framework/pom.xml
+++ b/gerrit-acceptance-framework/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
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 488f9f9..ae480c7 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
@@ -18,10 +18,13 @@
 import static com.google.gerrit.acceptance.GitUtil.initSsh;
 import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.eclipse.jgit.lib.Constants.HEAD;
 
 import com.google.common.base.Function;
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Chars;
@@ -39,6 +42,7 @@
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.EditInfo;
@@ -67,6 +71,7 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.mail.EmailHeader;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
@@ -76,6 +81,7 @@
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.FakeEmailSender;
+import com.google.gerrit.testutil.FakeEmailSender.Message;
 import com.google.gerrit.testutil.TempFileUtil;
 import com.google.gerrit.testutil.TestNotesMigration;
 import com.google.gson.Gson;
@@ -257,6 +263,11 @@
   public TemporaryFolder tempSiteDir = new TemporaryFolder();
 
   @Before
+  public void clearSender() {
+    sender.clear();
+  }
+
+  @Before
   public void startEventRecorder() {
     eventRecorder = eventRecorderFactory.create(admin);
   }
@@ -298,6 +309,10 @@
     return cfg.getBoolean("change", null, "submitWholeTopic", false);
   }
 
+  protected boolean isContributorAgreementsEnabled() {
+    return cfg.getBoolean("auth", null, "contributorAgreements", false);
+  }
+
   protected void beforeTest(Description description) throws Exception {
     GerritServer.Description classDesc =
       GerritServer.Description.forTestClass(description, configName);
@@ -405,15 +420,28 @@
   protected Project.NameKey createProject(String nameSuffix,
       Project.NameKey parent) throws RestApiException {
     // Default for createEmptyCommit should match TestProjectConfig.
-    return createProject(nameSuffix, parent, true);
+    return createProject(nameSuffix, parent, true, null);
   }
 
   protected Project.NameKey createProject(String nameSuffix,
-      Project.NameKey parent, boolean createEmptyCommit)
+      Project.NameKey parent, boolean createEmptyCommit) throws RestApiException {
+    // Default for createEmptyCommit should match TestProjectConfig.
+    return createProject(nameSuffix, parent, createEmptyCommit, null);
+  }
+
+  protected Project.NameKey createProject(String nameSuffix,
+      Project.NameKey parent, SubmitType submitType) throws RestApiException {
+    // Default for createEmptyCommit should match TestProjectConfig.
+    return createProject(nameSuffix, parent, true, submitType);
+  }
+
+  protected Project.NameKey createProject(String nameSuffix,
+      Project.NameKey parent, boolean createEmptyCommit, SubmitType submitType)
       throws RestApiException {
     ProjectInput in = new ProjectInput();
     in.name = name(nameSuffix);
     in.parent = parent != null ? parent.get() : null;
+    in.submitType = submitType;
     in.createEmptyCommit = createEmptyCommit;
     return createProject(in);
   }
@@ -490,6 +518,29 @@
     return result;
   }
 
+  protected PushOneCommit.Result createMergeCommitChange(String ref)
+      throws Exception {
+    ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+
+    PushOneCommit.Result p1 = pushFactory.create(db, admin.getIdent(),
+        testRepo, "parent 1", ImmutableMap.of("foo", "foo-1", "bar", "bar-1"))
+        .to(ref);
+
+    // reset HEAD in order to create a sibling of the first change
+    testRepo.reset(initial);
+
+    PushOneCommit.Result p2 = pushFactory.create(db, admin.getIdent(),
+        testRepo, "parent 2", ImmutableMap.of("foo", "foo-2", "bar", "bar-2"))
+        .to(ref);
+
+    PushOneCommit m = pushFactory.create(db, admin.getIdent(), testRepo, "merge",
+        ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+    m.setParents(ImmutableList.of(p1.getCommit(), p2.getCommit()));
+    PushOneCommit.Result result = m.to(ref);
+    result.assertOkStatus();
+    return result;
+  }
+
   protected PushOneCommit.Result createDraftChange() throws Exception {
     return pushTo("refs/drafts/master");
   }
@@ -694,6 +745,12 @@
 
   protected PermissionRule block(String permission, AccountGroup.UUID id, String ref)
       throws Exception {
+    return block(permission, id, ref, project);
+  }
+
+  protected PermissionRule block(String permission,
+      AccountGroup.UUID id, String ref, Project.NameKey project)
+      throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     PermissionRule rule = Util.block(cfg, permission, id, ref);
     saveProjectConfig(project, cfg);
@@ -875,4 +932,12 @@
   protected RevCommit getRemoteHead() throws Exception {
     return getRemoteHead(project, "master");
   }
+
+  protected void assertMailFrom(Message message, String email)
+      throws Exception {
+    assertThat(message.headers()).containsKey("Reply-To");
+    EmailHeader.String replyTo =
+        (EmailHeader.String)message.headers().get("Reply-To");
+    assertThat(replyTo.getString()).isEqualTo(email);
+  }
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index 3632502..bce0b5a 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
+import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -52,6 +53,7 @@
   private final SshKeyCache sshKeyCache;
   private final AccountCache accountCache;
   private final AccountByEmailCache byEmailCache;
+  private final AccountIndexer indexer;
 
   @Inject
   AccountCreator(SchemaFactory<ReviewDb> schema,
@@ -59,7 +61,8 @@
       GroupCache groupCache,
       SshKeyCache sshKeyCache,
       AccountCache accountCache,
-      AccountByEmailCache byEmailCache) {
+      AccountByEmailCache byEmailCache,
+      AccountIndexer indexer) {
     accounts = new HashMap<>();
     reviewDbProvider = schema;
     this.authorizedKeys = authorizedKeys;
@@ -67,6 +70,7 @@
     this.sshKeyCache = sshKeyCache;
     this.accountCache = accountCache;
     this.byEmailCache = byEmailCache;
+    this.indexer = indexer;
   }
 
   public synchronized TestAccount create(String username, String email,
@@ -113,6 +117,8 @@
       accountCache.evictByUsername(username);
       byEmailCache.evict(email);
 
+      indexer.index(id);
+
       account =
           new TestAccount(id, username, email, fullName, sshKey, httpPass);
       accounts.put(username, account);
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
index 872c912..390cae3 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
@@ -51,6 +51,16 @@
     return response.getStatusLine().getStatusCode();
   }
 
+  public String getContentType() {
+    return response.getFirstHeader("X-FYI-Content-Type").getValue();
+  }
+
+  public boolean hasContent() {
+    Preconditions.checkNotNull(response,
+        "Response is not initialized.");
+    return response.getEntity() != null;
+  }
+
   public String getEntityContent() throws IOException {
     Preconditions.checkNotNull(response,
         "Response is not initialized.");
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
index c71e13f..dde1875 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
@@ -48,9 +48,9 @@
   private Path pluginsSitePath;
   private Path pluginSubPath;
   private Path pluginSource;
-  private String pluginName;
   private boolean standalone;
 
+  protected String pluginName;
   protected Path testSite;
 
   @Override
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index 7179e80..c892877 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
@@ -44,6 +45,7 @@
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 
 import java.util.List;
+import java.util.Map;
 
 public class PushOneCommit {
   public static final String SUBJECT = "test commit";
@@ -91,6 +93,13 @@
         ReviewDb db,
         PersonIdent i,
         TestRepository<?> testRepo,
+        @Assisted String subject,
+        @Assisted Map<String, String> files);
+
+    PushOneCommit create(
+        ReviewDb db,
+        PersonIdent i,
+        TestRepository<?> testRepo,
         @Assisted("subject") String subject,
         @Assisted("fileName") String fileName,
         @Assisted("content") String content,
@@ -123,8 +132,7 @@
   private final TestRepository<?> testRepo;
 
   private final String subject;
-  private final String fileName;
-  private final String content;
+  private final Map<String, String> files;
   private String changeId;
   private Tag tag;
   private boolean force;
@@ -175,18 +183,43 @@
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
+      @Assisted String subject,
+      @Assisted Map<String, String> files) throws Exception {
+    this(notesFactory, approvalsUtil, queryProvider, db, i, testRepo,
+        subject, files, null);
+  }
+
+  @AssistedInject
+  PushOneCommit(ChangeNotes.Factory notesFactory,
+      ApprovalsUtil approvalsUtil,
+      Provider<InternalChangeQuery> queryProvider,
+      @Assisted ReviewDb db,
+      @Assisted PersonIdent i,
+      @Assisted TestRepository<?> testRepo,
       @Assisted("subject") String subject,
       @Assisted("fileName") String fileName,
       @Assisted("content") String content,
       @Nullable @Assisted("changeId") String changeId) throws Exception {
+    this(notesFactory, approvalsUtil, queryProvider, db, i, testRepo,
+        subject, ImmutableMap.of(fileName, content), changeId);
+  }
+
+  private PushOneCommit(ChangeNotes.Factory notesFactory,
+      ApprovalsUtil approvalsUtil,
+      Provider<InternalChangeQuery> queryProvider,
+      ReviewDb db,
+      PersonIdent i,
+      TestRepository<?> testRepo,
+      String subject,
+      Map<String, String> files,
+      String changeId) throws Exception {
     this.db = db;
     this.testRepo = testRepo;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.queryProvider = queryProvider;
     this.subject = subject;
-    this.fileName = fileName;
-    this.content = content;
+    this.files = files;
     this.changeId = changeId;
     if (changeId != null) {
       commitBuilder = testRepo.amendRef("HEAD")
@@ -206,13 +239,22 @@
     }
   }
 
+  public void setParent(RevCommit parent) throws Exception {
+    commitBuilder.noParents();
+    commitBuilder.parent(parent);
+  }
+
   public Result to(String ref) throws Exception {
-    commitBuilder.add(fileName, content);
+    for (Map.Entry<String, String> e : files.entrySet()) {
+      commitBuilder.add(e.getKey(), e.getValue());
+    }
     return execute(ref);
   }
 
   public Result rm(String ref) throws Exception {
-    commitBuilder.rm(fileName);
+    for (String fileName : files.keySet()) {
+      commitBuilder.rm(fileName);
+    }
     return execute(ref);
   }
 
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 7991afe2..93525a4 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
@@ -59,7 +59,9 @@
 import com.google.gerrit.gpg.testutil.TestKey;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.account.WatchConfig;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.RefPattern;
@@ -88,7 +90,9 @@
 import java.io.ByteArrayOutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -121,6 +125,8 @@
     db.accountExternalIds().delete(getExternalIds(admin));
     db.accountExternalIds().delete(getExternalIds(user));
     db.accountExternalIds().insert(savedExternalIds);
+    accountCache.evict(admin.getId());
+    accountCache.evict(user.getId());
   }
 
   @After
@@ -135,9 +141,9 @@
     }
   }
 
-  private List<AccountExternalId> getExternalIds(TestAccount account)
+  private Collection<AccountExternalId> getExternalIds(TestAccount account)
       throws Exception {
-    return db.accountExternalIds().byAccount(account.getId()).toList();
+    return accountCache.get(account.getId()).getExternalIds();
   }
 
   @After
@@ -183,9 +189,13 @@
         .accounts()
         .self()
         .get();
-    assertThat(info.name).isEqualTo("Administrator");
-    assertThat(info.email).isEqualTo("admin@example.com");
-    assertThat(info.username).isEqualTo("admin");
+    assertUser(info, admin);
+
+    info = gApi
+        .accounts()
+        .id("self")
+        .get();
+    assertUser(info, admin);
   }
 
   @Test
@@ -325,7 +335,9 @@
         .addReviewer(in);
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
-    assertThat(messages.get(0).rcpt()).containsExactly(user.emailAddress);
+    Message message = messages.get(0);
+    assertThat(message.rcpt()).containsExactly(user.emailAddress);
+    assertMailFrom(message, admin.email);
   }
 
   @Test
@@ -474,6 +486,40 @@
   }
 
   @Test
+  public void pushWatchConfigToUserBranch() throws Exception {
+    // change something in the user preferences to ensure that the user branch
+    // is created
+    GeneralPreferencesInfo input = new GeneralPreferencesInfo();
+    input.changesPerPage =
+        GeneralPreferencesInfo.defaults().changesPerPage + 10;
+    gApi.accounts().self().setPreferences(input);
+
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
+    allUsersRepo.reset("userRef");
+
+    Config wc = new Config();
+    wc.setString(WatchConfig.PROJECT, project.get(), WatchConfig.KEY_NOTIFY,
+        WatchConfig.NotifyValue
+            .create(null, EnumSet.of(NotifyType.ALL_COMMENTS)).toString());
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo,
+        "Add project watch", WatchConfig.WATCH_CONFIG, wc.toText());
+    push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
+
+    String invalidNotifyValue = "]invalid[";
+    wc.setString(WatchConfig.PROJECT, project.get(), WatchConfig.KEY_NOTIFY,
+        invalidNotifyValue);
+    push = pushFactory.create(db, admin.getIdent(), allUsersRepo,
+        "Add invalid project watch", WatchConfig.WATCH_CONFIG, wc.toText());
+    PushOneCommit.Result r = push.to(RefNames.REFS_USERS_SELF);
+    r.assertErrorStatus("invalid watch configuration");
+    r.assertMessage(String.format(
+        "%s: Invalid project watch of account %d for project %s: %s",
+        WatchConfig.WATCH_CONFIG, admin.getId().get(), project.get(),
+        invalidNotifyValue));
+  }
+
+  @Test
   public void addGpgKey() throws Exception {
     TestKey key = validKeyWithoutExpiration();
     String id = key.getKeyIdString();
@@ -513,6 +559,7 @@
         user.getId(), new AccountExternalId.Key("foo:myId"));
 
     db.accountExternalIds().insert(Collections.singleton(extId));
+    accountCache.evict(user.getId());
 
     TestKey key = validKeyWithSecondUserId();
     addGpgKey(key.getPublicKeyArmored());
@@ -749,4 +796,11 @@
         ImmutableList.of(armored),
         ImmutableList.<String> of());
   }
+
+  private void assertUser(AccountInfo info, TestAccount account)
+      throws Exception {
+    assertThat(info.name).isEqualTo(account.fullName);
+    assertThat(info.email).isEqualTo(account.email);
+    assertThat(info.username).isEqualTo(account.username);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
new file mode 100644
index 0000000..e6049d5
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -0,0 +1,154 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.accounts;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.extensions.api.groups.GroupApi;
+import com.google.gerrit.extensions.common.AgreementInfo;
+import com.google.gerrit.extensions.common.ServerInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.testutil.ConfigSuite;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+public class AgreementsIT extends AbstractDaemonTest {
+  private ContributorAgreement ca;
+  private ContributorAgreement ca2;
+
+  @ConfigSuite.Config
+  public static Config enableAgreementsConfig() {
+    Config cfg = new Config();
+    cfg.setBoolean("auth", null, "contributorAgreements", true);
+    return cfg;
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    String g = createGroup("cla-test-group");
+    GroupApi groupApi = gApi.groups().id(g);
+    groupApi.description("CLA test group");
+    AccountGroup caGroup = groupCache.get(
+        new AccountGroup.UUID(groupApi.detail().id));
+    GroupReference groupRef = GroupReference.forGroup(caGroup);
+    PermissionRule rule = new PermissionRule(groupRef);
+    rule.setAction(PermissionRule.Action.ALLOW);
+    ca = new ContributorAgreement("cla-test");
+    ca.setDescription("description");
+    ca.setAgreementUrl("agreement-url");
+    ca.setAutoVerify(groupRef);
+    ca.setAccepted(ImmutableList.of(rule));
+
+    ca2 = new ContributorAgreement("cla-test-no-auto-verify");
+    ca2.setDescription("description");
+    ca2.setAgreementUrl("agreement-url");
+
+    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    cfg.replace(ca);
+    cfg.replace(ca2);
+    saveProjectConfig(allProjects, cfg);
+    setApiUser(user);
+  }
+
+  @Test
+  public void getAvailableAgreements() throws Exception {
+    ServerInfo info = gApi.config().server().getInfo();
+    if (isContributorAgreementsEnabled()) {
+      assertThat(info.auth.useContributorAgreements).isTrue();
+      assertThat(info.auth.contributorAgreements).hasSize(2);
+      AgreementInfo agreementInfo = info.auth.contributorAgreements.get(0);
+      assertThat(agreementInfo.name).isEqualTo(ca.getName());
+      assertThat(agreementInfo.autoVerifyGroup.name)
+          .isEqualTo(ca.getAutoVerify().getName());
+      agreementInfo = info.auth.contributorAgreements.get(1);
+      assertThat(agreementInfo.name).isEqualTo(ca2.getName());
+      assertThat(agreementInfo.autoVerifyGroup).isNull();
+    } else {
+      assertThat(info.auth.useContributorAgreements).isNull();
+      assertThat(info.auth.contributorAgreements).isNull();
+    }
+  }
+
+  @Test
+  public void signNonExistingAgreement() throws Exception {
+    assume().that(isContributorAgreementsEnabled()).isTrue();
+    exception.expect(UnprocessableEntityException.class);
+    exception.expectMessage("contributor agreement not found");
+    gApi.accounts().self().signAgreement("does-not-exist");
+  }
+
+  @Test
+  public void signAgreementNoAutoVerify() throws Exception {
+    assume().that(isContributorAgreementsEnabled()).isTrue();
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("cannot enter a non-autoVerify agreement");
+    gApi.accounts().self().signAgreement(ca2.getName());
+  }
+
+  @Test
+  public void signAgreement() throws Exception {
+    assume().that(isContributorAgreementsEnabled()).isTrue();
+
+    // List of agreements is initially empty
+    List<AgreementInfo> result = gApi.accounts().self().listAgreements();
+    assertThat(result).isEmpty();
+
+    // Sign the agreement
+    gApi.accounts().self().signAgreement(ca.getName());
+    result = gApi.accounts().self().listAgreements();
+    assertThat(result).hasSize(1);
+    AgreementInfo info = result.get(0);
+    assertThat(info.name).isEqualTo(ca.getName());
+    assertThat(info.description).isEqualTo(ca.getDescription());
+    assertThat(info.url).isEqualTo(ca.getAgreementUrl());
+    assertThat(info.autoVerifyGroup.name)
+        .isEqualTo(ca.getAutoVerify().getName());
+
+    // Signing the same agreement again has no effect
+    gApi.accounts().self().signAgreement(ca.getName());
+    result = gApi.accounts().self().listAgreements();
+    assertThat(result).hasSize(1);
+  }
+
+  @Test
+  public void agreementsDisabledSign() throws Exception {
+    assume().that(isContributorAgreementsEnabled()).isFalse();
+    exception.expect(MethodNotAllowedException.class);
+    exception.expectMessage("contributor agreements disabled");
+    gApi.accounts().self().signAgreement(ca.getName());
+  }
+
+  @Test
+  public void agreementsDisabledList() throws Exception {
+    assume().that(isContributorAgreementsEnabled()).isFalse();
+    exception.expect(MethodNotAllowedException.class);
+    exception.expectMessage("contributor agreements disabled");
+    gApi.accounts().self().listAgreements();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index 6d8ae5c..f45bfbbe 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -28,7 +28,13 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.ReviewCategoryStrategy;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.TimeFormat;
 import com.google.gerrit.extensions.client.MenuItem;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -37,6 +43,9 @@
 
 @NoHttpd
 public class GeneralPreferencesIT extends AbstractDaemonTest {
+  @Inject
+  private AllUsersName allUsers;
+
   private TestAccount user42;
 
   @Before
@@ -45,6 +54,21 @@
     user42 = accounts.create(name, name + "@example.com", "User 42");
   }
 
+  @After
+  public void cleanUp() throws Exception {
+    gApi.accounts().id(user42.getId().toString())
+        .setPreferences(GeneralPreferencesInfo.defaults());
+
+    try (Repository git = repoManager.openRepository(allUsers)) {
+      if (git.exactRef(RefNames.REFS_USERS_DEFAULT) != null) {
+        RefUpdate u = git.updateRef(RefNames.REFS_USERS_DEFAULT);
+        u.setForceUpdate(true);
+        assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
+      }
+    }
+    accountCache.evictAll();
+  }
+
   @Test
   public void getAndSetPreferences() throws Exception {
     GeneralPreferencesInfo o = gApi.accounts()
@@ -81,4 +105,23 @@
     assertPrefs(o, i, "my");
     assertThat(o.my).hasSize(1);
   }
+
+  @Test
+  public void getPreferencesWithConfiguredDefaults() throws Exception {
+    GeneralPreferencesInfo d = GeneralPreferencesInfo.defaults();
+    int newChangesPerPage = d.changesPerPage * 2;
+    GeneralPreferencesInfo update = new GeneralPreferencesInfo();
+    update.changesPerPage = newChangesPerPage;
+    gApi.config().server().setDefaultPreferences(update);
+
+    GeneralPreferencesInfo o = gApi.accounts()
+        .id(user42.getId().toString())
+        .getPreferences();
+
+    // assert configured defaults
+    assertThat(o.changesPerPage).isEqualTo(newChangesPerPage);
+
+    // assert hard-coded defaults
+    assertPrefs(o, d, "my", "changesPerPage");
+  }
 }
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 b12b9be..db9bb09 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
@@ -28,23 +28,27 @@
 import static com.google.gerrit.server.project.Util.category;
 import static com.google.gerrit.server.project.Util.value;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.fail;
 
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.GerritConfigs;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -55,16 +59,20 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -335,6 +343,15 @@
   }
 
   @Test
+  public void voteOnClosedChange() throws Exception {
+    PushOneCommit.Result r = createChange();
+    merge(r);
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("change is closed");
+    revision(r).review(ReviewInput.reject());
+  }
+
+  @Test
   public void voteOnBehalfOf() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     LabelType codeReviewType = Util.codeReview();
@@ -523,9 +540,190 @@
   }
 
   @Test
+  public void pushCommitOfOtherUser() throws Exception {
+    // admin pushes commit of user
+    PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    CommitInfo commit = change.revisions.get(change.currentRevision).commit;
+    assertThat(commit.author.email).isEqualTo(user.email);
+    assertThat(commit.committer.email).isEqualTo(user.email);
+
+    // check that the author/committer was added as reviewer
+    Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId)
+        .isEqualTo(user.getId().get());
+    assertThat(change.reviewers.get(CC)).isNull();
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.body())
+        .contains(admin.fullName + " has uploaded a new change for review");
+    assertThat(m.body())
+        .contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+    assertMailFrom(m, admin.email);
+  }
+
+  @Test
+  public void pushCommitOfOtherUserThatCannotSeeChange() throws Exception {
+    // create hidden project that is only visible to administrators
+    Project.NameKey p = createProject("p");
+    ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
+    Util.allow(cfg,
+        Permission.READ,
+        groupCache.get(new AccountGroup.NameKey("Administrators"))
+            .getGroupUUID(),
+        "refs/*");
+    Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
+    saveProjectConfig(p, cfg);
+
+    // admin pushes commit of user
+    TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
+    PushOneCommit push = pushFactory.create(db, user.getIdent(), repo);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    CommitInfo commit = change.revisions.get(change.currentRevision).commit;
+    assertThat(commit.author.email).isEqualTo(user.email);
+    assertThat(commit.committer.email).isEqualTo(user.email);
+
+    // check the user cannot see the change
+    setApiUser(user);
+    try {
+      gApi.changes().id(result.getChangeId()).get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException e) {
+      // Expected.
+    }
+
+    // check that the author/committer was NOT added as reviewer (he can't see
+    // the change)
+    assertThat(change.reviewers.get(REVIEWER)).isNull();
+    assertThat(change.reviewers.get(CC)).isNull();
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
+  public void pushCommitWithFooterOfOtherUser() throws Exception {
+    // admin pushes commit that references 'user' in a footer
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT + "\n\n"
+            + FooterConstants.REVIEWED_BY.getName() + ": "
+            + user.getIdent().toExternalString(),
+        PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    // check that 'user' was added as reviewer
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId)
+        .isEqualTo(user.getId().get());
+    assertThat(change.reviewers.get(CC)).isNull();
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+    assertThat(m.body()).contains("I'd like you to do a code review.");
+    assertThat(m.body())
+        .contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+    assertMailFrom(m, admin.email);
+  }
+
+  @Test
+  public void pushCommitWithFooterOfOtherUserThatCannotSeeChange()
+      throws Exception {
+    // create hidden project that is only visible to administrators
+    Project.NameKey p = createProject("p");
+    ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
+    Util.allow(cfg,
+        Permission.READ, groupCache
+            .get(new AccountGroup.NameKey("Administrators")).getGroupUUID(),
+        "refs/*");
+    Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
+    saveProjectConfig(p, cfg);
+
+    // admin pushes commit that references 'user' in a footer
+    TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo,
+        PushOneCommit.SUBJECT + "\n\n" + FooterConstants.REVIEWED_BY.getName()
+            + ": " + user.getIdent().toExternalString(),
+        PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    // check that 'user' cannot see the change
+    setApiUser(user);
+    try {
+      gApi.changes().id(result.getChangeId()).get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException e) {
+      // Expected.
+    }
+
+    // check that 'user' was NOT added as cc ('user' can't see the change)
+    setApiUser(admin);
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    assertThat(change.reviewers.get(REVIEWER)).isNull();
+    assertThat(change.reviewers.get(CC)).isNull();
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
+  public void addReviewerThatCannotSeeChange() throws Exception {
+    // create hidden project that is only visible to administrators
+    Project.NameKey p = createProject("p");
+    ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
+    Util.allow(cfg,
+        Permission.READ,
+        groupCache.get(new AccountGroup.NameKey("Administrators"))
+            .getGroupUUID(),
+        "refs/*");
+    Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
+    saveProjectConfig(p, cfg);
+
+    // create change
+    TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    // check the user cannot see the change
+    setApiUser(user);
+    try {
+      gApi.changes().id(result.getChangeId()).get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException e) {
+      // Expected.
+    }
+
+    // try to add user as reviewer
+    setApiUser(admin);
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    exception.expect(UnprocessableEntityException.class);
+    exception.expectMessage("Change not visible to " + user.email);
+    gApi.changes()
+        .id(result.getChangeId())
+        .addReviewer(in);
+  }
+
+  @Test
   public void addReviewer() throws Exception {
     TestTimeUtil.resetWithClockStep(1, SECONDS);
-    sender.clear();
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
@@ -544,7 +742,7 @@
     assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
     assertThat(m.body()).contains("I'd like you to do a code review.");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
-
+    assertMailFrom(m, admin.email);
     ChangeInfo c = gApi.changes()
         .id(r.getChangeId())
         .get();
@@ -553,9 +751,44 @@
     // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
     // approval on the change which is treated as CC when the ChangeInfo is
     // created.
-    Collection<AccountInfo> reviewers = NoteDbMode.readWrite()
-        ? c.reviewers.get(REVIEWER)
-        : c.reviewers.get(CC);
+    Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId)
+        .isEqualTo(user.getId().get());
+
+    // Ensure ETag and lastUpdatedOn are updated.
+    rsrc = parseResource(r);
+    assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
+    assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs);
+  }
+
+  @Test
+  public void addSelfAsReviewer() throws Exception {
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
+    PushOneCommit.Result r = createChange();
+    ChangeResource rsrc = parseResource(r);
+    String oldETag = rsrc.getETag();
+    Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
+
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    setApiUser(user);
+    gApi.changes()
+        .id(r.getChangeId())
+        .addReviewer(in);
+
+    // There should be no email notification when adding self
+    assertThat(sender.getMessages()).isEmpty();
+
+    // When NoteDb is enabled adding a reviewer records that user as reviewer
+    // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
+    // approval on the change which is treated as CC when the ChangeInfo is
+    // created.
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+    Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
     assertThat(reviewers.iterator().next()._accountId)
@@ -598,27 +831,13 @@
         .id(r.getChangeId())
         .get();
     reviewers = c.reviewers.get(REVIEWER);
-    if (NoteDbMode.readWrite()) {
-      // When NoteDb is enabled adding a reviewer records that user as reviewer
-      // in NoteDb.
-      assertThat(reviewers).hasSize(2);
-      Iterator<AccountInfo> reviewerIt = reviewers.iterator();
-      assertThat(reviewerIt.next()._accountId)
-          .isEqualTo(admin.getId().get());
-      assertThat(reviewerIt.next()._accountId)
-          .isEqualTo(user.getId().get());
-      assertThat(c.reviewers).doesNotContainKey(CC);
-    } else {
-      // When NoteDb is disabled adding a reviewer results in a dummy 0 approval
-      // on the change which is treated as CC when the ChangeInfo is created.
-      assertThat(reviewers).hasSize(1);
-      assertThat(reviewers.iterator().next()._accountId)
-          .isEqualTo(admin.getId().get());
-      Collection<AccountInfo> ccs = c.reviewers.get(CC);
-      assertThat(ccs).hasSize(1);
-      assertThat(ccs.iterator().next()._accountId)
-          .isEqualTo(user.getId().get());
-    }
+    assertThat(reviewers).hasSize(2);
+    Iterator<AccountInfo> reviewerIt = reviewers.iterator();
+    assertThat(reviewerIt.next()._accountId)
+        .isEqualTo(admin.getId().get());
+    assertThat(reviewerIt.next()._accountId)
+        .isEqualTo(user.getId().get());
+    assertThat(c.reviewers).doesNotContainKey(CC);
   }
 
   @Test
@@ -654,6 +873,19 @@
 
   @Test
   public void removeReviewerNoVotes() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+
+    LabelType verified = category("Verified", value(1, "Passes"),
+        value(0, "No score"), value(-1, "Failed"));
+    cfg.getLabelSections().put(verified.getName(), verified);
+
+    AccountGroup.UUID registeredUsers =
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+    String heads = RefNames.REFS_HEADS + "*";
+    Util.allow(cfg, Permission.forLabel(Util.verified().getName()), -1, 1,
+        registeredUsers, heads);
+    saveProjectConfig(project, cfg);
+
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     gApi.changes()
@@ -668,12 +900,34 @@
     assertThat(reviewers.iterator().next()._accountId)
         .isEqualTo(user.getId().get());
 
+    sender.clear();
     gApi.changes()
         .id(changeId)
         .reviewer(user.getId().toString())
         .remove();
-    assertThat(gApi.changes().id(changeId).get().reviewers.isEmpty());
+    assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty();
 
+    assertThat(sender.getMessages()).hasSize(1);
+    Message message = sender.getMessages().get(0);
+    assertThat(message.body()).contains(
+        "Removed reviewer " + user.fullName + ".");
+    assertThat(message.body()).doesNotContain("with the following votes");
+
+    // Make sure the reviewer can still be added again.
+    gApi.changes()
+        .id(changeId)
+        .addReviewer(user.getId().toString());
+    reviewers = Iterables.concat(gApi.changes().id(changeId).get().reviewers.values());
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId)
+        .isEqualTo(user.getId().get());
+
+    // Remove again, and then try to remove once more to verify 404 is
+    // returned.
+    gApi.changes()
+        .id(changeId)
+        .reviewer(user.getId().toString())
+        .remove();
     exception.expect(ResourceNotFoundException.class);
     gApi.changes()
         .id(changeId)
@@ -708,12 +962,19 @@
     assertThat(reviewerIt.next()._accountId)
         .isEqualTo(user.getId().get());
 
+    sender.clear();
     setApiUser(admin);
     gApi.changes()
         .id(changeId)
         .reviewer(user.getId().toString())
         .remove();
 
+    assertThat(sender.getMessages()).hasSize(1);
+    Message message = sender.getMessages().get(0);
+    assertThat(message.body()).contains(
+        "Removed reviewer " + user.fullName + " with the following votes");
+    assertThat(message.body()).contains("* Code-Review+1 by " + user.fullName);
+
     reviewers = gApi.changes()
         .id(changeId)
         .get()
@@ -727,6 +988,24 @@
   }
 
   @Test
+  public void removeReviewerNotPermitted() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    gApi.changes()
+        .id(changeId)
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage("delete reviewer not permitted");
+    gApi.changes()
+        .id(r.getChangeId())
+        .reviewer(admin.getId().toString())
+        .remove();
+  }
+
+  @Test
   public void deleteVote() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
@@ -783,21 +1062,9 @@
     assertThat(message.author._accountId).isEqualTo(admin.getId().get());
     assertThat(message.message).isEqualTo(
         "Removed Code-Review+1 by User <user@example.com>\n");
-    if (NoteDbMode.readWrite()) {
-      // When NoteDb is enabled each reviewer is explicitly recorded in the
-      // NoteDb and this record stays even when all votes of that user have been
-      // deleted.
-      assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-          .containsExactlyElementsIn(
-              ImmutableSet.of(admin.getId(), user.getId()));
-    } else {
-      // When NoteDb is disabled users that have only dummy 0 approvals on the
-      // change are returned as CC and not as REVIEWER.
-      assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-          .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
-      assertThat(getReviewers(c.reviewers.get(CC)))
-          .containsExactlyElementsIn(ImmutableSet.of(user.getId()));
-    }
+    assertThat(getReviewers(c.reviewers.get(REVIEWER)))
+        .containsExactlyElementsIn(
+            ImmutableSet.of(admin.getId(), user.getId()));
   }
 
   @Test
@@ -836,7 +1103,7 @@
 
     setApiUser(user);
     exception.expect(AuthException.class);
-    exception.expectMessage("delete not permitted");
+    exception.expectMessage("delete vote not permitted");
     gApi.changes()
         .id(r.getChangeId())
         .reviewer(admin.getId().toString())
@@ -884,16 +1151,9 @@
         .id(changeId)
         .addReviewer(in);
     c = gApi.changes().id(changeId).get();
-    if (NoteDbMode.readWrite()) {
-      assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-          .containsExactlyElementsIn(ImmutableSet.of(
-              admin.getId(), user.getId()));
-    } else {
-      assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-          .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
-      assertThat(getReviewers(c.reviewers.get(CC)))
-          .containsExactlyElementsIn(ImmutableSet.of(user.getId()));
-    }
+    assertThat(getReviewers(c.reviewers.get(REVIEWER)))
+        .containsExactlyElementsIn(ImmutableSet.of(
+            admin.getId(), user.getId()));
 
     // Approve the change as user, then remove the approval
     // (only to confirm that the user does have Code-Review+2 permission)
@@ -916,16 +1176,9 @@
 
     // User should still be on the change
     c = gApi.changes().id(changeId).get();
-    if (NoteDbMode.readWrite()) {
-      assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-          .containsExactlyElementsIn(ImmutableSet.of(
-              admin.getId(), user.getId()));
-    } else {
-      assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-          .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
-      assertThat(getReviewers(c.reviewers.get(CC)))
-          .containsExactlyElementsIn(ImmutableSet.of(user.getId()));
-    }
+    assertThat(getReviewers(c.reviewers.get(REVIEWER)))
+        .containsExactlyElementsIn(ImmutableSet.of(
+            admin.getId(), user.getId()));
   }
 
   @Test
@@ -1247,6 +1500,10 @@
   }
 
   @Test
+  @GerritConfigs({
+    @GerritConfig(name = "gerrit.editGpgKeys", value = "true"),
+    @GerritConfig(name = "receive.enableSignedPush", value = "true"),
+  })
   public void pushCertificates() throws Exception {
     PushOneCommit.Result r1 = createChange();
     PushOneCommit.Result r2 = amendChange(r1.getChangeId());
@@ -1412,6 +1669,7 @@
         db, admin.getIdent(), adminTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
+
     // Amend draft as admin
     PushOneCommit.Result r2 = amendChange(
         r1.getChangeId(), "refs/drafts/master", admin, adminTestRepo);
@@ -1424,10 +1682,154 @@
     // Amend change as user
     PushOneCommit.Result r3 = amendChange(
         r1.getChangeId(), "refs/for/master", user, userTestRepo);
-    r3.assertErrorStatus("cannot replace "
+    r3.assertErrorStatus("cannot add patch set to "
         + r3.getChange().change().getChangeId() + ".");
   }
 
+  @Test
+  public void createNewPatchSetWithoutPermission() throws Exception {
+    // Create new project with clean permissions
+    Project.NameKey p = createProject("addPatchSet1");
+
+    // Clone separate repositories of the same project as admin and as user
+    TestRepository<InMemoryRepository> adminTestRepo =
+        cloneProject(p, admin);
+    TestRepository<InMemoryRepository> userTestRepo =
+        cloneProject(p, user);
+
+    // Block default permission
+    block(Permission.ADD_PATCH_SET,
+        REGISTERED_USERS, "refs/for/*", p);
+
+    // Create change as admin
+    PushOneCommit push = pushFactory.create(
+        db, admin.getIdent(), adminTestRepo);
+    PushOneCommit.Result r1 = push.to("refs/for/master");
+    r1.assertOkStatus();
+
+    // Fetch change
+    GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps");
+    userTestRepo.reset("ps");
+
+    // Amend change as user
+    PushOneCommit.Result r2 =
+        amendChange(r1.getChangeId(), "refs/for/master", user, userTestRepo);
+    r2.assertErrorStatus("cannot add patch set to "
+        + r1.getChange().getId().id + ".");
+  }
+
+  @Test
+  public void createNewSetPatchWithPermission() throws Exception {
+    // Clone separate repositories of the same project as admin and as user
+    TestRepository<?> adminTestRepo = cloneProject(project, admin);
+    TestRepository<?> userTestRepo = cloneProject(project, user);
+
+    // Create change as admin
+    PushOneCommit push = pushFactory.create(
+        db, admin.getIdent(), adminTestRepo);
+    PushOneCommit.Result r1 = push.to("refs/for/master");
+    r1.assertOkStatus();
+
+    // Fetch change
+    GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps");
+    userTestRepo.reset("ps");
+
+    // Amend change as user
+    PushOneCommit.Result r2 = amendChange(
+        r1.getChangeId(), "refs/for/master", user, userTestRepo);
+    r2.assertOkStatus();
+  }
+
+  @Test
+  public void createNewPatchSetAsOwnerWithoutPermission() throws Exception {
+    // Create new project with clean permissions
+    Project.NameKey p = createProject("addPatchSet2");
+    // Clone separate repositories of the same project as admin and as user
+    TestRepository<?> adminTestRepo = cloneProject(project, admin);
+
+    // Block default permission
+    block(Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/for/*", p);
+
+    // Create change as admin
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), adminTestRepo);
+    PushOneCommit.Result r1 = push.to("refs/for/master");
+    r1.assertOkStatus();
+
+    // Fetch change
+    GitUtil.fetch(adminTestRepo, r1.getPatchSet().getRefName() + ":ps");
+    adminTestRepo.reset("ps");
+
+    // Amend change as admin
+    PushOneCommit.Result r2 = amendChange(
+        r1.getChangeId(), "refs/for/master", admin, adminTestRepo);
+    r2.assertOkStatus();
+  }
+
+  @Test
+  public void createNewPatchSetAsReviewerOnDraftChange() throws Exception {
+    // Clone separate repositories of the same project as admin and as user
+    TestRepository<?> adminTestRepo = cloneProject(project, admin);
+    TestRepository<?> userTestRepo = cloneProject(project, user);
+
+    // Create change as admin
+    PushOneCommit push = pushFactory.create(
+        db, admin.getIdent(), adminTestRepo);
+    PushOneCommit.Result r1 = push.to("refs/drafts/master");
+    r1.assertOkStatus();
+
+    // Add user as reviewer
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    gApi.changes()
+        .id(r1.getChangeId())
+        .addReviewer(in);
+
+    // Fetch change
+    GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps");
+    userTestRepo.reset("ps");
+
+    // Amend change as user
+    PushOneCommit.Result r2 = amendChange(
+        r1.getChangeId(), "refs/for/master", user, userTestRepo);
+    r2.assertOkStatus();
+  }
+
+  @Test
+  public void createNewDraftPatchSetOnDraftChange() throws Exception {
+    // Create new project with clean permissions
+    Project.NameKey p = createProject("addPatchSet4");
+    // Clone separate repositories of the same project as admin and as user
+    TestRepository<?> adminTestRepo = cloneProject(p, admin);
+    TestRepository<?> userTestRepo = cloneProject(p, user);
+
+    // Block default permission
+    block(Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/for/*", p);
+
+    // Create change as admin
+    PushOneCommit push = pushFactory.create(
+        db, admin.getIdent(), adminTestRepo);
+    PushOneCommit.Result r1 = push.to("refs/drafts/master");
+    r1.assertOkStatus();
+
+    // Add user as reviewer
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    gApi.changes()
+        .id(r1.getChangeId())
+        .addReviewer(in);
+
+    // Fetch change
+    GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps");
+    userTestRepo.reset("ps");
+
+    // Amend change as user
+    PushOneCommit.Result r2 = amendChange(
+        r1.getChangeId(), "refs/drafts/master", user, userTestRepo);
+    r2.assertErrorStatus("cannot add patch set to "
+        + r1.getChange().getId().id + ".");
+  }
+
   private static Iterable<Account.Id> getReviewers(
       Collection<AccountInfo> r) {
     return Iterables.transform(r, new Function<AccountInfo, Account.Id>() {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
new file mode 100644
index 0000000..54fe28f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -0,0 +1,538 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.extensions.client.ChangeKind.MERGE_FIRST_PARENT_UPDATE;
+import static com.google.gerrit.extensions.client.ChangeKind.NO_CODE_CHANGE;
+import static com.google.gerrit.extensions.client.ChangeKind.REWORK;
+import static com.google.gerrit.extensions.client.ChangeKind.TRIVIAL_REBASE;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.category;
+import static com.google.gerrit.server.project.Util.value;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.Util;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+@NoHttpd
+public class StickyApprovalsIT extends AbstractDaemonTest {
+  @Before
+  public void setup() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+
+    // Overwrite "Code-Review" label that is inherited from All-Projects.
+    // This way changes to the "Code Review" label don't affect other tests.
+    LabelType codeReview =
+        category("Code-Review", value(2, "Looks good to me, approved"),
+            value(1, "Looks good to me, but someone else must approve"),
+            value(0, "No score"),
+            value(-1, "I would prefer that you didn't submit this"),
+            value(-2, "Do not submit"));
+    cfg.getLabelSections().put(codeReview.getName(), codeReview);
+
+    LabelType verified = category("Verified", value(1, "Passes"),
+        value(0, "No score"), value(-1, "Failed"));
+    cfg.getLabelSections().put(verified.getName(), verified);
+
+    AccountGroup.UUID registeredUsers =
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+    String heads = RefNames.REFS_HEADS + "*";
+    Util.allow(cfg, Permission.forLabel(Util.codeReview().getName()), -2, 2,
+        registeredUsers, heads);
+    Util.allow(cfg, Permission.forLabel(Util.verified().getName()), -1, 1,
+        registeredUsers, heads);
+    saveProjectConfig(project, cfg);
+  }
+
+  @Test
+  public void notSticky() throws Exception {
+    assertNotSticky(EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE,
+        MERGE_FIRST_PARENT_UPDATE));
+  }
+
+  @Test
+  public void stickyOnMinScore() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().get("Code-Review").setCopyMinScore(true);
+    saveProjectConfig(project, cfg);
+
+    for (ChangeKind changeKind : EnumSet.of(REWORK, TRIVIAL_REBASE,
+        NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE)) {
+      testRepo.reset(getRemoteHead());
+
+      String changeId = createChange(changeKind);
+      vote(admin, changeId, -1, 1);
+      vote(user, changeId, -2, -1);
+
+      updateChange(changeId, changeKind);
+      ChangeInfo c = detailedChange(changeId);
+      assertVotes(c, admin, 0, 0, changeKind);
+      assertVotes(c, user, -2, 0, changeKind);
+    }
+  }
+
+  @Test
+  public void stickyOnMaxScore() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().get("Code-Review").setCopyMaxScore(true);
+    saveProjectConfig(project, cfg);
+
+    for (ChangeKind changeKind : EnumSet.of(REWORK, TRIVIAL_REBASE,
+        NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE)) {
+      testRepo.reset(getRemoteHead());
+
+      String changeId = createChange(changeKind);
+      vote(admin, changeId, 2, 1);
+      vote(user, changeId, 1, -1);
+
+      updateChange(changeId, changeKind);
+      ChangeInfo c = detailedChange(changeId);
+      assertVotes(c, admin, 2, 0, changeKind);
+      assertVotes(c, user, 0, 0, changeKind);
+    }
+  }
+
+  @Test
+  public void stickyOnTrivialRebase() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().get("Code-Review")
+        .setCopyAllScoresOnTrivialRebase(true);
+    saveProjectConfig(project, cfg);
+
+    String changeId = createChange(TRIVIAL_REBASE);
+    vote(admin, changeId, 2, 1);
+    vote(user, changeId, -2, -1);
+
+    updateChange(changeId, TRIVIAL_REBASE);
+    ChangeInfo c = detailedChange(changeId);
+    assertVotes(c, admin, 2, 0, TRIVIAL_REBASE);
+    assertVotes(c, user, -2, 0, TRIVIAL_REBASE);
+
+    assertNotSticky(
+        EnumSet.of(REWORK, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE));
+
+    // check that votes are sticky when trivial rebase is done by cherry-pick
+    testRepo.reset(getRemoteHead());
+    changeId = createChange().getChangeId();
+    vote(admin, changeId, 2, 1);
+    vote(user, changeId, -2, -1);
+
+    String cherryPickChangeId = cherryPick(changeId, TRIVIAL_REBASE);
+    c = detailedChange(cherryPickChangeId);
+    assertVotes(c, admin, 2, 0);
+    assertVotes(c, user, -2, 0);
+
+    // check that votes are not sticky when rework is done by cherry-pick
+    testRepo.reset(getRemoteHead());
+    changeId = createChange().getChangeId();
+    vote(admin, changeId, 2, 1);
+    vote(user, changeId, -2, -1);
+
+    cherryPickChangeId = cherryPick(changeId, REWORK);
+    c = detailedChange(cherryPickChangeId);
+    assertVotes(c, admin, 0, 0);
+    assertVotes(c, user, 0, 0);
+  }
+
+  @Test
+  public void stickyOnNoCodeChange() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().get("Verified")
+        .setCopyAllScoresIfNoCodeChange(true);
+    saveProjectConfig(project, cfg);
+
+    String changeId = createChange(NO_CODE_CHANGE);
+    vote(admin, changeId, 2, 1);
+    vote(user, changeId, -2, -1);
+
+    updateChange(changeId, NO_CODE_CHANGE);
+    ChangeInfo c = detailedChange(changeId);
+    assertVotes(c, admin, 0, 1, NO_CODE_CHANGE);
+    assertVotes(c, user, 0, -1, NO_CODE_CHANGE);
+
+    assertNotSticky(
+        EnumSet.of(REWORK, TRIVIAL_REBASE, MERGE_FIRST_PARENT_UPDATE));
+  }
+
+  @Test
+  public void stickyOnMergeFirstParentUpdate() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().get("Code-Review")
+        .setCopyAllScoresOnMergeFirstParentUpdate(true);
+    saveProjectConfig(project, cfg);
+
+    String changeId = createChange(MERGE_FIRST_PARENT_UPDATE);
+    vote(admin, changeId, 2, 1);
+    vote(user, changeId, -2, -1);
+
+    updateChange(changeId, MERGE_FIRST_PARENT_UPDATE);
+    ChangeInfo c = detailedChange(changeId);
+    assertVotes(c, admin, 2, 0, MERGE_FIRST_PARENT_UPDATE);
+    assertVotes(c, user, -2, 0, MERGE_FIRST_PARENT_UPDATE);
+
+    assertNotSticky(EnumSet.of(REWORK, NO_CODE_CHANGE, TRIVIAL_REBASE));
+  }
+
+  @Test
+  public void removedVotesNotSticky() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().get("Code-Review")
+        .setCopyAllScoresOnTrivialRebase(true);
+    cfg.getLabelSections().get("Verified").setCopyAllScoresIfNoCodeChange(true);
+    saveProjectConfig(project, cfg);
+
+    for (ChangeKind changeKind : EnumSet.of(REWORK, TRIVIAL_REBASE,
+        NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE)) {
+      testRepo.reset(getRemoteHead());
+
+      String changeId = createChange(changeKind);
+      vote(admin, changeId, 2, 1);
+      vote(user, changeId, -2, -1);
+
+      // Remove votes by re-voting with 0
+      vote(admin, changeId, 0, 0);
+      vote(user, changeId, 0, 0);
+      ChangeInfo c = detailedChange(changeId);
+      assertVotes(c, admin, 0, 0, null);
+      assertVotes(c, user, 0, 0, null);
+
+      updateChange(changeId, changeKind);
+      c = detailedChange(changeId);
+      assertVotes(c, admin, 0, 0, changeKind);
+      assertVotes(c, user, 0, 0, changeKind);
+    }
+  }
+
+  @Test
+  public void stickyAcrossMultiplePatchSets() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().get("Code-Review")
+        .setCopyMaxScore(true);
+    cfg.getLabelSections().get("Verified")
+        .setCopyAllScoresIfNoCodeChange(true);
+    saveProjectConfig(project, cfg);
+
+    String changeId = createChange(REWORK);
+    vote(admin, changeId, 2, 1);
+
+    for (int i = 0; i < 5; i++) {
+      updateChange(changeId, NO_CODE_CHANGE);
+      ChangeInfo c = detailedChange(changeId);
+      assertVotes(c, admin, 2, 1, NO_CODE_CHANGE);
+    }
+
+    updateChange(changeId, REWORK);
+    ChangeInfo c = detailedChange(changeId);
+    assertVotes(c, admin, 2, 0, REWORK);
+  }
+
+  @Test
+  public void copyMinMaxAcrossMultiplePatchSets() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().get("Code-Review")
+        .setCopyMaxScore(true);
+    cfg.getLabelSections().get("Code-Review")
+        .setCopyMinScore(true);
+    saveProjectConfig(project, cfg);
+
+    // Vote max score on PS1
+    String changeId = createChange(REWORK);
+    vote(admin, changeId, 2, 1);
+
+    // Have someone else vote min score on PS2
+    updateChange(changeId, REWORK);
+    vote(user, changeId, -2, 0);
+    ChangeInfo c = detailedChange(changeId);
+    assertVotes(c, admin, 2, 0, REWORK);
+    assertVotes(c, user, -2, 0, REWORK);
+
+    // No vote changes on PS3
+    updateChange(changeId, REWORK);
+    c = detailedChange(changeId);
+    assertVotes(c, admin, 2, 0, REWORK);
+    assertVotes(c, user, -2, 0, REWORK);
+
+    // Both users revote on PS4
+    updateChange(changeId, REWORK);
+    vote(admin, changeId, 1, 1);
+    vote(user, changeId, 1, 1);
+    c = detailedChange(changeId);
+    assertVotes(c, admin, 1, 1, REWORK);
+    assertVotes(c, user, 1, 1, REWORK);
+
+    // New approvals shouldn't carry through to PS5
+    updateChange(changeId, REWORK);
+    c = detailedChange(changeId);
+    assertVotes(c, admin, 0, 0, REWORK);
+    assertVotes(c, user, 0, 0, REWORK);
+  }
+
+  private ChangeInfo detailedChange(String changeId) throws Exception {
+    return gApi.changes().id(changeId)
+        .get(EnumSet.of(ListChangesOption.DETAILED_LABELS,
+            ListChangesOption.CURRENT_REVISION,
+            ListChangesOption.CURRENT_COMMIT));
+  }
+
+  private void assertNotSticky(Set<ChangeKind> changeKinds) throws Exception {
+    for (ChangeKind changeKind : changeKinds) {
+      testRepo.reset(getRemoteHead());
+
+      String changeId = createChange(changeKind);
+      vote(admin, changeId, +2, 1);
+      vote(user, changeId, -2, -1);
+
+      updateChange(changeId, changeKind);
+      ChangeInfo c = detailedChange(changeId);
+      assertVotes(c, admin, 0, 0, changeKind);
+      assertVotes(c, user, 0, 0, changeKind);
+    }
+  }
+
+  private String createChange(ChangeKind kind) throws Exception {
+    switch (kind) {
+      case NO_CODE_CHANGE:
+      case REWORK:
+      case TRIVIAL_REBASE:
+      case NO_CHANGE:
+        return createChange().getChangeId();
+      case MERGE_FIRST_PARENT_UPDATE:
+        return createChangeForMergeCommit();
+      default:
+        throw new IllegalStateException("unexpected change kind: " + kind);
+    }
+  }
+
+  private void updateChange(String changeId, ChangeKind changeKind)
+      throws Exception {
+    switch (changeKind) {
+      case NO_CODE_CHANGE:
+        noCodeChange(changeId);
+        return;
+      case REWORK:
+        rework(changeId);
+        return;
+      case TRIVIAL_REBASE:
+        trivialRebase(changeId);
+        return;
+      case MERGE_FIRST_PARENT_UPDATE:
+        updateFirstParent(changeId);
+        return;
+      case NO_CHANGE:
+      default:
+        fail("unexpected change kind: " + changeKind);
+    }
+  }
+
+  private void noCodeChange(String changeId) throws Exception {
+    TestRepository<?>.CommitBuilder commitBuilder =
+        testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1));
+    commitBuilder.message("New subject " + System.nanoTime())
+        .author(admin.getIdent())
+        .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()));
+    commitBuilder.create();
+    GitUtil.pushHead(testRepo, "refs/for/master", false);
+    assertThat(getChangeKind(changeId)).isEqualTo(NO_CODE_CHANGE);
+  }
+
+  private void rework(String changeId) throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME,
+        "new content " + System.nanoTime(), changeId);
+    push.to("refs/for/master").assertOkStatus();
+    assertThat(getChangeKind(changeId)).isEqualTo(REWORK);
+  }
+
+  private void trivialRebase(String changeId) throws Exception {
+    setApiUser(admin);
+    testRepo.reset(getRemoteHead());
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, "Other Change",
+            "a" + System.nanoTime() + ".txt", PushOneCommit.FILE_CONTENT);
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+    RevisionApi revision = gApi.changes()
+        .id(r.getChangeId())
+        .current();
+    ReviewInput in = new ReviewInput()
+        .label("Code-Review", 2)
+        .label("Verified", 1);
+    revision.review(in);
+    revision.submit();
+
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .rebase();
+    assertThat(getChangeKind(changeId)).isEqualTo(TRIVIAL_REBASE);
+  }
+
+  private String createChangeForMergeCommit() throws Exception {
+    ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+
+    PushOneCommit.Result parent1 =
+        createChange("parent 1", "p1.txt", "content 1");
+
+    testRepo.reset(initial);
+    PushOneCommit.Result parent2 =
+        createChange("parent 2", "p2.txt", "content 2");
+
+    testRepo.reset(parent1.getCommit());
+
+    PushOneCommit merge = pushFactory.create(db, admin.getIdent(), testRepo);
+    merge.setParents(
+        ImmutableList.of(parent1.getCommit(), parent2.getCommit()));
+    PushOneCommit.Result result = merge.to("refs/for/master");
+    result.assertOkStatus();
+    return result.getChangeId();
+  }
+
+  private void updateFirstParent(String changeId) throws Exception {
+    ChangeInfo c = detailedChange(changeId);
+    List<CommitInfo> parents = c.revisions.get(c.currentRevision).commit.parents;
+    String parent1 = parents.get(0).commit;
+    String parent2 = parents.get(1).commit;
+    RevCommit commitParent2 =
+        testRepo.getRevWalk().parseCommit(ObjectId.fromString(parent2));
+
+    testRepo.reset(parent1);
+    PushOneCommit.Result newParent1 =
+        createChange("new parent 1", "p1-1.txt", "content 1-1");
+
+    PushOneCommit merge =
+        pushFactory.create(db, admin.getIdent(), testRepo, changeId);
+    merge.setParents(
+        ImmutableList.of(newParent1.getCommit(), commitParent2));
+    PushOneCommit.Result result = merge.to("refs/for/master");
+    result.assertOkStatus();
+
+    assertThat(getChangeKind(changeId)).isEqualTo(MERGE_FIRST_PARENT_UPDATE);
+  }
+
+  private String cherryPick(String changeId, ChangeKind changeKind) throws Exception {
+    switch (changeKind) {
+      case REWORK:
+      case TRIVIAL_REBASE:
+        break;
+      case NO_CODE_CHANGE:
+      case NO_CHANGE:
+      case MERGE_FIRST_PARENT_UPDATE:
+      default:
+        fail("unexpected change kind: " + changeKind);
+    }
+
+    testRepo.reset(getRemoteHead());
+    PushOneCommit.Result r = pushFactory
+        .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "other.txt", "new content " + System.nanoTime())
+        .to("refs/for/master");
+    r.assertOkStatus();
+    vote(admin, r.getChangeId(), 2, 1);
+    merge(r);
+
+    String subject = TRIVIAL_REBASE.equals(changeKind)
+        ? PushOneCommit.SUBJECT
+        : "Reworked change " + System.nanoTime();
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "master";
+    in.message =
+        String.format("%s\n\nChange-Id: %s", subject, changeId);
+    ChangeInfo c = gApi.changes()
+        .id(changeId)
+        .revision("current")
+        .cherryPick(in)
+        .get();
+    return c.changeId;
+  }
+
+  private ChangeKind getChangeKind(String changeId) throws Exception {
+    ChangeInfo c = gApi.changes().id(changeId)
+        .get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
+    return c.revisions.get(c.currentRevision).kind;
+  }
+
+  private void vote(TestAccount user, String changeId, int codeReviewVote,
+      int verifiedVote) throws Exception {
+    setApiUser(user);
+    ReviewInput in = new ReviewInput()
+        .label("Code-Review", codeReviewVote)
+        .label("Verified", verifiedVote);
+    gApi.changes().id(changeId).current().review(in);
+  }
+
+  private void assertVotes(ChangeInfo c, TestAccount user, int codeReviewVote,
+      int verifiedVote) {
+    assertVotes(c, user, codeReviewVote, verifiedVote, null);
+  }
+
+  private void assertVotes(ChangeInfo c, TestAccount user, int codeReviewVote,
+      int verifiedVote, ChangeKind changeKind) {
+    assertVotes(c, user, "Code-Review", codeReviewVote, changeKind);
+    assertVotes(c, user, "Verified", verifiedVote, changeKind);
+  }
+
+  private void assertVotes(ChangeInfo c, TestAccount user, String label,
+      int expectedVote, ChangeKind changeKind) {
+    Integer vote = 0;
+    if (c.labels.get(label) != null && c.labels.get(label).all != null) {
+      for (ApprovalInfo approval : c.labels.get(label).all) {
+        if (approval._accountId == user.id.get()) {
+          vote = approval.value;
+          break;
+        }
+      }
+    }
+
+    String name = "label = " + label;
+    if (changeKind != null) {
+      name += "; changeKind = " + changeKind.name();
+    }
+    assertThat(vote)
+        .named(name)
+        .isEqualTo(expectedVote);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java
new file mode 100644
index 0000000..1dcdaed
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.config;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.AssertUtil.assertPrefs;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.After;
+import org.junit.Test;
+
+@NoHttpd
+public class GeneralPreferencesIT extends AbstractDaemonTest {
+  @Inject
+  private AllUsersName allUsers;
+
+  @After
+  public void cleanUp() throws Exception {
+    try (Repository git = repoManager.openRepository(allUsers)) {
+      if (git.exactRef(RefNames.REFS_USERS_DEFAULT) != null) {
+        RefUpdate u = git.updateRef(RefNames.REFS_USERS_DEFAULT);
+        u.setForceUpdate(true);
+        assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
+      }
+    }
+    accountCache.evictAll();
+  }
+
+  @Test
+  public void getGeneralPreferences() throws Exception {
+    GeneralPreferencesInfo result =
+        gApi.config().server().getDefaultPreferences();
+    assertPrefs(result, GeneralPreferencesInfo.defaults(), "my");
+  }
+
+  @Test
+  public void setGeneralPreferences() throws Exception {
+    boolean newSignedOffBy = !GeneralPreferencesInfo.defaults().signedOffBy;
+    GeneralPreferencesInfo update = new GeneralPreferencesInfo();
+    update.signedOffBy = newSignedOffBy;
+    GeneralPreferencesInfo result =
+        gApi.config().server().setDefaultPreferences(update);
+    assertThat(result.signedOffBy).named("signedOffBy").isEqualTo(newSignedOffBy);
+
+    result = gApi.config().server().getDefaultPreferences();
+    GeneralPreferencesInfo expected = GeneralPreferencesInfo.defaults();
+    expected.signedOffBy = newSignedOffBy;
+    assertPrefs(result, expected, "my");
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
index a3de2b4..6892893 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.RefNames;
 
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
@@ -44,8 +45,8 @@
             .get()
             .name);
 
-    RevCommit head = getRemoteHead(name, "refs/meta/config");
-    eventRecorder.assertRefUpdatedEvents(name, "refs/meta/config",
+    RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
+    eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG,
         null, head);
 
     eventRecorder.assertRefUpdatedEvents(name, "refs/heads/master",
@@ -61,8 +62,8 @@
             .get()
             .name);
 
-    RevCommit head = getRemoteHead(name, "refs/meta/config");
-    eventRecorder.assertRefUpdatedEvents(name, "refs/meta/config",
+    RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
+    eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG,
         null, head);
 
     eventRecorder.assertRefUpdatedEvents(name, "refs/heads/master",
@@ -81,8 +82,8 @@
             .get()
             .name);
 
-    RevCommit head = getRemoteHead(name, "refs/meta/config");
-    eventRecorder.assertRefUpdatedEvents(name, "refs/meta/config",
+    RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
+    eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG,
         null, head);
 
     head = getRemoteHead(name, "refs/heads/master");
@@ -133,7 +134,7 @@
 
   @Test
   public void description() throws Exception {
-    RevCommit initialHead = getRemoteHead(project, "refs/meta/config");
+    RevCommit initialHead = getRemoteHead(project, RefNames.REFS_CONFIG);
     assertThat(gApi.projects()
             .name(project.get())
             .description())
@@ -148,14 +149,14 @@
             .description())
         .isEqualTo(in.description);
 
-    RevCommit updatedHead = getRemoteHead(project, "refs/meta/config");
-    eventRecorder.assertRefUpdatedEvents(project.get(), "refs/meta/config",
+    RevCommit updatedHead = getRemoteHead(project, RefNames.REFS_CONFIG);
+    eventRecorder.assertRefUpdatedEvents(project.get(), RefNames.REFS_CONFIG,
         initialHead, updatedHead);
   }
 
   @Test
   public void config() throws Exception {
-    RevCommit initialHead = getRemoteHead(project, "refs/meta/config");
+    RevCommit initialHead = getRemoteHead(project, RefNames.REFS_CONFIG);
 
     ConfigInfo info = gApi.projects().name(project.get()).config();
     assertThat(info.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
@@ -166,8 +167,8 @@
     info = gApi.projects().name(project.get()).config();
     assertThat(info.submitType).isEqualTo(SubmitType.CHERRY_PICK);
 
-    RevCommit updatedHead = getRemoteHead(project, "refs/meta/config");
-    eventRecorder.assertRefUpdatedEvents(project.get(), "refs/meta/config",
+    RevCommit updatedHead = getRemoteHead(project, RefNames.REFS_CONFIG);
+    eventRecorder.assertRefUpdatedEvents(project.get(), RefNames.REFS_CONFIG,
         initialHead, updatedHead);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 24cbac4..3629e29 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -27,8 +27,8 @@
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
@@ -52,6 +52,7 @@
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ETagView;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.server.change.GetRevisionActions;
@@ -77,7 +78,6 @@
 import java.util.Locale;
 import java.util.Map;
 
-@NoHttpd
 public class RevisionIT extends AbstractDaemonTest {
 
   @Inject
@@ -526,6 +526,35 @@
   }
 
   @Test
+  public void filesOnMergeCommitChange() throws Exception {
+    PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
+
+    // list files against auto-merge
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .files()
+        .keySet()
+      ).containsExactly(Patch.COMMIT_MSG, "foo", "bar");
+
+    // list files against parent 1
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .files(1)
+        .keySet()
+      ).containsExactly(Patch.COMMIT_MSG, "bar");
+
+    // list files against parent 2
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .files(2)
+        .keySet()
+      ).containsExactly(Patch.COMMIT_MSG, "foo");
+  }
+
+  @Test
   public void diff() throws Exception {
     PushOneCommit.Result r = createChange();
     DiffInfo diff = gApi.changes()
@@ -538,6 +567,61 @@
   }
 
   @Test
+  public void diffNonExistingFile() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    exception.expect(ResourceNotFoundException.class);
+    exception.expectMessage("non-existing");
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .file("non-existing")
+        .diff();
+  }
+
+  @Test
+  public void diffOnMergeCommitChange() throws Exception {
+    PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
+
+    DiffInfo diff;
+
+    // automerge
+    diff = gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .file("foo")
+        .diff();
+    assertThat(diff.metaA.lines).isEqualTo(5);
+    assertThat(diff.metaB.lines).isEqualTo(1);
+
+    diff = gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .file("bar")
+        .diff();
+    assertThat(diff.metaA.lines).isEqualTo(5);
+    assertThat(diff.metaB.lines).isEqualTo(1);
+
+    // parent 1
+    diff = gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .file("bar")
+        .diff(1);
+    assertThat(diff.metaA.lines).isEqualTo(1);
+    assertThat(diff.metaB.lines).isEqualTo(1);
+
+    // parent 2
+    diff = gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .file("foo")
+        .diff(2);
+    assertThat(diff.metaA.lines).isEqualTo(1);
+    assertThat(diff.metaB.lines).isEqualTo(1);
+  }
+
+  @Test
   public void content() throws Exception {
     PushOneCommit.Result r = createChange();
     BinaryResult bin = gApi.changes()
@@ -551,6 +635,20 @@
     assertThat(res).isEqualTo(FILE_CONTENT);
   }
 
+  @Test
+  public void contentType() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    String endPoint = "/changes/" + r.getChangeId()
+      + "/revisions/" + r.getCommit().name()
+      + "/files/" + FILE_NAME
+      + "/content";
+    RestResponse response = adminRestSession.head(endPoint);
+    response.assertOK();
+    assertThat(response.getContentType()).startsWith("text/plain");
+    assertThat(response.hasContent()).isFalse();
+  }
+
   private void assertMergeable(String id, boolean expected) throws Exception {
     MergeableInfo m = gApi.changes().id(id).current().mergeable();
     assertThat(m.mergeable).isEqualTo(expected);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 6a2c49f..fdf18a6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
@@ -29,6 +30,7 @@
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -42,6 +44,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeEdits.EditMessage;
 import com.google.gerrit.server.change.ChangeEdits.Post;
@@ -61,6 +64,8 @@
 import com.google.inject.Inject;
 
 import org.apache.commons.codec.binary.StringUtils;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -768,6 +773,28 @@
     assertThat(diff.diffHeader.get(0)).contains(FILE_NAME);
   }
 
+  @Test
+  public void createEditWithoutPushPatchSetPermission() throws Exception {
+    // Create new project with clean permissions
+    Project.NameKey p = createProject("addPatchSetEdit");
+    // Clone repository as user
+    TestRepository<InMemoryRepository> userTestRepo =
+        cloneProject(p, user);
+
+    // Block default permission
+    block(Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/for/*", p);
+
+    // Create change as user
+    PushOneCommit push = pushFactory.create(
+        db, user.getIdent(), userTestRepo);
+    PushOneCommit.Result r1 = push.to("refs/for/master");
+    r1.assertOkStatus();
+
+    // Try to create edit as admin
+    assertThat(modifier.createEdit(r1.getChange().change(),
+        r1.getPatchSet())).isEqualTo(RefUpdate.Result.REJECTED);
+  }
+
   private List<ChangeInfo> queryEdits() throws Exception {
     return query("project:{" + project.get() + "} has:edit");
   }
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 0e1389b..7dee60f 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
@@ -14,29 +14,30 @@
 
 package com.google.gerrit.acceptance.git;
 
-import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
 import static com.google.gerrit.acceptance.GitUtil.assertPushRejected;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
-import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.project.Util.category;
 import static com.google.gerrit.server.project.Util.value;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -48,6 +49,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
@@ -57,6 +59,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RefSpec;
 import org.junit.AfterClass;
 import org.junit.Before;
@@ -119,14 +122,14 @@
   }
 
   @Test
-  public void testPushForMaster() throws Exception {
+  public void pushForMaster() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
     r.assertChange(Change.Status.NEW, null);
   }
 
   @Test
-  public void testOutput() throws Exception {
+  public void output() throws Exception {
     String url = canonicalWebUrl.get();
     ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
     PushOneCommit.Result r1 = pushTo("refs/for/master");
@@ -159,7 +162,7 @@
   }
 
   @Test
-  public void testPushForMasterWithTopic() throws Exception {
+  public void pushForMasterWithTopic() throws Exception {
     // specify topic in ref
     String topic = "my/topic";
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
@@ -173,7 +176,7 @@
   }
 
   @Test
-  public void testPushForMasterWithNotify() throws Exception {
+  public void pushForMasterWithNotify() throws Exception {
     TestAccount user2 = accounts.user2();
     String pushSpec = "refs/for/master"
         + "%reviewer=" + user.email
@@ -207,7 +210,7 @@
   }
 
   @Test
-  public void testPushForMasterWithCc() throws Exception {
+  public void pushForMasterWithCc() throws Exception {
     // cc one user
     String topic = "my/topic";
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
@@ -230,7 +233,7 @@
   }
 
   @Test
-  public void testPushForMasterWithReviewer() throws Exception {
+  public void pushForMasterWithReviewer() throws Exception {
     // add one reviewer
     String topic = "my/topic";
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email);
@@ -254,7 +257,7 @@
   }
 
   @Test
-  public void testPushForMasterAsDraft() throws Exception {
+  public void pushForMasterAsDraft() throws Exception {
     // create draft by pushing to 'refs/drafts/'
     PushOneCommit.Result r = pushTo("refs/drafts/master");
     r.assertOkStatus();
@@ -267,7 +270,20 @@
   }
 
   @Test
-  public void testPushForMasterAsEdit() throws Exception {
+  public void publishDraftChangeByPushingNonDraftPatchSet() throws Exception {
+    // create draft change
+    PushOneCommit.Result r = pushTo("refs/drafts/master");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.DRAFT, null);
+
+    // publish draft change by pushing non-draft patch set
+    r = amendChange(r.getChangeId(), "refs/for/master");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, null);
+  }
+
+  @Test
+  public void pushForMasterAsEdit() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
     EditInfo edit = getEdit(r.getChangeId());
@@ -278,10 +294,14 @@
     r.assertOkStatus();
     edit = getEdit(r.getChangeId());
     assertThat(edit).isNotNull();
+    r.assertMessage("Updated Changes:\n  "
+        + canonicalWebUrl.get()
+        + r.getChange().getId()
+        + " " + edit.commit.subject + " [EDIT]\n");
   }
 
   @Test
-  public void testPushForMasterWithMessage() throws Exception {
+  public void pushForMasterWithMessage() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master/%m=my_test_message");
     r.assertOkStatus();
     r.assertChange(Change.Status.NEW, null);
@@ -295,7 +315,7 @@
   }
 
   @Test
-  public void testPushForMasterWithApprovals() throws Exception {
+  public void pushForMasterWithApprovals() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review");
     r.assertOkStatus();
     ChangeInfo ci = get(r.getChangeId());
@@ -340,7 +360,7 @@
    * applied on behalf of the uploader a single label is sufficient.
    */
   @Test
-  public void testPushForMasterWithApprovalsForgeCommitterButNoForgeVote()
+  public void pushForMasterWithApprovalsForgeCommitterButNoForgeVote()
       throws Exception {
     // Create a commit with "User" as author and committer
     RevCommit c = commitBuilder()
@@ -374,7 +394,7 @@
   }
 
   @Test
-  public void testPushWithMultipleApprovals()
+  public void pushWithMultipleApprovals()
       throws Exception {
     LabelType Q = category("Custom-Label",
         value(1, "Positive"),
@@ -405,7 +425,7 @@
   }
 
   @Test
-  public void testPushNewPatchsetToRefsChanges() throws Exception {
+  public void pushNewPatchsetToRefsChanges() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
     PushOneCommit push =
@@ -416,38 +436,39 @@
   }
 
   @Test
-  public void testPushNewPatchsetToPatchSetLockedChange() throws Exception {
+  public void pushNewPatchsetToPatchSetLockedChange() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
         PushOneCommit.SUBJECT, "b.txt", "anotherContent", r.getChangeId());
     revision(r).review(new ReviewInput().label("Patch-Set-Lock", 1));
     r = push.to("refs/for/master");
-    r.assertErrorStatus("cannot replace " + r.getChange().change().getChangeId()
+    r.assertErrorStatus("cannot add patch set to "
+        + r.getChange().change().getChangeId()
         + ". Change is patch set locked.");
   }
 
   @Test
-  public void testPushForMasterWithApprovals_MissingLabel() throws Exception {
+  public void pushForMasterWithApprovals_MissingLabel() throws Exception {
       PushOneCommit.Result r = pushTo("refs/for/master/%l=Verify");
       r.assertErrorStatus("label \"Verify\" is not a configured label");
   }
 
   @Test
-  public void testPushForMasterWithApprovals_ValueOutOfRange() throws Exception {
+  public void pushForMasterWithApprovals_ValueOutOfRange() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review-3");
     r.assertErrorStatus("label \"Code-Review\": -3 is not a valid value");
   }
 
   @Test
-  public void testPushForNonExistingBranch() throws Exception {
+  public void pushForNonExistingBranch() throws Exception {
     String branchName = "non-existing";
     PushOneCommit.Result r = pushTo("refs/for/" + branchName);
     r.assertErrorStatus("branch " + branchName + " not found");
   }
 
   @Test
-  public void testPushForMasterWithHashtags() throws Exception {
+  public void pushForMasterWithHashtags() throws Exception {
     // Hashtags only work when reading from NoteDB is enabled
     assume().that(notesMigration.readChanges()).isTrue();
 
@@ -474,7 +495,7 @@
   }
 
   @Test
-  public void testPushForMasterWithMultipleHashtags() throws Exception {
+  public void pushForMasterWithMultipleHashtags() throws Exception {
     // Hashtags only work when reading from NoteDB is enabled
     assume().that(notesMigration.readChanges()).isTrue();
 
@@ -504,7 +525,7 @@
   }
 
   @Test
-  public void testPushForMasterWithHashtagsNoteDbDisabled() throws Exception {
+  public void pushForMasterWithHashtagsNoteDbDisabled() throws Exception {
     // Push with hashtags should fail when reading from NoteDb is disabled.
     assume().that(notesMigration.readChanges()).isFalse();
     PushOneCommit.Result r = pushTo("refs/for/master%hashtag=tag1");
@@ -512,7 +533,7 @@
   }
 
   @Test
-  public void testPushCommitUsingSignedOffBy() throws Exception {
+  public void pushCommitUsingSignedOffBy() throws Exception {
     PushOneCommit push =
         pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             "b.txt", "anotherContent");
@@ -537,7 +558,7 @@
   }
 
   @Test
-  public void testCreateNewChangeForAllNotInTarget() throws Exception {
+  public void createNewChangeForAllNotInTarget() throws Exception {
     ProjectConfig config = projectCache.checkedGet(project).getConfig();
     config.getProject().setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
     saveProjectConfig(project, config);
@@ -553,27 +574,61 @@
             "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
+
+    gApi.projects()
+        .name(project.get())
+        .branch("otherBranch")
+        .create(new BranchInput());
+
+    PushOneCommit.Result r2 = push.to("refs/for/otherBranch");
+    r2.assertOkStatus();
+    assertTwoChangesWithSameRevision(r);
   }
 
   @Test
-  public void testPushAFewChanges() throws Exception {
+  public void pushSameCommitTwiceUsingMagicBranchBaseOption()
+      throws Exception {
+    grant(Permission.PUSH, project, "refs/heads/master");
+    PushOneCommit.Result rBase = pushTo("refs/heads/master");
+    rBase.assertOkStatus();
+
+    gApi.projects()
+        .name(project.get())
+        .branch("foo")
+        .create(new BranchInput());
+
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent");
+
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    PushResult pr = GitUtil.pushHead(
+        testRepo, "refs/for/foo%base=" + rBase.getCommit().name(), false, false);
+    assertThat(pr.getMessages()).contains("changes: new: 1, refs: 1, done");
+
+    assertTwoChangesWithSameRevision(r);
+  }
+
+  private void assertTwoChangesWithSameRevision(PushOneCommit.Result result)
+      throws Exception {
+    List<ChangeInfo> changes = query(result.getCommit().name());
+    assertThat(changes).hasSize(2);
+    ChangeInfo c1 = get(changes.get(0).id);
+    ChangeInfo c2 = get(changes.get(1).id);
+    assertThat(c1.project).isEqualTo(c2.project);
+    assertThat(c1.branch).isNotEqualTo(c2.branch);
+    assertThat(c1.changeId).isEqualTo(c2.changeId);
+    assertThat(c1.currentRevision).isEqualTo(c2.currentRevision);
+  }
+
+  @Test
+  public void pushAFewChanges() throws Exception {
     int n = 10;
     String r = "refs/for/master";
     ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
-    List<RevCommit> commits = new ArrayList<>(n);
-
-    // Create and push N changes.
-    for (int i = 1; i <= n; i++) {
-      TestRepository<?>.CommitBuilder cb = testRepo.branch("HEAD").commit()
-          .message("Change " + i).insertChangeId();
-      if (!commits.isEmpty()) {
-        cb.parent(commits.get(commits.size() - 1));
-      }
-      RevCommit c = cb.create();
-      testRepo.getRevWalk().parseBody(c);
-      commits.add(c);
-    }
-    assertPushOk(pushHead(testRepo, r, false), r);
+    List<RevCommit> commits = createChanges(n, r);
 
     // Check that a change was created for each.
     for (RevCommit c : commits) {
@@ -582,21 +637,7 @@
           .isEqualTo(c.getShortMessage());
     }
 
-    // Amend each change.
-    testRepo.reset(initialHead);
-    List<RevCommit> commits2 = new ArrayList<>(n);
-    for (RevCommit c : commits) {
-      TestRepository<?>.CommitBuilder cb = testRepo.branch("HEAD").commit()
-          .message(c.getShortMessage() + "v2")
-          .insertChangeId(getChangeId(c).substring(1));
-      if (!commits2.isEmpty()) {
-        cb.parent(commits2.get(commits2.size() - 1));
-      }
-      RevCommit c2 = cb.create();
-      testRepo.getRevWalk().parseBody(c2);
-      commits2.add(c2);
-    }
-    assertPushOk(pushHead(testRepo, r, false), r);
+    List<RevCommit> commits2 = amendChanges(initialHead, commits, r);
 
     // Check that there are correct patch sets.
     for (int i = 0; i < n; i++) {
@@ -616,7 +657,7 @@
   }
 
   @Test
-  public void testCantAutoCloseChangeAlreadyMergedToBranch() throws Exception {
+  public void cantAutoCloseChangeAlreadyMergedToBranch() throws Exception {
     PushOneCommit.Result r1 = createChange();
     Change.Id id1 = r1.getChange().getId();
     PushOneCommit.Result r2 = createChange();
@@ -645,7 +686,7 @@
   }
 
   @Test
-  public void testAccidentallyPushNewPatchSetDirectlyToBranchAndRecoverByPushingToRefsChanges()
+  public void accidentallyPushNewPatchSetDirectlyToBranchAndRecoverByPushingToRefsChanges()
       throws Exception {
     Change.Id id = accidentallyPushNewPatchSetDirectlyToBranch();
     ChangeData cd = byChangeId(id);
@@ -665,7 +706,7 @@
   }
 
   @Test
-  public void testAccidentallyPushNewPatchSetDirectlyToBranchAndCantRecoverByPushingToRefsFor()
+  public void accidentallyPushNewPatchSetDirectlyToBranchAndCantRecoverByPushingToRefsFor()
       throws Exception {
     Change.Id id = accidentallyPushNewPatchSetDirectlyToBranch();
     ChangeData cd = byChangeId(id);
@@ -712,6 +753,141 @@
     return c.getId();
   }
 
+  @Test
+  public void pushWithEmailInFooter() throws Exception {
+    pushWithReviewerInFooter(user.emailAddress.toString(), user);
+  }
+
+  @Test
+  public void pushWithNameInFooter() throws Exception {
+    pushWithReviewerInFooter(user.fullName, user);
+  }
+
+  @Test
+  public void pushWithEmailInFooterNotFound() throws Exception {
+    pushWithReviewerInFooter(
+        new Address("No Body", "notarealuser@example.com").toString(),
+        null);
+  }
+
+  @Test
+  public void pushWithNameInFooterNotFound() throws Exception {
+    pushWithReviewerInFooter("Notauser", null);
+  }
+
+  @Test
+  // TODO(dborowitz): This is to exercise a specific case in the database search
+  // path. Once the account index becomes obligatory this method can be removed.
+  @GerritConfig(name = "index.testDisable", value = "accounts")
+  public void pushWithNameInFooterNotFoundWithDbSearch() throws Exception {
+    pushWithReviewerInFooter("Notauser", null);
+  }
+
+  @Test
+  public void pushNewPatchsetOverridingStickyLabel() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    LabelType codeReview = Util.codeReview();
+    codeReview.setCopyMaxScore(true);
+    cfg.getLabelSections().put(codeReview.getName(), codeReview);
+    saveProjectConfig(cfg);
+
+    PushOneCommit.Result r = pushTo("refs/for/master%l=Code-Review+2");
+    r.assertOkStatus();
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent", r.getChangeId());
+    r = push.to("refs/for/master%l=Code-Review+1");
+    r.assertOkStatus();
+  }
+
+  private void pushWithReviewerInFooter(String nameEmail,
+      TestAccount expectedReviewer) throws Exception {
+    int n = 5;
+    String r = "refs/for/master";
+    ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
+    List<RevCommit> commits =
+        createChanges(n, r, ImmutableList.of("Acked-By: " + nameEmail));
+    for (int i = 0; i < n; i++) {
+      RevCommit c = commits.get(i);
+      ChangeData cd = byCommit(c);
+      String name = "reviewers for " + (i + 1);
+      if (expectedReviewer != null) {
+        assertThat(cd.reviewers().all()).named(name)
+            .containsExactly(expectedReviewer.getId());
+        gApi.changes()
+            .id(cd.getId().get())
+            .reviewer(expectedReviewer.getId().toString())
+            .remove();
+      }
+      assertThat(byCommit(c).reviewers().all()).named(name).isEmpty();
+    }
+
+    List<RevCommit> commits2 = amendChanges(initialHead, commits, r);
+    for (int i = 0; i < n; i++) {
+      RevCommit c = commits2.get(i);
+      ChangeData cd = byCommit(c);
+      String name = "reviewers for " + (i + 1);
+      if (expectedReviewer != null) {
+        assertThat(cd.reviewers().all()).named(name)
+            .containsExactly(expectedReviewer.getId());
+      } else {
+        assertThat(byCommit(c).reviewers().all()).named(name).isEmpty();
+      }
+    }
+  }
+
+  private List<RevCommit> createChanges(int n, String refsFor)
+      throws Exception {
+    return createChanges(n, refsFor, ImmutableList.<String>of());
+  }
+
+  private List<RevCommit> createChanges(int n, String refsFor,
+      List<String> footerLines) throws Exception {
+    List<RevCommit> commits = new ArrayList<>(n);
+    for (int i = 1; i <= n; i++) {
+      String msg = "Change " + i;
+      if (!footerLines.isEmpty()) {
+        StringBuilder sb = new StringBuilder(msg).append("\n\n");
+        for (String line : footerLines) {
+          sb.append(line).append('\n');
+        }
+        msg = sb.toString();
+      }
+      TestRepository<?>.CommitBuilder cb = testRepo.branch("HEAD").commit()
+          .message(msg).insertChangeId();
+      if (!commits.isEmpty()) {
+        cb.parent(commits.get(commits.size() - 1));
+      }
+      RevCommit c = cb.create();
+      testRepo.getRevWalk().parseBody(c);
+      commits.add(c);
+    }
+    assertPushOk(pushHead(testRepo, refsFor, false), refsFor);
+    return commits;
+  }
+
+  private List<RevCommit> amendChanges(ObjectId initialHead,
+      List<RevCommit> origCommits, String refsFor) throws Exception {
+    testRepo.reset(initialHead);
+    List<RevCommit> newCommits = new ArrayList<>(origCommits.size());
+    for (RevCommit c : origCommits) {
+      String msg = c.getShortMessage() + "v2";
+      if (!c.getShortMessage().equals(c.getFullMessage())) {
+        msg = msg + c.getFullMessage().substring(c.getShortMessage().length());
+      }
+      TestRepository<?>.CommitBuilder cb = testRepo.branch("HEAD").commit()
+          .message(msg);
+      if (!newCommits.isEmpty()) {
+        cb.parent(origCommits.get(newCommits.size() - 1));
+      }
+      RevCommit c2 = cb.create();
+      testRepo.getRevWalk().parseBody(c2);
+      newCommits.add(c2);
+    }
+    assertPushOk(pushHead(testRepo, refsFor, false), refsFor);
+    return newCommits;
+  }
+
   private static Map<Integer, String> getPatchSetRevisions(ChangeData cd)
       throws Exception {
     Map<Integer, String> revisions = new HashMap<>();
@@ -732,8 +908,4 @@
     assertThat(cds).named("change " + id).hasSize(1);
     return cds.get(0);
   }
-
-  private static String getChangeId(RevCommit c) {
-    return getOnlyElement(c.getFooterLines(CHANGE_ID));
-  }
 }
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 721628d..28f7ff8 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
@@ -21,6 +21,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -40,14 +41,52 @@
 import java.util.concurrent.atomic.AtomicInteger;
 
 public abstract class AbstractSubmoduleSubscription extends AbstractDaemonTest {
+
+  protected SubmitType getSubmitType() {
+    return cfg.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
+  }
+
+  protected static Config submitByMergeAlways() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "submitWholeTopic", true);
+    cfg.setEnum("project", null, "submitType", SubmitType.MERGE_ALWAYS);
+    return cfg;
+  }
+
+  protected static Config submitByMergeIfNecessary() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "submitWholeTopic", true);
+    cfg.setEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
+    return cfg;
+  }
+
+  protected static Config submitByCherryPickConifg() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "submitWholeTopic", true);
+    cfg.setEnum("project", null, "submitType", SubmitType.CHERRY_PICK);
+    return cfg;
+  }
+
+  protected static Config submitByRebaseConifg() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "submitWholeTopic", true);
+    cfg.setEnum("project", null, "submitType", SubmitType.REBASE_IF_NECESSARY);
+    return cfg;
+  }
+
   protected TestRepository<?> createProjectWithPush(String name,
-      @Nullable Project.NameKey parent) throws Exception {
-    Project.NameKey project = createProject(name, parent);
+      @Nullable Project.NameKey parent, SubmitType submitType) throws Exception {
+    Project.NameKey project = createProject(name, parent, submitType);
     grant(Permission.PUSH, project, "refs/heads/*");
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/*");
     return cloneProject(project);
   }
 
+  protected TestRepository<?> createProjectWithPush(String name,
+      @Nullable Project.NameKey parent) throws Exception {
+    return createProjectWithPush(name, parent, getSubmitType());
+  }
+
   protected TestRepository<?> createProjectWithPush(String name)
       throws Exception {
     return createProjectWithPush(name, null);
@@ -56,15 +95,16 @@
   private static AtomicInteger contentCounter = new AtomicInteger(0);
 
   protected ObjectId pushChangeTo(TestRepository<?> repo, String ref,
-      String message, String topic) throws Exception {
+      String file, String content, String message, String topic)
+      throws Exception {
     ObjectId ret = repo.branch("HEAD").commit().insertChangeId()
       .message(message)
-      .add("a.txt", "a contents: " + contentCounter.incrementAndGet())
+      .add(file, content)
       .create();
 
     String pushedRef = ref;
     if (!topic.isEmpty()) {
-      pushedRef += "/" + topic;
+      pushedRef += "/" + name(topic);
     }
     String refspec = "HEAD:" + pushedRef;
 
@@ -79,23 +119,36 @@
     return ret;
   }
 
+  protected ObjectId pushChangeTo(TestRepository<?> repo, String ref,
+      String message, String topic) throws Exception {
+    return pushChangeTo(repo, ref, "a.txt",
+        "a contents: " + contentCounter.incrementAndGet(), message, topic);
+  }
+
   protected ObjectId pushChangeTo(TestRepository<?> repo, String branch)
       throws Exception {
     return pushChangeTo(repo, "refs/heads/" + branch, "some change", "");
   }
 
-  protected void allowSubmoduleSubscription(String submodule, String subBranch,
-      String superproject, String superBranch) throws Exception {
+  protected void allowSubmoduleSubscription(String submodule,
+      String subBranch, String superproject, String superBranch, boolean match)
+      throws Exception {
     Project.NameKey sub = new Project.NameKey(name(submodule));
     Project.NameKey superName = new Project.NameKey(name(superproject));
     try (MetaDataUpdate md = metaDataUpdateFactory.create(sub)) {
       md.setMessage("Added superproject subscription");
       ProjectConfig pc = ProjectConfig.read(md);
       SubscribeSection s = new SubscribeSection(superName);
+      String refspec;
       if (superBranch == null) {
-        s.addRefSpec(subBranch);
+        refspec = subBranch;
       } else {
-        s.addRefSpec(subBranch + ":" + superBranch);
+        refspec = subBranch + ":" + superBranch;
+      }
+      if (match) {
+        s.addMatchingRefSpec(refspec);
+      } else {
+        s.addMultiMatchRefSpec(refspec);
       }
       pc.addSubscribeSection(s);
       ObjectId oldId = pc.getRevision();
@@ -105,6 +158,13 @@
     }
   }
 
+  protected void allowMatchingSubmoduleSubscription(String submodule,
+      String subBranch, String superproject, String superBranch)
+      throws Exception {
+    allowSubmoduleSubscription(submodule, subBranch, superproject,
+        superBranch, true);
+  }
+
   protected void createSubmoduleSubscription(TestRepository<?> repo, String branch,
       String subscribeToRepo, String subscribeToBranch) throws Exception {
     Config config = new Config();
@@ -135,16 +195,25 @@
 
   protected void prepareSubmoduleConfigEntry(Config config,
       String subscribeToRepo, String subscribeToBranch) {
+    // The submodule subscription module checks for gerrit.canonicalWebUrl to
+    // detect if it's configured for automatic updates. It doesn't matter if
+    // it serves from that URL.
+    prepareSubmoduleConfigEntry(config, subscribeToRepo, subscribeToRepo, subscribeToBranch);
+  }
+
+  protected void prepareSubmoduleConfigEntry(Config config,
+      String subscribeToRepo, String subscribeToRepoPath, String subscribeToBranch) {
     subscribeToRepo = name(subscribeToRepo);
+    subscribeToRepoPath = name(subscribeToRepoPath);
     // The submodule subscription module checks for gerrit.canonicalWebUrl to
     // detect if it's configured for automatic updates. It doesn't matter if
     // it serves from that URL.
     String url = cfg.getString("gerrit", null, "canonicalWebUrl") + "/"
         + subscribeToRepo;
-    config.setString("submodule", subscribeToRepo, "path", subscribeToRepo);
-    config.setString("submodule", subscribeToRepo, "url", url);
+    config.setString("submodule", subscribeToRepoPath, "path", subscribeToRepoPath);
+    config.setString("submodule", subscribeToRepoPath, "url", url);
     if (subscribeToBranch != null) {
-      config.setString("submodule", subscribeToRepo, "branch", subscribeToBranch);
+      config.setString("submodule", subscribeToRepoPath, "branch", subscribeToBranch);
     }
   }
 
@@ -161,6 +230,27 @@
   }
 
   protected void expectToHaveSubmoduleState(TestRepository<?> repo,
+      String branch, String submodule, TestRepository<?> subRepo,
+      String subBranch) throws Exception {
+
+    submodule = name(submodule);
+    ObjectId commitId = repo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/" + branch).getObjectId();
+
+    ObjectId subHead = subRepo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/" + subBranch).getObjectId();
+
+    RevWalk rw = repo.getRevWalk();
+    RevCommit c = rw.parseCommit(commitId);
+    rw.parseBody(c.getTree());
+
+    RevTree tree = c.getTree();
+    RevObject actualId = repo.get(tree, submodule);
+
+    assertThat(actualId).isEqualTo(subHead);
+  }
+
+  protected void expectToHaveSubmoduleState(TestRepository<?> repo,
       String branch, String submodule, ObjectId expectedId) throws Exception {
 
     submodule = name(submodule);
@@ -176,4 +266,69 @@
 
     assertThat(actualId).isEqualTo(expectedId);
   }
+
+  protected void deleteAllSubscriptions(TestRepository<?> repo, String branch)
+      throws Exception {
+    repo.git().fetch().setRemote("origin").call();
+    repo.reset("refs/remotes/origin/" + branch);
+
+    ObjectId expectedId = repo.branch("HEAD").commit().insertChangeId()
+        .message("delete contents in .gitmodules")
+        .add(".gitmodules", "") // Just remove the contents of the file!
+        .create();
+    repo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/" + branch)).call();
+
+    ObjectId actualId = repo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/master").getObjectId();
+    assertThat(actualId).isEqualTo(expectedId);
+  }
+
+  protected void deleteGitModulesFile(TestRepository<?> repo, String branch)
+      throws Exception {
+    repo.git().fetch().setRemote("origin").call();
+    repo.reset("refs/remotes/origin/" + branch);
+
+    ObjectId expectedId = repo.branch("HEAD").commit().insertChangeId()
+        .message("delete .gitmodules")
+        .rm(".gitmodules")
+        .create();
+    repo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/" + branch)).call();
+
+    ObjectId actualId = repo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/master").getObjectId();
+    assertThat(actualId).isEqualTo(expectedId);
+  }
+
+  protected boolean hasSubmodule(TestRepository<?> repo, String branch,
+      String submodule) throws Exception {
+
+    submodule = name(submodule);
+    ObjectId commitId = repo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/" + branch).getObjectId();
+
+    RevWalk rw = repo.getRevWalk();
+    RevCommit c = rw.parseCommit(commitId);
+    rw.parseBody(c.getTree());
+
+    RevTree tree = c.getTree();
+    try {
+      repo.get(tree, submodule);
+      return true;
+    } catch (AssertionError e) {
+      return false;
+    }
+  }
+
+  protected void expectToHaveCommitMessage(TestRepository<?> repo,
+      String branch, String expectedMessage) throws Exception {
+
+    ObjectId commitId = repo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/" + branch).getObjectId();
+
+    RevWalk rw = repo.getRevWalk();
+    RevCommit c = rw.parseCommit(commitId);
+    assertThat(c.getFullMessage()).isEqualTo(expectedMessage);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index 980593f..848b428 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -93,13 +94,13 @@
     grant(Permission.SUBMIT, project, "refs/for/refs/meta/config");
 
     git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
-    testRepo.reset("refs/meta/config");
+    testRepo.reset(RefNames.REFS_CONFIG);
 
     PushOneCommit.Result r = pushTo("refs/for/refs/meta/config%submit");
     r.assertOkStatus();
     r.assertChange(Change.Status.MERGED, null, admin);
     assertSubmitApproval(r.getPatchSetId());
-    assertCommit(project, "refs/meta/config");
+    assertCommit(project, RefNames.REFS_CONFIG);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java
index 6a4454f..09e498f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java
@@ -367,4 +367,27 @@
 
     assertThat(res).containsExactlyElementsIn(expected);
   }
+
+  @Test
+  public void testWithOverlyDeepRelativeURI() throws Exception {
+    Project.NameKey p1 = createProject("nested/a");
+    Config cfg = new Config();
+    cfg.fromText(""
+        + "[submodule \"a\"]\n"
+        + "path = a\n"
+        + "url = ../../" + p1.get() + "\n"
+        + "branch = master\n");
+
+    Branch.NameKey targetBranch = new Branch.NameKey(
+        new Project.NameKey("nested/project"), "master");
+
+    Set<SubmoduleSubscription> res = new SubmoduleSectionParser(
+        cfg, THIS_SERVER, targetBranch).parseAllSections();
+
+    Set<SubmoduleSubscription> expected = Sets.newHashSet(
+        new SubmoduleSubscription(targetBranch, new Branch.NameKey(
+            p1, "master"), "a"));
+
+    assertThat(res).containsExactlyElementsIn(expected);
+  }
 }
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 f535759..6684e85 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
@@ -26,7 +26,6 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RefSpec;
@@ -42,7 +41,21 @@
 
   @Test
   @GerritConfig(name = "submodule.enableSuperProjectSubscriptions", value = "false")
-  public void testSubscriptionWithoutServerSetting() throws Exception {
+  public void testSubscriptionWithoutGlobalServerSetting() 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");
+
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
+    pushChangeTo(subRepo, "master");
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isFalse();
+  }
+
+  @Test
+  public void testSubscriptionWithoutSpecificSubscription() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
@@ -57,13 +70,15 @@
   public void testSubscriptionToEmptyRepo() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     createSubmoduleSubscription(superRepo, "master",
         "subscribed-to-project", "master");
     pushChangeTo(subRepo, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isTrue();
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEAD);
   }
@@ -72,13 +87,15 @@
   public void testSubscriptionToExistingRepo() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
     createSubmoduleSubscription(superRepo, "master",
         "subscribed-to-project", "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isTrue();
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEAD);
   }
@@ -87,8 +104,8 @@
   public void testSubscriptionWildcardACLForSingleBranch() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    // master is allowed to be subscribed to any superprojects branch:
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    // master is allowed to be subscribed to master branch only:
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", null);
     // create 'branch':
     pushChangeTo(superRepo, "branch");
@@ -103,14 +120,14 @@
 
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEAD);
-    expectToHaveSubmoduleState(superRepo, "branch",
-        "subscribed-to-project", subHEAD);
+    assertThat(hasSubmodule(superRepo, "branch",
+        "subscribed-to-project")).isFalse();
   }
 
   @Test
   public void testSubscriptionWildcardACLForMissingProject() throws Exception {
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
         "not-existing-super-project", "refs/heads/*");
     pushChangeTo(subRepo, "master");
   }
@@ -119,7 +136,7 @@
   public void testSubscriptionWildcardACLForMissingBranch() throws Exception {
     createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
         "super-project", "refs/heads/*");
     pushChangeTo(subRepo, "foo");
   }
@@ -128,7 +145,7 @@
   public void testSubscriptionWildcardACLForMissingGitmodules() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
         "super-project", "refs/heads/*");
     pushChangeTo(superRepo, "master");
     pushChangeTo(subRepo, "master");
@@ -139,7 +156,7 @@
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
     // any branch is allowed to be subscribed to the same superprojects branch:
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
         "super-project", "refs/heads/*");
 
     // create 'branch' in both repos:
@@ -172,11 +189,52 @@
   }
 
   @Test
+  public void testSubscriptionWildcardACLForManyBranches() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    // Any branch is allowed to be subscribed to any superproject branch:
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+        "super-project", null, false);
+    pushChangeTo(superRepo, "branch");
+    pushChangeTo(subRepo, "another-branch");
+    createSubmoduleSubscription(superRepo, "branch",
+        "subscribed-to-project", "another-branch");
+    ObjectId subHEAD = pushChangeTo(subRepo, "another-branch");
+    expectToHaveSubmoduleState(superRepo, "branch",
+        "subscribed-to-project", subHEAD);
+  }
+
+  @Test
+  public void testSubscriptionWildcardACLOneToManyBranches() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    // Any branch is allowed to be subscribed to any superproject branch:
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/*", false);
+    pushChangeTo(superRepo, "branch");
+    createSubmoduleSubscription(superRepo, "branch",
+        "subscribed-to-project", "master");
+    ObjectId subHEAD = pushChangeTo(subRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "branch",
+        "subscribed-to-project", subHEAD);
+
+    createSubmoduleSubscription(superRepo, "branch",
+        "subscribed-to-project", "branch");
+    pushChangeTo(subRepo, "branch");
+
+    // no change expected, as only master is subscribed:
+    expectToHaveSubmoduleState(superRepo, "branch",
+        "subscribed-to-project", subHEAD);
+  }
+
+  @Test
   @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "false")
   public void testSubmoduleShortCommitMessage() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -199,10 +257,11 @@
   }
 
   @Test
-  public void testSubmoduleCommitMessage() throws Exception {
+  @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "SUBJECT_ONLY")
+  public void testSubmoduleSubjectCommitMessage() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -212,28 +271,53 @@
 
     // The first update doesn't include the rev log
     RevWalk rw = subRepo.getRevWalk();
-    RevCommit subCommitMsg = rw.parseCommit(subHEAD);
     expectToHaveCommitMessage(superRepo, "master",
         "Update git submodules\n\n" +
-        "Project: " + name("subscribed-to-project")
-            + " master " + subHEAD.name() + "\n\n");
+            "* Update " + name("subscribed-to-project") + " from branch 'master'");
 
     // The next commit should generate only its commit message,
     // omitting previous commit logs
     subHEAD = pushChangeTo(subRepo, "master");
-    subCommitMsg = rw.parseCommit(subHEAD);
+    RevCommit subCommitMsg = rw.parseCommit(subHEAD);
     expectToHaveCommitMessage(superRepo, "master",
         "Update git submodules\n\n" +
-        "Project: " + name("subscribed-to-project")
-            + " master " + subHEAD.name() + "\n\n" +
-        subCommitMsg.getFullMessage() + "\n\n");
+            "* Update " + name("subscribed-to-project") + " from branch 'master'"
+            + "\n  - " + subCommitMsg.getShortMessage());
+  }
+
+  @Test
+  public void testSubmoduleCommitMessage() 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");
+    ObjectId subHEAD = pushChangeTo(subRepo, "master");
+
+    // The first update doesn't include the rev log
+    RevWalk rw = subRepo.getRevWalk();
+    expectToHaveCommitMessage(superRepo, "master",
+        "Update git submodules\n\n" +
+            "* Update " + name("subscribed-to-project") + " from branch 'master'");
+
+    // The next commit should generate only its commit message,
+    // omitting previous commit logs
+    subHEAD = pushChangeTo(subRepo, "master");
+    RevCommit subCommitMsg = rw.parseCommit(subHEAD);
+    expectToHaveCommitMessage(superRepo, "master",
+        "Update git submodules\n\n" +
+            "* Update " + name("subscribed-to-project") + " from branch 'master'"
+             + "\n  - " + subCommitMsg.getFullMessage().replace("\n", "\n    "));
   }
 
   @Test
   public void testSubscriptionUnsubscribe() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -260,7 +344,7 @@
       throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -286,7 +370,7 @@
   public void testSubscriptionToDifferentBranches() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/foo",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/foo",
         "super-project", "refs/heads/master");
 
     createSubmoduleSubscription(superRepo, "master",
@@ -299,12 +383,12 @@
   }
 
   @Test
-  public void testCircularSubscriptionIsDetected() throws Exception {
+  public void testBranchCircularSubscription() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
-    allowSubmoduleSubscription("super-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("super-project", "refs/heads/master",
         "subscribed-to-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -322,6 +406,37 @@
         "subscribed-to-project")).isFalse();
   }
 
+  @Test
+  public void testProjectCircularSubscription() 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");
+    allowMatchingSubmoduleSubscription("super-project", "refs/heads/dev",
+        "subscribed-to-project", "refs/heads/dev");
+
+    pushChangeTo(subRepo, "master");
+    pushChangeTo(superRepo, "master");
+    pushChangeTo(subRepo, "dev");
+    pushChangeTo(superRepo, "dev");
+
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
+    createSubmoduleSubscription(subRepo, "dev", "super-project", "dev");
+
+    ObjectId subMasterHead = pushChangeTo(subRepo, "master");
+    ObjectId superDevHead = pushChangeTo(superRepo, "dev");
+
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isTrue();
+    assertThat(hasSubmodule(subRepo, "dev",
+        "super-project")).isTrue();
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subMasterHead);
+    expectToHaveSubmoduleState(subRepo, "dev",
+        "super-project", superDevHead);
+  }
 
   @Test
   public void testSubscriptionFailOnMissingACL() throws Exception {
@@ -340,7 +455,7 @@
   public void testSubscriptionFailOnWrongProjectACL() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "wrong-super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -355,7 +470,7 @@
   public void testSubscriptionFailOnWrongBranchACL() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/wrong-branch");
 
     pushChangeTo(subRepo, "master");
@@ -374,7 +489,7 @@
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project",
         new Project.NameKey(name("config-repo2")));
-    allowSubmoduleSubscription("config-repo", "refs/heads/*",
+    allowMatchingSubmoduleSubscription("config-repo", "refs/heads/*",
         "super-project", "refs/heads/*");
 
     pushChangeTo(subRepo, "master");
@@ -389,7 +504,7 @@
   public void testAllowedButNotSubscribed() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -416,7 +531,7 @@
     TestRepository<?> subRepo = createProjectWithPush(
         "nested/subscribed-to-project");
     // master is allowed to be subscribed to any superprojects branch:
-    allowSubmoduleSubscription("nested/subscribed-to-project",
+    allowMatchingSubmoduleSubscription("nested/subscribed-to-project",
         "refs/heads/master", "super-project", null);
 
     pushChangeTo(subRepo, "master");
@@ -428,68 +543,4 @@
     expectToHaveSubmoduleState(superRepo, "master",
         "nested/subscribed-to-project", subHEAD);
   }
-
-  private void deleteAllSubscriptions(TestRepository<?> repo, String branch)
-      throws Exception {
-    repo.git().fetch().setRemote("origin").call();
-    repo.reset("refs/remotes/origin/" + branch);
-
-    ObjectId expectedId = repo.branch("HEAD").commit().insertChangeId()
-      .message("delete contents in .gitmodules")
-      .add(".gitmodules", "") // Just remove the contents of the file!
-      .create();
-    repo.git().push().setRemote("origin").setRefSpecs(
-      new RefSpec("HEAD:refs/heads/" + branch)).call();
-
-    ObjectId actualId = repo.git().fetch().setRemote("origin").call()
-      .getAdvertisedRef("refs/heads/master").getObjectId();
-    assertThat(actualId).isEqualTo(expectedId);
-  }
-
-  private void deleteGitModulesFile(TestRepository<?> repo, String branch)
-      throws Exception {
-    repo.git().fetch().setRemote("origin").call();
-    repo.reset("refs/remotes/origin/" + branch);
-
-    ObjectId expectedId = repo.branch("HEAD").commit().insertChangeId()
-      .message("delete .gitmodules")
-      .rm(".gitmodules")
-      .create();
-    repo.git().push().setRemote("origin").setRefSpecs(
-      new RefSpec("HEAD:refs/heads/" + branch)).call();
-
-    ObjectId actualId = repo.git().fetch().setRemote("origin").call()
-      .getAdvertisedRef("refs/heads/master").getObjectId();
-    assertThat(actualId).isEqualTo(expectedId);
-  }
-
-  private boolean hasSubmodule(TestRepository<?> repo, String branch,
-      String submodule) throws Exception {
-
-    ObjectId commitId = repo.git().fetch().setRemote("origin").call()
-        .getAdvertisedRef("refs/heads/" + branch).getObjectId();
-
-    RevWalk rw = repo.getRevWalk();
-    RevCommit c = rw.parseCommit(commitId);
-    rw.parseBody(c.getTree());
-
-    RevTree tree = c.getTree();
-    try {
-      repo.get(tree, submodule);
-      return true;
-    } catch (AssertionError e) {
-      return false;
-    }
-  }
-
-  private void expectToHaveCommitMessage(TestRepository<?> repo,
-      String branch, String expectedMessage) throws Exception {
-
-    ObjectId commitId = repo.git().fetch().setRemote("origin").call()
-        .getAdvertisedRef("refs/heads/" + branch).getObjectId();
-
-    RevWalk rw = repo.getRevWalk();
-    RevCommit c = rw.parseCommit(commitId);
-    assertThat(c.getFullMessage()).isEqualTo(expectedMessage);
-  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 0a067e9..0ff3af5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.acceptance.git;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.GitUtil.getChangeId;
 
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.testutil.ConfigSuite;
 
 import org.eclipse.jgit.junit.TestRepository;
@@ -33,15 +35,30 @@
   extends AbstractSubmoduleSubscription {
 
   @ConfigSuite.Default
-  public static Config submitWholeTopicEnabled() {
-    return submitWholeTopicEnabledConfig();
+  public static Config mergeIfNecessary() {
+    return submitByMergeIfNecessary();
+  }
+
+  @ConfigSuite.Config
+  public static Config mergeAlways() {
+    return submitByMergeAlways();
+  }
+
+  @ConfigSuite.Config
+  public static Config cherryPick() {
+    return submitByCherryPickConifg();
+  }
+
+  @ConfigSuite.Config
+  public static Config rebase() {
+    return submitByRebaseConifg();
   }
 
   @Test
   public void testSubscriptionUpdateOfManyChanges() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
@@ -96,7 +113,7 @@
   public void testSubscriptionUpdateIncludingChangeInSuperproject() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
@@ -164,11 +181,11 @@
     TestRepository<?> sub2 = createProjectWithPush("sub2");
     TestRepository<?> sub3 = createProjectWithPush("sub3");
 
-    allowSubmoduleSubscription("sub1", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("sub1", "refs/heads/master",
         "super-project", "refs/heads/master");
-    allowSubmoduleSubscription("sub2", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("sub2", "refs/heads/master",
         "super-project", "refs/heads/master");
-    allowSubmoduleSubscription("sub3", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("sub3", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     Config config = new Config();
@@ -192,9 +209,9 @@
 
     gApi.changes().id(getChangeId(sub1, sub1Id).get()).current().submit();
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1Id);
-    expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2Id);
-    expectToHaveSubmoduleState(superRepo, "master", "sub3", sub3Id);
+    expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1, "master");
+    expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2, "master");
+    expectToHaveSubmoduleState(superRepo, "master", "sub3", sub3, "master");
 
     superRepo.git().fetch().setRemote("origin").call()
       .getAdvertisedRef("refs/heads/master").getObjectId();
@@ -204,4 +221,216 @@
         .that(superRepo.getRepository().resolve("origin/master^"))
         .isEqualTo(superPreviousId);
   }
+
+  @Test
+  public void testDifferentPaths() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> sub = createProjectWithPush("sub");
+
+    allowMatchingSubmoduleSubscription("sub", "refs/heads/master",
+        "super-project", "refs/heads/master");
+
+    Config config = new Config();
+    prepareSubmoduleConfigEntry(config, "sub", "master");
+    prepareSubmoduleConfigEntry(config, "sub", "sub-copy", "master");
+    pushSubmoduleConfig(superRepo, "master", config);
+
+    ObjectId superPreviousId = pushChangeTo(superRepo, "master");
+
+    ObjectId subId = pushChangeTo(sub, "refs/for/master", "some message", "");
+
+    approve(getChangeId(sub, subId).get());
+
+    gApi.changes().id(getChangeId(sub, subId).get()).current().submit();
+
+    expectToHaveSubmoduleState(superRepo, "master", "sub", sub, "master");
+    expectToHaveSubmoduleState(superRepo, "master", "sub-copy", sub, "master");
+
+    superRepo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/master").getObjectId();
+
+    assertWithMessage("submodule subscription update "
+        + "should have made one commit")
+        .that(superRepo.getRepository().resolve("origin/master^"))
+        .isEqualTo(superPreviousId);
+  }
+
+  @Test
+  public void testNonSubmoduleInSameTopic() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> sub = createProjectWithPush("sub");
+    TestRepository<?> standAlone = createProjectWithPush("standalone");
+
+    allowMatchingSubmoduleSubscription("sub", "refs/heads/master",
+        "super-project", "refs/heads/master");
+
+    createSubmoduleSubscription(superRepo, "master", "sub", "master");
+
+    ObjectId superPreviousId = pushChangeTo(superRepo, "master");
+
+    ObjectId subId =
+        pushChangeTo(sub, "refs/for/master", "some message", "same-topic");
+    ObjectId standAloneId =
+        pushChangeTo(standAlone, "refs/for/master", "some message",
+            "same-topic");
+
+    String subChangeId = getChangeId(sub, subId).get();
+    String standAloneChangeId = getChangeId(standAlone, standAloneId).get();
+    approve(subChangeId);
+    approve(standAloneChangeId);
+
+    gApi.changes().id(subChangeId).current().submit();
+
+    expectToHaveSubmoduleState(superRepo, "master", "sub", sub, "master");
+
+    ChangeStatus status = gApi.changes().id(standAloneChangeId).info().status;
+    assertThat(status).isEqualTo(ChangeStatus.MERGED);
+
+    superRepo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/master").getObjectId();
+
+    assertWithMessage("submodule subscription update "
+        + "should have made one commit")
+        .that(superRepo.getRepository().resolve("origin/master^"))
+        .isEqualTo(superPreviousId);
+  }
+
+  @Test
+  public void testRecursiveSubmodules() throws Exception {
+    TestRepository<?> topRepo = createProjectWithPush("top-project");
+    TestRepository<?> midRepo = createProjectWithPush("mid-project");
+    TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
+
+    allowMatchingSubmoduleSubscription("mid-project", "refs/heads/master",
+        "top-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription("bottom-project", "refs/heads/master",
+        "mid-project", "refs/heads/master");
+
+    createSubmoduleSubscription(topRepo, "master", "mid-project", "master");
+    createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
+
+    ObjectId bottomHead =
+        pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
+    ObjectId topHead =
+        pushChangeTo(topRepo, "refs/for/master", "some message", "same-topic");
+
+    String id1 = getChangeId(bottomRepo, bottomHead).get();
+    String id2 = getChangeId(topRepo, topHead).get();
+
+    gApi.changes().id(id1).current().review(ReviewInput.approve());
+    gApi.changes().id(id2).current().review(ReviewInput.approve());
+
+    gApi.changes().id(id1).current().submit();
+
+    expectToHaveSubmoduleState(midRepo, "master", "bottom-project", bottomRepo, "master");
+    expectToHaveSubmoduleState(topRepo, "master", "mid-project", midRepo, "master");
+  }
+
+  @Test
+  public void testTriangleSubmodules() throws Exception {
+    TestRepository<?> topRepo = createProjectWithPush("top-project");
+    TestRepository<?> midRepo = createProjectWithPush("mid-project");
+    TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
+
+    allowMatchingSubmoduleSubscription("mid-project", "refs/heads/master",
+        "top-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription("bottom-project", "refs/heads/master",
+        "mid-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription("bottom-project", "refs/heads/master",
+        "top-project", "refs/heads/master");
+
+    createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
+    Config config = new Config();
+    prepareSubmoduleConfigEntry(config, "bottom-project", "master");
+    prepareSubmoduleConfigEntry(config, "mid-project", "master");
+    pushSubmoduleConfig(topRepo, "master", config);
+
+    ObjectId bottomHead =
+        pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
+    ObjectId topHead =
+        pushChangeTo(topRepo, "refs/for/master", "some message", "same-topic");
+
+    String id1 = getChangeId(bottomRepo, bottomHead).get();
+    String id2 = getChangeId(topRepo, topHead).get();
+
+    gApi.changes().id(id1).current().review(ReviewInput.approve());
+    gApi.changes().id(id2).current().review(ReviewInput.approve());
+
+    gApi.changes().id(id1).current().submit();
+
+    expectToHaveSubmoduleState(midRepo, "master", "bottom-project", bottomRepo, "master");
+    expectToHaveSubmoduleState(topRepo, "master", "mid-project", midRepo, "master");
+    expectToHaveSubmoduleState(topRepo, "master", "bottom-project", bottomRepo, "master");
+  }
+
+  @Test
+  public void testBranchCircularSubscription() throws Exception {
+    TestRepository<?> topRepo = createProjectWithPush("top-project");
+    TestRepository<?> midRepo = createProjectWithPush("mid-project");
+    TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
+
+    createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
+    createSubmoduleSubscription(topRepo, "master", "mid-project", "master");
+    createSubmoduleSubscription(bottomRepo, "master", "top-project", "master");
+
+    allowMatchingSubmoduleSubscription("bottom-project", "refs/heads/master",
+        "mid-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription("mid-project", "refs/heads/master",
+        "top-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription("top-project", "refs/heads/master",
+        "bottom-project", "refs/heads/master");
+
+    ObjectId bottomMasterHead =
+        pushChangeTo(bottomRepo, "refs/for/master", "some message", "");
+    String changeId = getChangeId(bottomRepo, bottomMasterHead).get();
+
+    approve(changeId);
+
+    exception.expectMessage("Branch level circular subscriptions detected");
+    exception.expectMessage("top-project,refs/heads/master");
+    exception.expectMessage("mid-project,refs/heads/master");
+    exception.expectMessage("bottom-project,refs/heads/master");
+    gApi.changes().id(changeId).current().submit();
+
+    assertThat(hasSubmodule(midRepo, "master", "bottom-project")).isFalse();
+    assertThat(hasSubmodule(topRepo, "master", "mid-project")).isFalse();
+  }
+
+  @Test
+  public void testProjectCircularSubscriptionWholeTopic() 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");
+    allowMatchingSubmoduleSubscription("super-project", "refs/heads/dev",
+        "subscribed-to-project", "refs/heads/dev");
+
+    pushChangeTo(subRepo, "dev");
+    pushChangeTo(superRepo, "dev");
+
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
+    createSubmoduleSubscription(subRepo, "dev", "super-project", "dev");
+
+    ObjectId subMasterHead =
+        pushChangeTo(subRepo, "refs/for/master", "b.txt", "content b",
+            "some message", "same-topic");
+    ObjectId superDevHead =
+        pushChangeTo(superRepo, "refs/for/dev",
+            "some message", "same-topic");
+
+    approve(getChangeId(subRepo, subMasterHead).get());
+    approve(getChangeId(superRepo, superDevHead).get());
+
+    exception.expectMessage("Project level circular subscriptions detected");
+    exception.expectMessage("subscribed-to-project");
+    exception.expectMessage("super-project");
+    gApi.changes().id(getChangeId(subRepo, subMasterHead).get()).current()
+        .submit();
+
+    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project"))
+        .isFalse();
+    assertThat(hasSubmodule(subRepo, "dev", "super-project")).isFalse();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
index 010d216..fd2385b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.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 static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -29,7 +30,9 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.edit.ChangeEditModifier;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -139,8 +142,8 @@
   public void allRefsVisibleNoRefsMetaConfig() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     Util.allow(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
-    Util.allow(cfg, Permission.READ, admins, "refs/meta/config");
-    Util.doNotInherit(cfg, Permission.READ, "refs/meta/config");
+    Util.allow(cfg, Permission.READ, admins, RefNames.REFS_CONFIG);
+    Util.doNotInherit(cfg, Permission.READ, RefNames.REFS_CONFIG);
     saveProjectConfig(project, cfg);
 
     setApiUser(user);
@@ -159,7 +162,7 @@
   @Test
   public void allRefsVisibleWithRefsMetaConfig() throws Exception {
     allow(Permission.READ, REGISTERED_USERS, "refs/*");
-    allow(Permission.READ, REGISTERED_USERS, "refs/meta/config");
+    allow(Permission.READ, REGISTERED_USERS, RefNames.REFS_CONFIG);
 
     assertRefs(
         "HEAD",
@@ -169,7 +172,7 @@
         r2 + "meta",
         "refs/heads/branch",
         "refs/heads/master",
-        "refs/meta/config",
+        RefNames.REFS_CONFIG,
         "refs/tags/branch-tag",
         "refs/tags/master-tag");
   }
@@ -241,7 +244,6 @@
       setApiUser(admin);
       editModifier.createEdit(c, ps1);
       setApiUser(user);
-      editModifier.createEdit(c, ps1);
 
       assertRefs(
           // Change 1 is visible due to accessDatabase capability, even though
@@ -255,8 +257,7 @@
           // See comment in subsetOfBranchesVisibleNotIncludingHead.
           "refs/tags/master-tag",
           // All edits are visible due to accessDatabase capability.
-          "refs/users/00/1000000/edit-" + c1.get() + "/1",
-          "refs/users/01/1000001/edit-" + c1.get() + "/1");
+          "refs/users/00/1000000/edit-" + c1.get() + "/1");
     } finally {
       removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
     }
@@ -284,7 +285,7 @@
         r3 + "meta",
         "refs/heads/branch",
         "refs/heads/master",
-        "refs/meta/config",
+        RefNames.REFS_CONFIG,
         "refs/tags/branch-tag",
         "refs/tags/master-tag");
 
@@ -326,6 +327,27 @@
     }
   }
 
+  @Test
+  public void sequencesWithAccessDatabase() throws Exception {
+    assume().that(notesMigration.readChangeSequence()).isTrue();
+    try (Repository repo = repoManager.openRepository(allProjects)) {
+      setApiUser(user);
+      assertRefs(repo, newFilter(db, repo, allProjects), true);
+
+      allowGlobalCapabilities(
+          REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+      try {
+        setApiUser(user);
+        assertRefs(
+            repo, newFilter(db, repo, allProjects), true,
+            "refs/sequences/changes");
+      } finally {
+        removeGlobalCapabilities(
+            REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+      }
+    }
+  }
+
   /**
    * Assert that refs seen by a non-admin user match expected.
    *
@@ -372,4 +394,12 @@
   private ProjectControl projectControl() throws Exception {
     return projectControlFactory.controlFor(project, userProvider.get());
   }
+
+  private VisibleRefFilter newFilter(ReviewDb db, Repository repo,
+      Project.NameKey project) throws Exception {
+    return new VisibleRefFilter(
+        tagCache, notesFactory, null, repo,
+        projectControlFactory.controlFor(project, userProvider.get()),
+        db, true);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
index 0e17478..32cfc9b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 
 import org.junit.Test;
@@ -94,6 +95,46 @@
   }
 
   @Test
+  public void setConflictingWatches() throws Exception {
+    String projectName = createProject(NEW_PROJECT_NAME).get();
+
+    List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.notifySubmittedChanges = true;
+    pwi.notifyNewPatchSets = true;
+    projectsToWatch.add(pwi);
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("duplicate entry for project " + projectName);
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+  }
+
+  @Test
+  public void setAndGetEmptyWatch() throws Exception {
+    String projectName = createProject(NEW_PROJECT_NAME).get();
+
+    List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    projectsToWatch.add(pwi);
+
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+    List<ProjectWatchInfo> persistedWatchedProjects =
+        gApi.accounts().self().getWatchedProjects();
+    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
+  }
+
+  @Test
   public void watchNonExistingProject() throws Exception {
     String projectName = NEW_PROJECT_NAME + "3";
 
@@ -111,7 +152,7 @@
   }
 
   @Test
-  public void deleteNonExistingProject() throws Exception {
+  public void deleteNonExistingProjectWatch() throws Exception {
     String projectName = project.get();
 
     // Let another user watch a project
@@ -131,8 +172,8 @@
     List<ProjectWatchInfo> d = Lists.newArrayList(pwi);
     gApi.accounts().self().deleteWatchedProjects(d);
 
+    // Check that trying to delete a non-existing watch doesn't fail
     setApiUser(user);
-    exception.expect(UnprocessableEntityException.class);
     gApi.accounts().self().deleteWatchedProjects(d);
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
new file mode 100644
index 0000000..aa7e864
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -0,0 +1,639 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.extensions.client.ReviewerState.CC;
+import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewResult;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
+import com.google.gerrit.server.change.PostReviewers;
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.testutil.FakeEmailSender.Message;
+import com.google.gson.stream.JsonReader;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+public class ChangeReviewersIT extends AbstractDaemonTest {
+  @Test
+  public void addGroupAsReviewer() throws Exception {
+    // Set up two groups, one that is too large too add as reviewer, and one
+    // that is too large to add without confirmation.
+    String largeGroup = createGroup("largeGroup");
+    String mediumGroup = createGroup("mediumGroup");
+
+    int largeGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS + 1;
+    int mediumGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK + 1;
+    List<TestAccount> users =
+        createAccounts(largeGroupSize, "addGroupAsReviewer");
+    List<String> largeGroupUsernames = new ArrayList<>(mediumGroupSize);
+    for (TestAccount u : users) {
+      largeGroupUsernames.add(u.username);
+    }
+    List<String> mediumGroupUsernames =
+        largeGroupUsernames.subList(0, mediumGroupSize);
+    gApi.groups().id(largeGroup).addMembers(
+        largeGroupUsernames.toArray(new String[largeGroupSize]));
+    gApi.groups().id(mediumGroup).addMembers(
+        mediumGroupUsernames.toArray(new String[mediumGroupSize]));
+
+    // Attempt to add overly large group as reviewers.
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    AddReviewerResult result = addReviewer(changeId, largeGroup);
+    assertThat(result.input).isEqualTo(largeGroup);
+    assertThat(result.confirm).isNull();
+    assertThat(result.error)
+        .contains("has too many members to add them all as reviewers");
+    assertThat(result.reviewers).isNull();
+
+    // Attempt to add medium group without confirmation.
+    result = addReviewer(changeId, mediumGroup);
+    assertThat(result.input).isEqualTo(mediumGroup);
+    assertThat(result.confirm).isTrue();
+    assertThat(result.error)
+        .contains("has " + mediumGroupSize + " members. Do you want to add them"
+            + " all as reviewers?");
+    assertThat(result.reviewers).isNull();
+
+    // Add medium group with confirmation.
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = mediumGroup;
+    in.confirmed = true;
+    result = addReviewer(changeId, in);
+    assertThat(result.input).isEqualTo(mediumGroup);
+    assertThat(result.confirm).isNull();
+    assertThat(result.error).isNull();
+    assertThat(result.reviewers).hasSize(mediumGroupSize);
+
+    // Verify that group members were added as reviewers.
+    ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+    assertReviewers(c, REVIEWER, users.subList(0, mediumGroupSize));
+  }
+
+  @Test
+  public void addCcAccount() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    in.state = CC;
+    AddReviewerResult result = addReviewer(changeId, in);
+
+    assertThat(result.input).isEqualTo(user.email);
+    assertThat(result.confirm).isNull();
+    assertThat(result.error).isNull();
+    ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+    if (notesMigration.readChanges()) {
+      assertThat(result.reviewers).isNull();
+      assertThat(result.ccs).hasSize(1);
+      AccountInfo ai = result.ccs.get(0);
+      assertThat(ai._accountId).isEqualTo(user.id.get());
+      assertReviewers(c, CC, user);
+    } else {
+      assertThat(result.ccs).isNull();
+      assertThat(result.reviewers).hasSize(1);
+      AccountInfo ai = result.reviewers.get(0);
+      assertThat(ai._accountId).isEqualTo(user.id.get());
+      assertReviewers(c, REVIEWER, user);
+    }
+
+    // Verify email was sent to CCed account.
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    if (notesMigration.readChanges()) {
+      assertThat(m.body())
+          .contains(admin.fullName + " has uploaded a new change for review.");
+    } else {
+      assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+      assertThat(m.body()).contains("I'd like you to do a code review.");
+    }
+  }
+
+  @Test
+  public void addCcGroup() throws Exception {
+    List<TestAccount> users = createAccounts(6, "addCcGroup");
+    List<String> usernames = new ArrayList<>(6);
+    for (TestAccount u : users) {
+      usernames.add(u.username);
+    }
+
+    List<TestAccount> firstUsers = users.subList(0, 3);
+    List<String> firstUsernames = usernames.subList(0, 3);
+
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = createGroup("cc1");
+    in.state = CC;
+    gApi.groups().id(in.reviewer)
+        .addMembers(firstUsernames.toArray(new String[firstUsernames.size()]));
+    AddReviewerResult result = addReviewer(changeId, in);
+
+    assertThat(result.input).isEqualTo(in.reviewer);
+    assertThat(result.confirm).isNull();
+    assertThat(result.error).isNull();
+    if (notesMigration.readChanges()) {
+      assertThat(result.reviewers).isNull();
+    } else {
+      assertThat(result.ccs).isNull();
+    }
+    ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+    if (notesMigration.readChanges()) {
+      assertReviewers(c, CC, firstUsers);
+    } else {
+      assertReviewers(c, REVIEWER, firstUsers);
+      assertReviewers(c, CC);
+    }
+
+    // Verify emails were sent to each of the group's accounts.
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    List<Address> expectedAddresses = new ArrayList<>(firstUsers.size());
+    for (TestAccount u : firstUsers) {
+      expectedAddresses.add(u.emailAddress);
+    }
+    assertThat(m.rcpt()).containsExactlyElementsIn(expectedAddresses);
+
+    // CC a group that overlaps with some existing reviewers and CCed accounts.
+    TestAccount reviewer = accounts.create(name("reviewer"),
+        "addCcGroup-reviewer@example.com", "Reviewer");
+    result = addReviewer(changeId, reviewer.username);
+    assertThat(result.error).isNull();
+    sender.clear();
+    in.reviewer = createGroup("cc2");
+    gApi.groups().id(in.reviewer)
+        .addMembers(usernames.toArray(new String[usernames.size()]));
+    gApi.groups().id(in.reviewer).addMembers(reviewer.username);
+    result = addReviewer(changeId, in);
+    assertThat(result.input).isEqualTo(in.reviewer);
+    assertThat(result.confirm).isNull();
+    assertThat(result.error).isNull();
+    c = gApi.changes().id(r.getChangeId()).get();
+    if (notesMigration.readChanges()) {
+      assertThat(result.ccs).hasSize(3);
+      assertThat(result.reviewers).isNull();
+      assertReviewers(c, REVIEWER, reviewer);
+      assertReviewers(c, CC, users);
+    } else {
+      assertThat(result.ccs).isNull();
+      assertThat(result.reviewers).hasSize(3);
+      List<TestAccount> expectedUsers = new ArrayList<>(users.size() + 2);
+      expectedUsers.addAll(users);
+      expectedUsers.add(reviewer);
+      assertReviewers(c, REVIEWER, expectedUsers);
+    }
+
+    messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    m = messages.get(0);
+    expectedAddresses = new ArrayList<>(4);
+    for (int i = 0; i < 3; i++) {
+      expectedAddresses.add(users.get(users.size() - i - 1).emailAddress);
+    }
+    if (!notesMigration.readChanges()) {
+      for (int i = 0; i < 3; i++) {
+        expectedAddresses.add(users.get(i).emailAddress);
+      }
+    }
+    expectedAddresses.add(reviewer.emailAddress);
+    assertThat(m.rcpt()).containsExactlyElementsIn(expectedAddresses);
+  }
+
+  @Test
+  public void transitionCcToReviewer() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    in.state = CC;
+    addReviewer(changeId, in);
+    ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+    if (notesMigration.readChanges()) {
+      assertReviewers(c, REVIEWER);
+      assertReviewers(c, CC, user);
+    } else {
+      assertReviewers(c, REVIEWER, user);
+      assertReviewers(c, CC);
+    }
+
+    in.state = REVIEWER;
+    addReviewer(changeId, in);
+    c = gApi.changes().id(r.getChangeId()).get();
+    assertReviewers(c, REVIEWER, user);
+    assertReviewers(c, CC);
+  }
+
+  @Test
+  public void reviewAndAddReviewers() throws Exception {
+    TestAccount observer = accounts.user2();
+    PushOneCommit.Result r = createChange();
+    ReviewInput input = ReviewInput.approve()
+        .reviewer(user.email)
+        .reviewer(observer.email, CC, false);
+
+    ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
+    assertThat(result.labels).isNotNull();
+    assertThat(result.reviewers).isNotNull();
+    assertThat(result.reviewers).hasSize(2);
+
+    // Verify reviewer and CC were added. If not in NoteDb read mode, both
+    // parties will be returned as CCed.
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+    if (notesMigration.readChanges()) {
+      assertReviewers(c, REVIEWER, admin, user);
+      assertReviewers(c, CC, observer);
+    } else {
+      // In legacy mode, everyone should be a reviewer.
+      assertReviewers(c, REVIEWER, admin, user, observer);
+      assertReviewers(c, CC);
+    }
+
+    // Verify emails were sent to added reviewers.
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(3);
+    // First email to user.
+    Message m = messages.get(0);
+    if (notesMigration.readChanges()) {
+      assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    } else {
+      assertThat(m.rcpt()).containsExactly(
+          user.emailAddress, observer.emailAddress);
+    }
+    assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+    assertThat(m.body()).contains("I'd like you to do a code review.");
+    assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+    // Second email to reviewer and observer.
+    m = messages.get(1);
+    if (notesMigration.readChanges()) {
+      assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress);
+      assertThat(m.body()).contains(admin.fullName + " has uploaded a new change for review.");
+    } else {
+      assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress);
+      assertThat(m.body()).contains("Hello " + observer.fullName + ",\n");
+      assertThat(m.body()).contains("I'd like you to do a code review.");
+    }
+
+    // Third email is review to user and observer.
+    m = messages.get(2);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress);
+    assertThat(m.body()).contains(admin.fullName + " has posted comments on this change.");
+    assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+    assertThat(m.body()).contains("Patch Set 1: Code-Review+2\n");
+  }
+
+  @Test
+  public void reviewAndAddGroupReviewers() throws Exception {
+    int largeGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS + 1;
+    int mediumGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK + 1;
+    List<TestAccount> users =
+        createAccounts(largeGroupSize, "reviewAndAddGroupReviewers");
+    List<String> usernames = new ArrayList<>(largeGroupSize);
+    for (TestAccount u : users) {
+      usernames.add(u.username);
+    }
+
+    String largeGroup = createGroup("largeGroup");
+    String mediumGroup = createGroup("mediumGroup");
+    gApi.groups().id(largeGroup).addMembers(
+        usernames.toArray(new String[largeGroupSize]));
+    gApi.groups().id(mediumGroup).addMembers(
+        usernames.subList(0, mediumGroupSize)
+            .toArray(new String[mediumGroupSize]));
+
+    TestAccount observer = accounts.user2();
+    PushOneCommit.Result r = createChange();
+
+    // Attempt to add overly large group as reviewers.
+    ReviewInput input = ReviewInput.approve()
+        .reviewer(user.email)
+        .reviewer(observer.email, CC, false)
+        .reviewer(largeGroup);
+    ReviewResult result = review(
+        r.getChangeId(), r.getCommit().name(), input, SC_BAD_REQUEST);
+    assertThat(result.labels).isNull();
+    assertThat(result.reviewers).isNotNull();
+    assertThat(result.reviewers).hasSize(3);
+    AddReviewerResult reviewerResult = result.reviewers.get(largeGroup);
+    assertThat(reviewerResult).isNotNull();
+    assertThat(reviewerResult.confirm).isNull();
+    assertThat(reviewerResult.error).isNotNull();
+    assertThat(reviewerResult.error).contains("has too many members to add them all as reviewers");
+
+    // No labels should have changed, and no reviewers/CCs should have been added.
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+    assertThat(c.messages).hasSize(1);
+    assertThat(c.reviewers.get(REVIEWER)).isNull();
+    assertThat(c.reviewers.get(CC)).isNull();
+
+    // Attempt to add group large enough to require confirmation, without
+    // confirmation, as reviewers.
+    input = ReviewInput.approve()
+        .reviewer(user.email)
+        .reviewer(observer.email, CC, false)
+        .reviewer(mediumGroup);
+    result = review(r.getChangeId(), r.getCommit().name(), input,
+        SC_BAD_REQUEST);
+    assertThat(result.labels).isNull();
+    assertThat(result.reviewers).isNotNull();
+    assertThat(result.reviewers).hasSize(3);
+    reviewerResult = result.reviewers.get(mediumGroup);
+    assertThat(reviewerResult).isNotNull();
+    assertThat(reviewerResult.confirm).isTrue();
+    assertThat(reviewerResult.error)
+        .contains("has " + mediumGroupSize + " members. Do you want to add them all"
+            + " as reviewers?");
+
+    // No labels should have changed, and no reviewers/CCs should have been added.
+    c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+    assertThat(c.messages).hasSize(1);
+    assertThat(c.reviewers.get(REVIEWER)).isNull();
+    assertThat(c.reviewers.get(CC)).isNull();
+
+    // Retrying with confirmation should successfully approve and add reviewers/CCs.
+    input = ReviewInput.approve()
+        .reviewer(user.email)
+        .reviewer(mediumGroup, CC, true);
+    result = review(r.getChangeId(), r.getCommit().name(), input);
+    assertThat(result.labels).isNotNull();
+    assertThat(result.reviewers).isNotNull();
+    assertThat(result.reviewers).hasSize(2);
+
+    c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+    assertThat(c.messages).hasSize(2);
+
+    if (notesMigration.readChanges()) {
+      assertReviewers(c, REVIEWER, admin, user);
+      assertReviewers(c, CC, users.subList(0, mediumGroupSize));
+    } else {
+      // If not in NoteDb mode, then everyone is a REVIEWER.
+      List<TestAccount> expected = users.subList(0, mediumGroupSize);
+      expected.add(admin);
+      expected.add(user);
+      assertReviewers(c, REVIEWER, expected);
+      assertReviewers(c, CC);
+    }
+  }
+
+  @Test
+  public void noteDbAddReviewerToReviewerChangeInfo() throws Exception {
+    assume().that(notesMigration.readChanges()).isTrue();
+
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    in.state = CC;
+    addReviewer(changeId, in);
+
+    in.state = REVIEWER;
+    addReviewer(changeId, in);
+
+    gApi.changes().id(changeId).current().review(ReviewInput.dislike());
+
+    setApiUser(user);
+    // NoteDb adds reviewer to a change on every review.
+    gApi.changes().id(changeId).current().review(ReviewInput.dislike());
+
+    deleteReviewer(changeId, user).assertNoContent();
+
+    ChangeInfo c = gApi.changes().id(changeId).get();
+    assertThat(c.reviewerUpdates).isNotNull();
+    assertThat(c.reviewerUpdates).hasSize(3);
+
+    Iterator<ReviewerUpdateInfo> it = c.reviewerUpdates.iterator();
+    ReviewerUpdateInfo reviewerChange = it.next();
+    assertThat(reviewerChange.state).isEqualTo(CC);
+    assertThat(reviewerChange.reviewer._accountId).isEqualTo(
+        user.getId().get());
+    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(
+        admin.getId().get());
+
+    reviewerChange = it.next();
+    assertThat(reviewerChange.state).isEqualTo(REVIEWER);
+    assertThat(reviewerChange.reviewer._accountId).isEqualTo(
+        user.getId().get());
+    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(
+        admin.getId().get());
+
+    reviewerChange = it.next();
+    assertThat(reviewerChange.state).isEqualTo(REMOVED);
+    assertThat(reviewerChange.reviewer._accountId).isEqualTo(
+        user.getId().get());
+    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(
+        admin.getId().get());
+  }
+
+  @Test
+  public void addDuplicateReviewers() throws Exception {
+    PushOneCommit.Result r = createChange();
+    ReviewInput input = ReviewInput.approve()
+        .reviewer(user.email)
+        .reviewer(user.email);
+    ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
+    assertThat(result.reviewers).isNotNull();
+    assertThat(result.reviewers).hasSize(1);
+    AddReviewerResult reviewerResult = result.reviewers.get(user.email);
+    assertThat(reviewerResult).isNotNull();
+    assertThat(reviewerResult.confirm).isNull();
+    assertThat(reviewerResult.error).isNull();
+  }
+
+  @Test
+  public void addOverlappingGroups() throws Exception {
+    String emailPrefix = "addOverlappingGroups-";
+    TestAccount user1 = accounts.create(name("user1"),
+        emailPrefix + "user1@example.com", "User1");
+    TestAccount user2 = accounts.create(name("user2"),
+        emailPrefix + "user2@example.com", "User2");
+    TestAccount user3 = accounts.create(name("user3"),
+        emailPrefix + "user3@example.com", "User3");
+    String group1 = createGroup("group1");
+    String group2 = createGroup("group2");
+    gApi.groups().id(group1).addMembers(user1.username, user2.username);
+    gApi.groups().id(group2).addMembers(user2.username, user3.username);
+
+    PushOneCommit.Result r = createChange();
+    ReviewInput input = ReviewInput.approve()
+        .reviewer(group1)
+        .reviewer(group2);
+    ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
+    assertThat(result.reviewers).isNotNull();
+    assertThat(result.reviewers).hasSize(2);
+    AddReviewerResult reviewerResult = result.reviewers.get(group1);
+    assertThat(reviewerResult.error).isNull();
+    assertThat(reviewerResult.reviewers).hasSize(2);
+    reviewerResult = result.reviewers.get(group2);
+    assertThat(reviewerResult.error).isNull();
+    assertThat(reviewerResult.reviewers).hasSize(1);
+
+    // Repeat the above for CCs
+    if (!notesMigration.readChanges()) {
+      return;
+    }
+    r = createChange();
+    input = ReviewInput.approve()
+        .reviewer(group1, CC, false)
+        .reviewer(group2, CC, false);
+    result = review(r.getChangeId(), r.getCommit().name(), input);
+    assertThat(result.reviewers).isNotNull();
+    assertThat(result.reviewers).hasSize(2);
+    reviewerResult = result.reviewers.get(group1);
+    assertThat(reviewerResult.error).isNull();
+    assertThat(reviewerResult.ccs).hasSize(2);
+    reviewerResult = result.reviewers.get(group2);
+    assertThat(reviewerResult.error).isNull();
+    assertThat(reviewerResult.ccs).hasSize(1);
+
+    // Repeat again with one group REVIEWER, the other CC. The overlapping
+    // member should end up as a REVIEWER.
+    r = createChange();
+    input = ReviewInput.approve()
+        .reviewer(group1, REVIEWER, false)
+        .reviewer(group2, CC, false);
+    result = review(r.getChangeId(), r.getCommit().name(), input);
+    assertThat(result.reviewers).isNotNull();
+    assertThat(result.reviewers).hasSize(2);
+    reviewerResult = result.reviewers.get(group1);
+    assertThat(reviewerResult.error).isNull();
+    assertThat(reviewerResult.reviewers).hasSize(2);
+    reviewerResult = result.reviewers.get(group2);
+    assertThat(reviewerResult.error).isNull();
+    assertThat(reviewerResult.reviewers).isNull();
+    assertThat(reviewerResult.ccs).hasSize(1);
+  }
+
+  private AddReviewerResult addReviewer(String changeId, String reviewer)
+      throws Exception {
+    return addReviewer(changeId, reviewer, SC_OK);
+  }
+
+  private AddReviewerResult addReviewer(
+      String changeId, String reviewer, int expectedStatus) throws Exception {
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = reviewer;
+    return addReviewer(changeId, in, expectedStatus);
+  }
+
+  private AddReviewerResult addReviewer(String changeId, AddReviewerInput in)
+      throws Exception {
+    return addReviewer(changeId, in, SC_OK);
+  }
+
+  private AddReviewerResult addReviewer(String changeId, AddReviewerInput in,
+      int expectedStatus) throws Exception {
+    RestResponse resp =
+        adminRestSession.post("/changes/" + changeId + "/reviewers", in);
+    return readContentFromJson(
+        resp, expectedStatus, AddReviewerResult.class);
+  }
+
+  private RestResponse deleteReviewer(String changeId, TestAccount account)
+      throws Exception {
+    return adminRestSession.delete("/changes/" + changeId + "/reviewers/" +
+        account.getId().get());
+  }
+
+  private ReviewResult review(
+      String changeId, String revisionId, ReviewInput in) throws Exception {
+    return review(changeId, revisionId, in, SC_OK);
+  }
+
+  private ReviewResult review(
+      String changeId, String revisionId, ReviewInput in, int expectedStatus)
+      throws Exception {
+    RestResponse resp = adminRestSession.post(
+        "/changes/" + changeId + "/revisions/" + revisionId + "/review", in);
+    return readContentFromJson(resp, expectedStatus, ReviewResult.class);
+  }
+
+  private static <T> T readContentFromJson(
+      RestResponse r, int expectedStatus, Class<T> clazz)
+      throws Exception {
+    r.assertStatus(expectedStatus);
+    JsonReader jsonReader = new JsonReader(r.getReader());
+    jsonReader.setLenient(true);
+    return newGson().fromJson(jsonReader, clazz);
+  }
+
+  private static void assertReviewers(ChangeInfo c, ReviewerState reviewerState,
+      TestAccount... accounts) throws Exception {
+    List<TestAccount> accountList = new ArrayList<>(accounts.length);
+    for (TestAccount a : accounts) {
+      accountList.add(a);
+    }
+    assertReviewers(c, reviewerState, accountList);
+  }
+
+  private static void assertReviewers(ChangeInfo c, ReviewerState reviewerState,
+      Iterable<TestAccount> accounts) throws Exception {
+    Collection<AccountInfo> actualAccounts = c.reviewers.get(reviewerState);
+    if (actualAccounts == null) {
+      assertThat(accounts.iterator().hasNext()).isFalse();
+      return;
+    }
+    assertThat(actualAccounts).isNotNull();
+    List<Integer> actualAccountIds = new ArrayList<>(actualAccounts.size());
+    for (AccountInfo account : actualAccounts) {
+      actualAccountIds.add(account._accountId);
+    }
+    List<Integer> expectedAccountIds = new ArrayList<>();
+    for (TestAccount account : accounts) {
+      expectedAccountIds.add(account.getId().get());
+    }
+    assertThat(actualAccountIds).containsExactlyElementsIn(expectedAccountIds);
+  }
+
+  private List<TestAccount> createAccounts(int n, String emailPrefix)
+      throws Exception {
+    List<TestAccount> result = new ArrayList<>(n);
+    for (int i = 0; i < n; i++) {
+      result.add(accounts.create(name("u" + i),
+          emailPrefix + "-" + i + "@example.com", "Full Name " + i));
+    }
+    return result;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index 6781ef1..b7f09d1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.Util;
 
@@ -47,7 +48,7 @@
     Util.allow(cfg, Permission.OWNER, REGISTERED_USERS, "refs/*");
     Util.allow(
         cfg, Permission.PUSH, REGISTERED_USERS, "refs/for/refs/meta/config");
-    Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/meta/config");
+    Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, RefNames.REFS_CONFIG);
     saveProjectConfig(project, cfg);
 
     setApiUser(user);
@@ -89,7 +90,7 @@
         .isEqualTo(desc);
     String changeRev = gApi.changes().id(id).get().currentRevision;
     String branchRev = gApi.projects().name(project.get())
-        .branch("refs/meta/config").get().revision;
+        .branch(RefNames.REFS_CONFIG).get().revision;
     assertThat(changeRev).isEqualTo(branchRev);
     return id;
   }
@@ -145,7 +146,7 @@
   private void fetchRefsMetaConfig() throws Exception {
     git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config"))
         .call();
-    testRepo.reset("refs/meta/config");
+    testRepo.reset(RefNames.REFS_CONFIG);
   }
 
   private Config readProjectConfig() throws Exception {
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 13f5063..d696af4 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
@@ -20,26 +20,37 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.MergeInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 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.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.git.ChangeAlreadyMergedException;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.TestTimeUtil;
 
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.RefSpec;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -137,6 +148,99 @@
     }
   }
 
+  @Test
+  public void createMergeChange() throws Exception {
+    changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
+    ChangeInput in =
+        newMergeChangeInput("branchA", "branchB", "");
+    assertCreateSucceeds(in);
+  }
+
+  @Test
+  public void createMergeChange_Conflicts() throws Exception {
+    changeInTwoBranches("branchA", "shared.txt", "branchB", "shared.txt");
+    ChangeInput in =
+        newMergeChangeInput("branchA", "branchB", "");
+    assertCreateFails(in, RestApiException.class, "merge conflict");
+  }
+
+  @Test
+  public void createMergeChange_Conflicts_Ours() throws Exception {
+    changeInTwoBranches("branchA", "shared.txt", "branchB", "shared.txt");
+    ChangeInput in =
+        newMergeChangeInput("branchA", "branchB", "ours");
+    assertCreateSucceeds(in);
+  }
+
+  @Test
+  public void invalidSource() throws Exception {
+    changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
+    ChangeInput in =
+        newMergeChangeInput("branchA", "invalid", "");
+    assertCreateFails(in, BadRequestException.class,
+        "Cannot resolve 'invalid' to a commit");
+  }
+
+  @Test
+  public void invalidStrategy() throws Exception {
+    changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
+    ChangeInput in =
+        newMergeChangeInput("branchA", "branchB", "octopus");
+    assertCreateFails(in, BadRequestException.class,
+        "invalid merge strategy: octopus");
+  }
+
+  @Test
+  public void alreadyMerged() throws Exception {
+    ObjectId c0 = testRepo.branch("HEAD").commit().insertChangeId()
+        .message("first commit")
+        .add("a.txt", "a contents ")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/master")).call();
+
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("second commit")
+        .add("b.txt", "b contents ")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/master")).call();
+
+    ChangeInput in =
+        newMergeChangeInput("master", c0.getName(), "");
+    assertCreateFails(in, ChangeAlreadyMergedException.class,
+        "'" + c0.getName() + "' has already been merged");
+  }
+
+  @Test
+  public void onlyContentMerged() throws Exception {
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("first commit")
+        .add("a.txt", "a contents ")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/master")).call();
+
+    // create a change, and cherrypick into master
+    PushOneCommit.Result cId = createChange();
+    RevCommit commitId = cId.getCommit();
+    CherryPickInput cpi = new CherryPickInput();
+    cpi.destination = "master";
+    cpi.message = "cherry pick the commit";
+    ChangeApi orig = gApi.changes()
+        .id(cId.getChangeId());
+    ChangeApi cherry = orig.current().cherryPick(cpi);
+    cherry.current().review(ReviewInput.approve());
+    cherry.current().submit();
+
+    ObjectId remoteId = getRemoteHead();
+    assertThat(remoteId).isNotEqualTo(commitId);
+
+    ChangeInput in =
+        newMergeChangeInput("master", commitId.getName(), "");
+    assertCreateSucceeds(in);
+  }
+
   private ChangeInput newChangeInput(ChangeStatus status) {
     ChangeInput in = new ChangeInput();
     in.project = project.get();
@@ -191,4 +295,48 @@
 
     assertThat(o.signedOffBy).isTrue();
   }
+
+  private ChangeInput newMergeChangeInput(String targetBranch, String sourceRef,
+      String strategy) {
+    // create a merge change from branchA to master in gerrit
+    ChangeInput in = new ChangeInput();
+    in.project = project.get();
+    in.branch = targetBranch;
+    in.subject = "merge " + sourceRef + " to " + targetBranch;
+    in.status = ChangeStatus.NEW;
+    MergeInput mergeInput = new MergeInput();
+    mergeInput.source = sourceRef;
+    in.merge = mergeInput;
+    if (!Strings.isNullOrEmpty(strategy)) {
+      in.merge.strategy = strategy;
+    }
+    return in;
+  }
+
+  private void changeInTwoBranches(String branchA, String fileA, String branchB,
+      String fileB) throws Exception {
+    // create a initial commit in master
+    Result initialCommit = pushFactory
+        .create(db, user.getIdent(), testRepo, "initial commit", "readme.txt",
+            "initial commit")
+        .to("refs/heads/master");
+    initialCommit.assertOkStatus();
+
+    // create two new branches
+    createBranch(new Branch.NameKey(project, branchA));
+    createBranch(new Branch.NameKey(project, branchB));
+
+    // create a commit in branchA
+    Result changeA = pushFactory
+        .create(db, user.getIdent(), testRepo, "change A", fileA, "A content")
+        .to("refs/heads/" + branchA);
+    changeA.assertOkStatus();
+
+    // create a commit in branchB
+    PushOneCommit commitB = pushFactory
+        .create(db, user.getIdent(), testRepo, "change B", fileB, "B content");
+    commitB.setParent(initialCommit.getCommit());
+    Result changeB = commitB.to("refs/heads/" + branchB);
+    changeB.assertOkStatus();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
index b7b0ec6..31e52f7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
@@ -40,6 +40,8 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Test;
@@ -190,6 +192,62 @@
     }
   }
 
+  @Test
+  public void deleteDraftPatchSetAndPushNewDraftPatchSet() throws Exception {
+    String ref = "refs/drafts/master";
+
+    // Clone repository
+    TestRepository<InMemoryRepository> testRepo =
+        cloneProject(project, admin);
+
+    // Create change
+    PushOneCommit push = pushFactory.create(
+        db, admin.getIdent(), testRepo);
+    PushOneCommit.Result r1 = push.to(ref);
+    r1.assertOkStatus();
+    String revPs1 = r1.getChange().currentPatchSet().getRevision().get();
+
+    // Push draft patch set
+    PushOneCommit.Result r2 = amendChange(
+        r1.getChangeId(), ref, admin, testRepo);
+    r2.assertOkStatus();
+    String revPs2 = r2.getChange().currentPatchSet().getRevision().get();
+
+    assertThat(
+        gApi.changes()
+            .id(r1.getChange().getId().get()).get()
+            .currentRevision)
+        .isEqualTo(revPs2);
+
+    // Remove draft patch set
+    gApi.changes()
+        .id(r1.getChange().getId().get())
+        .revision(revPs2)
+        .delete();
+
+    assertThat(
+        gApi.changes()
+            .id(r1.getChange().getId().get()).get()
+            .currentRevision)
+        .isEqualTo(revPs1);
+
+    // Push new draft patch set
+    PushOneCommit.Result r3 = amendChange(
+        r1.getChangeId(), ref, admin, testRepo);
+    r3.assertOkStatus();
+    String revPs3 = r2.getChange().currentPatchSet().getRevision().get();
+
+    assertThat(
+        gApi.changes()
+            .id(r1.getChange().getId().get()).get()
+            .currentRevision)
+        .isEqualTo(revPs3);
+
+    // Check that all patch sets have different SHA1s
+    assertThat(revPs1).doesNotMatch(revPs2);
+    assertThat(revPs2).doesNotMatch(revPs3);
+  }
+
   private Ref getDraftRef(TestAccount account, Change.Id changeId)
       throws Exception {
     try (Repository repo = repoManager.openRepository(allUsers)) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
index 3228a22a..2a32abe 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.testutil.ConfigSuite;
-import com.google.gerrit.testutil.NoteDbMode;
 
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
@@ -129,9 +128,7 @@
     assertThat(label.all.get(0)._accountId).isEqualTo(user.id.get());
     assertThat(label.all.get(0).value).isEqualTo(0);
 
-    ReviewerState rs = NoteDbMode.readWrite()
-        ? ReviewerState.REVIEWER : ReviewerState.CC;
-    Collection<AccountInfo> ccs = info.reviewers.get(rs);
+    Collection<AccountInfo> ccs = info.reviewers.get(ReviewerState.REVIEWER);
     assertThat(ccs).hasSize(1);
     assertThat(ccs.iterator().next()._accountId).isEqualTo(user.id.get());
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 9577d2f..29fda2d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -22,10 +22,8 @@
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Project;
 
 import org.eclipse.jgit.junit.TestRepository;
@@ -486,8 +484,10 @@
     gApi.changes().id(draftResult.getChangeId()).delete();
 
     // approve and submit the change
-    submit(changeResult.getChangeId(), new SubmitInput(),
-        ResourceConflictException.class, "nothing to merge");
+    submitWithConflict(changeResult.getChangeId(),
+        "Failed to submit 1 change due to the following problems:\n"
+            + "Change " + changeResult.getChange().getId()
+            + ": depends on change that was not submitted");
 
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
index 288e96e..9b3fd15 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.acceptance.GitUtil.getChangeId;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
@@ -151,6 +152,57 @@
   }
 
   @Test
+  public void submitWithRebaseMergeCommit() throws Exception {
+    /*
+        *  (HEAD, origin/master, origin/HEAD) Merge changes X,Y
+        |\
+        | *   Merge branch 'master' into origin/master
+        | |\
+        | | * SHA Added a
+        | |/
+        * | Before
+        |/
+        * Initial empty repository
+     */
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change1 = createChange("Added a", "a.txt", "");
+
+    PushOneCommit change2Push = pushFactory.create(db, admin.getIdent(), testRepo,
+        "Merge to master", "m.txt", "");
+    change2Push.setParents(ImmutableList.of(initialHead, change1.getCommit()));
+    PushOneCommit.Result change2 = change2Push.to("refs/for/master");
+
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change3 = createChange("Before", "b.txt", "");
+
+    approve(change3.getChangeId());
+    submit(change3.getChangeId());
+
+    approve(change1.getChangeId());
+    approve(change2.getChangeId());
+    submit(change2.getChangeId());
+
+    RevCommit newHead = getRemoteHead();
+    assertThat(newHead.getParentCount()).isEqualTo(2);
+
+    RevCommit headParent1 = parse(newHead.getParent(0).getId());
+    RevCommit headParent2 = parse(newHead.getParent(1).getId());
+
+    assertThat(headParent1.getId()).isEqualTo(change3.getCommit().getId());
+    assertThat(headParent1.getParentCount()).isEqualTo(1);
+    assertThat(headParent1.getParent(0)).isEqualTo(initialHead);
+
+    assertThat(headParent2.getId()).isEqualTo(change2.getCommit().getId());
+    assertThat(headParent2.getParentCount()).isEqualTo(2);
+
+    RevCommit headGrandparent1 = parse(headParent2.getParent(0).getId());
+    RevCommit headGrandparent2 = parse(headParent2.getParent(1).getId());
+
+    assertThat(headGrandparent1.getId()).isEqualTo(initialHead.getId());
+    assertThat(headGrandparent2.getId()).isEqualTo(change1.getCommit().getId());
+  }
+
+  @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge() throws Exception {
     RevCommit initialHead = getRemoteHead();
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 3eb46cf..e4f56f8 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
@@ -168,7 +168,6 @@
   }
 
   @Test
-  @GerritConfig(name = "suggest.fullTextSearch", value = "true")
   public void suggestReviewersFullTextSearch() throws Exception {
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
@@ -221,18 +220,6 @@
   }
 
   @Test
-  @GerritConfigs(
-      {@GerritConfig(name = "suggest.fulltextsearch", value = "true"),
-       @GerritConfig(name = "suggest.fullTextSearchMaxMatches", value = "2")
-  })
-  public void suggestReviewersFullTextSearchLimitMaxMatches() throws Exception {
-    String changeId = createChange().getChangeId();
-    List<SuggestedReviewerInfo> reviewers =
-        suggestReviewers(changeId, name("user"), 2);
-    assertThat(reviewers).hasSize(2);
-  }
-
-  @Test
   public void suggestReviewersWithoutLimitOptionSpecified() throws Exception {
     String changeId = createChange().getChangeId();
     String query = user3.username;
@@ -243,6 +230,45 @@
     assertThat(suggestedReviewerInfos).hasSize(1);
   }
 
+  @Test
+  @GerritConfigs({
+    @GerritConfig(name = "addreviewer.maxAllowed", value="2"),
+    @GerritConfig(name = "addreviewer.maxWithoutConfirmation", value="1"),
+  })
+  public void suggestReviewersGroupSizeConsiderations() throws Exception {
+    AccountGroup largeGroup = group("large");
+    AccountGroup mediumGroup = group("medium");
+
+    // Both groups have Administrator as a member. Add two users to large
+    // group to push it past maxAllowed, and one to medium group to push it
+    // past maxWithoutConfirmation.
+    user("individual 0", "Test0 Last0", largeGroup, mediumGroup);
+    user("individual 1", "Test1 Last1", largeGroup);
+
+    String changeId = createChange().getChangeId();
+    List<SuggestedReviewerInfo> reviewers;
+    SuggestedReviewerInfo reviewer;
+
+    // Individual account suggestions have count of 1 and no confirm.
+    reviewers = suggestReviewers(changeId, "test", 10);
+    assertThat(reviewers).hasSize(2);
+    reviewer = reviewers.get(0);
+    assertThat(reviewer.count).isEqualTo(1);
+    assertThat(reviewer.confirm).isNull();
+
+    // Large group should never be suggested.
+    reviewers = suggestReviewers(changeId, largeGroup.getName(), 10);
+    assertThat(reviewers).isEmpty();
+
+    // Medium group should be suggested with appropriate count and confirm.
+    reviewers = suggestReviewers(changeId, mediumGroup.getName(), 10);
+    assertThat(reviewers).hasSize(1);
+    reviewer = reviewers.get(0);
+    assertThat(reviewer.group.name).isEqualTo(mediumGroup.getName());
+    assertThat(reviewer.count).isEqualTo(2);
+    assertThat(reviewer.confirm).isTrue();
+  }
+
   private List<SuggestedReviewerInfo> suggestReviewers(String changeId,
       String query, int n) throws Exception {
     return gApi.changes()
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/TopicIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/TopicIT.java
new file mode 100644
index 0000000..f52fccd
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/TopicIT.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.RestResponse;
+
+import org.junit.Test;
+
+public class TopicIT extends AbstractDaemonTest {
+  @Test
+  public void topic() throws Exception {
+    Result result = createChange();
+    String endpoint = "/changes/" + result.getChangeId() + "/topic";
+    RestResponse response = adminRestSession.put(endpoint, "topic");
+    response.assertOK();
+
+    response = adminRestSession.delete(endpoint);
+    response.assertNoContent();
+
+    response = adminRestSession.put(endpoint, "topic");
+    response.assertOK();
+
+    response = adminRestSession.put(endpoint, "");
+    response.assertNoContent();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
index 259a1b4..bdcdfae 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
@@ -62,4 +62,13 @@
         .put("/config/server/email.confirm", in)
         .assertUnprocessableEntity();
   }
+
+  @Test
+  public void confirmAlreadyInUse_UnprocessableEntity() throws Exception {
+    ConfirmEmail.Input in = new ConfirmEmail.Input();
+    in.token = emailTokenVerifier.encode(admin.getId(), user.email);
+    adminRestSession
+        .put("/config/server/email.confirm", in)
+        .assertUnprocessableEntity();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 54fa74c..2269c77 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -21,12 +21,12 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GerritConfigs;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
-import com.google.gerrit.server.config.GetServerInfo.ServerInfo;
 
 import org.junit.Test;
 
@@ -79,7 +79,7 @@
     // auth
     assertThat(i.auth.authType).isEqualTo(AuthType.HTTP);
     assertThat(i.auth.editableAccountFields).containsExactly(
-        Account.FieldName.REGISTER_NEW_EMAIL, Account.FieldName.FULL_NAME);
+        AccountFieldName.REGISTER_NEW_EMAIL, AccountFieldName.FULL_NAME);
     assertThat(i.auth.useContributorAgreements).isTrue();
     assertThat(i.auth.loginUrl).isEqualTo("https://example.com/login");
     assertThat(i.auth.loginText).isEqualTo("LOGIN");
@@ -147,8 +147,8 @@
     // auth
     assertThat(i.auth.authType).isEqualTo(AuthType.OPENID);
     assertThat(i.auth.editableAccountFields).containsExactly(
-        Account.FieldName.REGISTER_NEW_EMAIL, Account.FieldName.FULL_NAME,
-        Account.FieldName.USER_NAME);
+        AccountFieldName.REGISTER_NEW_EMAIL, AccountFieldName.FULL_NAME,
+        AccountFieldName.USER_NAME);
     assertThat(i.auth.useContributorAgreements).isNull();
     assertThat(i.auth.loginUrl).isNull();
     assertThat(i.auth.loginText).isNull();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
new file mode 100644
index 0000000..31e7382
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import org.junit.Test;
+
+public class CreateGroupIT extends AbstractDaemonTest {
+
+  @Test
+  public void createGroup() throws Exception {
+    GroupInput in = new GroupInput();
+    in.name = name("group");
+    gApi.groups().create(in);
+    AccountGroup accountGroup =
+        groupCache.get(new AccountGroup.NameKey(in.name));
+    assertThat(accountGroup).isNotNull();
+    assertThat(accountGroup.getName()).isEqualTo(in.name);
+  }
+
+  @Test
+  public void createGroupAlreadyExists() throws Exception {
+    GroupInput in = new GroupInput();
+    in.name = name("group");
+    gApi.groups().create(in);
+    assertThat(groupCache.get(new AccountGroup.NameKey(in.name))).isNotNull();
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("group '" + in.name + "' already exists");
+    gApi.groups().create(in);
+  }
+
+  @Test
+  public void createGroupWithDifferentCase() throws Exception {
+    GroupInput in = new GroupInput();
+    in.name = name("group");
+    gApi.groups().create(in);
+    assertThat(groupCache.get(new AccountGroup.NameKey(in.name))).isNotNull();
+
+    GroupInput inLowerCase = new GroupInput();
+    inLowerCase.name =  in.name.toUpperCase();
+    gApi.groups().create(inLowerCase);
+    assertThat(groupCache.get(new AccountGroup.NameKey(inLowerCase.name)))
+        .isNotNull();
+  }
+
+  @Test
+  public void createSystemGroupWithDifferentCase() throws Exception {
+    String registeredUsers = "Registered Users";
+    GroupInput in = new GroupInput();
+    in.name = registeredUsers.toUpperCase();
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("group '" + registeredUsers + "' already exists");
+    gApi.groups().create(in);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 975dc2b..c78b291 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.group.SystemGroupBackend;
 
@@ -72,7 +73,7 @@
   @Test
   public void addAccessSection() throws Exception {
     Project.NameKey p = new Project.NameKey(newProjectName);
-    RevCommit initialHead = getRemoteHead(p, "refs/meta/config");
+    RevCommit initialHead = getRemoteHead(p, RefNames.REFS_CONFIG);
 
     ProjectAccessInput accessInput = newProjectAccessInput();
     AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
@@ -82,8 +83,8 @@
 
     assertThat(pApi.access().local).isEqualTo(accessInput.add);
 
-    RevCommit updatedHead = getRemoteHead(p, "refs/meta/config");
-    eventRecorder.assertRefUpdatedEvents(p.get(), "refs/meta/config",
+    RevCommit updatedHead = getRemoteHead(p, RefNames.REFS_CONFIG);
+    eventRecorder.assertRefUpdatedEvents(p.get(), RefNames.REFS_CONFIG,
         null, initialHead,
         initialHead, updatedHead);
   }
@@ -377,7 +378,7 @@
     // Load current permissions
     String config = gApi.projects()
         .name(allProjects.get())
-        .branch("refs/meta/config").file("project.config").asString();
+        .branch(RefNames.REFS_CONFIG).file("project.config").asString();
 
     // Append and push unknown permission
     Config cfg = new Config();
@@ -387,12 +388,12 @@
     PushOneCommit push = pushFactory.create(
         db, admin.getIdent(), allProjectsRepo, "Subject", "project.config",
         config);
-    push.to("refs/meta/config").assertOkStatus();
+    push.to(RefNames.REFS_CONFIG).assertOkStatus();
 
     // Verify that unknownPermission is present
     config = gApi.projects()
         .name(allProjects.get())
-        .branch("refs/meta/config").file("project.config").asString();
+        .branch(RefNames.REFS_CONFIG).file("project.config").asString();
     cfg.fromText(config);
     assertThat(cfg.getString(access, refsFor, unknownPermission))
         .isEqualTo(registeredUsers);
@@ -409,7 +410,7 @@
     // Verify that unknownPermission is still present
     config = gApi.projects()
         .name(allProjects.get())
-        .branch("refs/meta/config").file("project.config").asString();
+        .branch(RefNames.REFS_CONFIG).file("project.config").asString();
     cfg.fromText(config);
     assertThat(cfg.getString(access, refsFor, unknownPermission))
         .isEqualTo(registeredUsers);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CheckMergeabilityIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CheckMergeabilityIT.java
new file mode 100644
index 0000000..a094f93
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CheckMergeabilityIT.java
@@ -0,0 +1,248 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.common.MergeableInfo;
+import com.google.gerrit.reviewdb.client.Branch;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.RefSpec;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CheckMergeabilityIT extends AbstractDaemonTest {
+
+  private Branch.NameKey branch;
+
+  @Before
+  public void setUp() throws Exception {
+    branch = new Branch.NameKey(project, "test");
+    gApi.projects()
+        .name(branch.getParentKey().get())
+        .branch(branch.get()).create(new BranchInput());
+  }
+
+  @Test
+  public void checkMergeableCommit() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("some change in a")
+        .add("a.txt", "a contents ")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/master")).call();
+
+    testRepo.reset(initialHead);
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("some change in b")
+        .add("b.txt", "b contents ")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/test")).call();
+
+    assertMergeable("master", "test", "recursive");
+  }
+
+  @Test
+  public void checkUnMergeableCommit() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("some change in a")
+        .add("a.txt", "a contents ")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/master")).call();
+
+    testRepo.reset(initialHead);
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("some change in a too")
+        .add("a.txt", "a contents too")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/test")).call();
+
+    assertUnMergeable("master", "test", "recursive", "a.txt");
+  }
+
+  @Test
+  public void checkOursMergeStrategy() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("some change in a")
+        .add("a.txt", "a contents ")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/master")).call();
+
+    testRepo.reset(initialHead);
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("some change in a too")
+        .add("a.txt", "a contents too")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/test")).call();
+
+    assertMergeable("master", "test", "ours");
+  }
+
+  @Test
+  public void checkAlreadyMergedCommit() throws Exception {
+    ObjectId c0 = testRepo.branch("HEAD").commit().insertChangeId()
+        .message("first commit")
+        .add("a.txt", "a contents ")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/master")).call();
+
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("second commit")
+        .add("b.txt", "b contents ")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/master")).call();
+
+    assertCommitMerged("master", c0.getName(), "");
+  }
+
+  @Test
+  public void checkContentMergedCommit() throws Exception {
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("first commit")
+        .add("a.txt", "a contents ")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/master")).call();
+
+    // create a change, and cherrypick into master
+    PushOneCommit.Result cId = createChange();
+    RevCommit commitId = cId.getCommit();
+    CherryPickInput cpi = new CherryPickInput();
+    cpi.destination = "master";
+    cpi.message = "cherry pick the commit";
+    ChangeApi orig = gApi.changes()
+        .id(cId.getChangeId());
+    ChangeApi cherry = orig.current().cherryPick(cpi);
+    cherry.current().review(ReviewInput.approve());
+    cherry.current().submit();
+
+    ObjectId remoteId = getRemoteHead();
+    assertThat(remoteId).isNotEqualTo(commitId);
+    assertContentMerged("master", commitId.getName(), "recursive");
+  }
+
+  @Test
+  public void checkInvalidSource() throws Exception {
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("first commit")
+        .add("a.txt", "a contents ")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/master")).call();
+
+    assertBadRequest("master", "fdsafsdf", "recursive",
+        "Cannot resolve 'fdsafsdf' to a commit");
+  }
+
+  @Test
+  public void checkInvalidStrategy() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("first commit")
+        .add("a.txt", "a contents ")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/master")).call();
+
+    testRepo.reset(initialHead);
+    testRepo.branch("HEAD").commit().insertChangeId()
+        .message("some change in a too")
+        .add("a.txt", "a contents too")
+        .create();
+    testRepo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/test")).call();
+
+    assertBadRequest("master", "test", "octopus",
+        "invalid merge strategy: octopus");
+  }
+
+  private void assertMergeable(String targetBranch, String source,
+      String strategy) throws Exception {
+    MergeableInfo
+        mergeableInfo = getMergeableInfo(targetBranch, source, strategy);
+    assertThat(mergeableInfo.mergeable).isTrue();
+  }
+
+  private void assertUnMergeable(String targetBranch, String source,
+      String strategy, String... conflicts) throws Exception {
+    MergeableInfo mergeableInfo = getMergeableInfo(targetBranch, source, strategy);
+    assertThat(mergeableInfo.mergeable).isFalse();
+    assertThat(mergeableInfo.conflicts).containsExactly((Object[]) conflicts);
+  }
+
+  private void assertCommitMerged(String targetBranch, String source,
+      String strategy) throws Exception {
+    MergeableInfo
+        mergeableInfo = getMergeableInfo(targetBranch, source, strategy);
+    assertThat(mergeableInfo.mergeable).isTrue();
+    assertThat(mergeableInfo.commitMerged).isTrue();
+  }
+
+  private void assertContentMerged(String targetBranch, String source,
+      String strategy) throws Exception {
+    MergeableInfo
+        mergeableInfo = getMergeableInfo(targetBranch, source, strategy);
+    assertThat(mergeableInfo.mergeable).isTrue();
+    assertThat(mergeableInfo.contentMerged).isTrue();
+  }
+
+  private void assertBadRequest(String targetBranch, String source,
+      String strategy, String errMsg) throws Exception {
+    String url = "/projects/" + project.get() + "/branches/" + targetBranch;
+    url += "/mergeable?source=" + source;
+    if (!Strings.isNullOrEmpty(strategy)) {
+      url += "&strategy=" + strategy;
+    }
+
+    RestResponse r = userRestSession.get(url);
+    r.assertBadRequest();
+    assertThat(r.getEntityContent()).isEqualTo(errMsg);
+  }
+
+  private MergeableInfo getMergeableInfo(String targetBranch, String source,
+      String strategy) throws Exception {
+    String url = "/projects/" + project.get() + "/branches/" + targetBranch;
+    url += "/mergeable?source=" + source;
+    if (!Strings.isNullOrEmpty(strategy)) {
+      url += "&strategy=" + strategy;
+    }
+
+    RestResponse r = userRestSession.get(url);
+    r.assertOK();
+    MergeableInfo result = newGson().fromJson(r.getReader(), MergeableInfo.class);
+    r.consume();
+    return result;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
index 856eefe..af1383b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.RefNames;
 
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
@@ -142,7 +143,7 @@
 
   private void assertBranches(List<String> branches) throws Exception {
     List<String> expected = Lists.newArrayList(
-        "HEAD", "refs/meta/config", "refs/heads/master");
+        "HEAD", RefNames.REFS_CONFIG, "refs/heads/master");
     expected.addAll(branches);
     assertRefNames(expected, project().branches().get());
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index a3a107d..7c98188 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.RefNames;
 
 import org.junit.Test;
 
@@ -48,7 +49,7 @@
   public void listBranchesOfEmptyProject() throws Exception {
     assertBranches(ImmutableList.of(
           branch("HEAD", null, false),
-          branch("refs/meta/config",  null, false)),
+          branch(RefNames.REFS_CONFIG,  null, false)),
         list().get());
   }
 
@@ -58,7 +59,7 @@
     String dev = pushTo("refs/heads/dev").getCommit().name();
     assertBranches(ImmutableList.of(
           branch("HEAD", "master", false),
-          branch("refs/meta/config",  null, false),
+          branch(RefNames.REFS_CONFIG,  null, false),
           branch("refs/heads/dev", dev, true),
           branch("refs/heads/master", master, false)),
         list().get());
@@ -98,7 +99,7 @@
     // Using only limit.
     assertRefNames(ImmutableList.of(
           "HEAD",
-          "refs/meta/config",
+          RefNames.REFS_CONFIG,
           "refs/heads/master",
           "refs/heads/someBranch1"),
         list().withLimit(4).get());
@@ -106,7 +107,7 @@
     // Limit higher than total number of branches.
     assertRefNames(ImmutableList.of(
           "HEAD",
-          "refs/meta/config",
+          RefNames.REFS_CONFIG,
           "refs/heads/master",
           "refs/heads/someBranch1",
           "refs/heads/someBranch2",
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 96a672f..d9f1a5c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
 
+import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
@@ -56,6 +57,7 @@
 
 @NoHttpd
 public class CommentsIT extends AbstractDaemonTest {
+
   @Inject
   private Provider<ChangesCollection> changes;
 
@@ -87,12 +89,35 @@
       PushOneCommit.Result r = createChange();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
-      DraftInput comment = newDraft("file1", Side.REVISION, line, "comment 1");
+      String path = "file1";
+      DraftInput comment = newDraft(path, Side.REVISION, line, "comment 1");
       addDraft(changeId, revId, comment);
       Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
       assertThat(result).hasSize(1);
       CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
-      assertCommentInfo(comment, actual);
+      assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
+    }
+  }
+
+  @Test
+  public void createDraftOnMergeCommitChange() throws Exception {
+    for (Integer line : lines) {
+      PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
+      String changeId = r.getChangeId();
+      String revId = r.getCommit().getName();
+      String path = "file1";
+      DraftInput c1 = newDraft(path, Side.REVISION, line, "ps-1");
+      DraftInput c2 = newDraft(path, Side.PARENT, line, "auto-merge of ps-1");
+      DraftInput c3 = newDraftOnParent(path, 1, line, "parent-1 of ps-1");
+      DraftInput c4 = newDraftOnParent(path, 2, line, "parent-2 of ps-1");
+      addDraft(changeId, revId, c1);
+      addDraft(changeId, revId, c2);
+      addDraft(changeId, revId, c3);
+      addDraft(changeId, revId, c4);
+      Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
+      assertThat(result).hasSize(1);
+      assertThat(Lists.transform(result.get(path), infoToDraft(path)))
+          .containsExactly(c1, c2, c3, c4);
     }
   }
 
@@ -114,8 +139,31 @@
       Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
       assertThat(result).isNotEmpty();
       CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
-      assertCommentInfo(comment, actual);
-      assertCommentInfo(actual, getPublishedComment(changeId, revId, actual.id));
+      assertThat(comment).isEqualTo(infoToInput(file).apply(actual));
+      assertThat(comment).isEqualTo(infoToInput(file).apply(
+          getPublishedComment(changeId, revId, actual.id)));
+    }
+  }
+
+  @Test
+  public void postCommentOnMergeCommitChange() throws Exception {
+    for (Integer line : lines) {
+      final String file = "/COMMIT_MSG";
+      PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
+      String changeId = r.getChangeId();
+      String revId = r.getCommit().getName();
+      ReviewInput input = new ReviewInput();
+      CommentInput c1 = newComment(file, Side.REVISION, line, "ps-1");
+      CommentInput c2 = newComment(file, Side.PARENT, line, "auto-merge of ps-1");
+      CommentInput c3 = newCommentOnParent(file, 1, line, "parent-1 of ps-1");
+      CommentInput c4 = newCommentOnParent(file, 2, line, "parent-2 of ps-1");
+      input.comments = new HashMap<>();
+      input.comments.put(file, ImmutableList.of(c1, c2, c3, c4));
+      revision(r).review(input);
+      Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
+      assertThat(result).isNotEmpty();
+      assertThat(Lists.transform(result.get(file), infoToInput(file)))
+          .containsExactly(c1, c2, c3, c4);
     }
   }
 
@@ -129,7 +177,7 @@
     String revId = r.getCommit().getName();
     assertThat(getPublishedComments(changeId, revId)).isEmpty();
 
-    List<Comment> expectedComments = new ArrayList<>();
+    List<CommentInput> expectedComments = new ArrayList<>();
     for (Integer line : lines) {
       ReviewInput input = new ReviewInput();
       CommentInput comment = newComment(file, Side.REVISION, line, "comment " + line);
@@ -142,10 +190,8 @@
     Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
     assertThat(result).isNotEmpty();
     List<CommentInfo> actualComments = result.get(file);
-    assertThat(actualComments).hasSize(expectedComments.size());
-    for (int i = 0; i < actualComments.size(); i++) {
-      assertCommentInfo(expectedComments.get(i), actualComments.get(i));
-    }
+    assertThat(Lists.transform(actualComments, infoToInput(file)))
+        .containsExactlyElementsIn(expectedComments);
   }
 
   @Test
@@ -155,17 +201,18 @@
       Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
-      DraftInput comment = newDraft("file1", Side.REVISION, line, "comment 1");
+      String path = "file1";
+      DraftInput comment = newDraft(path, Side.REVISION, line, "comment 1");
       addDraft(changeId, revId, comment);
       Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
       CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
-      assertCommentInfo(comment, actual);
+      assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
       String uuid = actual.id;
       comment.message = "updated comment 1";
       updateDraft(changeId, revId, comment, uuid);
       result = getDraftComments(changeId, revId);
       actual = Iterables.getOnlyElement(result.get(comment.path));
-      assertCommentInfo(comment, actual);
+      assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
 
       // Posting a draft comment doesn't cause lastUpdatedOn to change.
       assertThat(r.getChange().change().getLastUpdatedOn())
@@ -181,7 +228,7 @@
     String revId = r.getCommit().getName();
     assertThat(getDraftComments(changeId, revId)).isEmpty();
 
-    List<Comment> expectedDrafts = new ArrayList<>();
+    List<DraftInput> expectedDrafts = new ArrayList<>();
     for (Integer line : lines) {
       DraftInput comment = newDraft(file, Side.REVISION, line, "comment " + line);
       expectedDrafts.add(comment);
@@ -191,10 +238,8 @@
     Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
     assertThat(result).isNotEmpty();
     List<CommentInfo> actualComments = result.get(file);
-    assertThat(actualComments).hasSize(expectedDrafts.size());
-    for (int i = 0; i < actualComments.size(); i++) {
-      assertCommentInfo(expectedDrafts.get(i), actualComments.get(i));
-    }
+    assertThat(Lists.transform(actualComments, infoToDraft(file)))
+        .containsExactlyElementsIn(expectedDrafts);
   }
 
   @Test
@@ -203,11 +248,12 @@
       PushOneCommit.Result r = createChange();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
+      String path = "file1";
       DraftInput comment = newDraft(
-          "file1", Side.REVISION, line, "comment 1");
+          path, Side.REVISION, line, "comment 1");
       CommentInfo returned = addDraft(changeId, revId, comment);
       CommentInfo actual = getDraftComment(changeId, revId, returned.id);
-      assertCommentInfo(comment, actual);
+      assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
     }
   }
 
@@ -257,7 +303,9 @@
       Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
       assertThat(result).isNotEmpty();
       CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
-      assertCommentInfo(comment, actual);
+      CommentInput ci = infoToInput(file).apply(actual);
+      ci.updated = comment.updated;
+      assertThat(comment).isEqualTo(ci);
       assertThat(actual.updated)
           .isEqualTo(gApi.changes().id(r.getChangeId()).info().created);
 
@@ -597,45 +645,35 @@
     return gApi.changes().id(changeId).revision(revId).draft(uuid).get();
   }
 
-  private static void assertCommentInfo(Comment expected, CommentInfo actual) {
-    assertThat(actual.line).isEqualTo(expected.line);
-    assertThat(actual.message).isEqualTo(expected.message);
-    assertThat(actual.inReplyTo).isEqualTo(expected.inReplyTo);
-    assertCommentRange(expected.range, actual.range);
-    if (actual.side == null && expected.side != null) {
-      assertThat(Side.REVISION).isEqualTo(expected.side);
-    }
-  }
-
-  private static void assertCommentRange(Comment.Range expected,
-      Comment.Range actual) {
-    if (expected == null) {
-      assertThat(actual).isNull();
-    } else {
-      assertThat(actual).isNotNull();
-      assertThat(actual.startLine).isEqualTo(expected.startLine);
-      assertThat(actual.startCharacter).isEqualTo(expected.startCharacter);
-      assertThat(actual.endLine).isEqualTo(expected.endLine);
-      assertThat(actual.endCharacter).isEqualTo(expected.endCharacter);
-    }
-  }
-
   private static CommentInput newComment(String path, Side side, int line,
       String message) {
     CommentInput c = new CommentInput();
-    return populate(c, path, side, line, message);
+    return populate(c, path, side, null, line, message);
+  }
+
+  private static CommentInput newCommentOnParent(String path, int parent,
+      int line, String message) {
+    CommentInput c = new CommentInput();
+    return populate(c, path, Side.PARENT, Integer.valueOf(parent), line, message);
   }
 
   private DraftInput newDraft(String path, Side side, int line,
       String message) {
     DraftInput d = new DraftInput();
-    return populate(d, path, side, line, message);
+    return populate(d, path, side, null, line, message);
+  }
+
+  private DraftInput newDraftOnParent(String path, int parent, int line,
+      String message) {
+    DraftInput d = new DraftInput();
+    return populate(d, path, Side.PARENT, Integer.valueOf(parent), line, message);
   }
 
   private static <C extends Comment> C populate(C c, String path, Side side,
-      int line, String message) {
+      Integer parent, int line, String message) {
     c.path = path;
     c.side = side;
+    c.parent = parent;
     c.line = line != 0 ? line : null;
     c.message = message;
     if (line != 0) {
@@ -648,4 +686,38 @@
     }
     return c;
   }
+
+  private static Function<CommentInfo, CommentInput> infoToInput(
+      final String path) {
+    return new Function<CommentInfo, CommentInput>() {
+      @Override
+      public CommentInput apply(CommentInfo info) {
+        CommentInput ci = new CommentInput();
+        ci.path = path;
+        copy(info, ci);
+        return ci;
+      }
+    };
+  }
+
+  private static Function<CommentInfo, DraftInput> infoToDraft(
+      final String path) {
+    return new Function<CommentInfo, DraftInput>() {
+      @Override
+      public DraftInput apply(CommentInfo info) {
+        DraftInput di = new DraftInput();
+        di.path = path;
+        copy(info, di);
+        return di;
+      }
+    };
+  }
+
+  private static void copy(Comment from, Comment to) {
+    to.side = from.side == null ? Side.REVISION : from.side;
+    to.parent = from.parent;
+    to.line = from.line;
+    to.message = from.message;
+    to.range = from.range;
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index f846151..37e551f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -14,19 +14,22 @@
 
 package com.google.gerrit.acceptance.server.change;
 
-import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.testutil.TestChanges.newChange;
+import static com.google.gerrit.extensions.common.ProblemInfo.Status.FIXED;
+import static com.google.gerrit.extensions.common.ProblemInfo.Status.FIX_FAILED;
 import static com.google.gerrit.testutil.TestChanges.newPatchSet;
 import static java.util.Collections.singleton;
 
-import com.google.common.base.Function;
-import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ProblemInfo;
@@ -34,22 +37,39 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ConsistencyChecker;
-import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
 import com.google.gerrit.testutil.TestChanges;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 @NoHttpd
@@ -58,25 +78,36 @@
   private ChangeControl.GenericFactory changeControlFactory;
 
   @Inject
-  private ChangeUpdate.Factory changeUpdateFactory;
-
-  @Inject
   private Provider<ConsistencyChecker> checkerProvider;
 
   @Inject
   private IdentifiedUser.GenericFactory userFactory;
 
+  @Inject
+  private BatchUpdate.Factory updateFactory;
+
+  @Inject
+  private ChangeInserter.Factory changeInserterFactory;
+
+  @Inject
+  private PatchSetInserter.Factory patchSetInserterFactory;
+
+  @Inject
+  private ChangeNoteUtil noteUtil;
+
+  @Inject
+  @AnonymousCowardName
+  private String anonymousCowardName;
+
+  @Inject
+  private Sequences sequences;
+
   private RevCommit tip;
   private Account.Id adminId;
   private ConsistencyChecker checker;
 
   @Before
   public void setUp() throws Exception {
-    // TODO(dborowitz): Re-enable when ConsistencyChecker supports NoteDb.
-    // Note that we *do* want to enable these tests with GERRIT_CHECK_NOTEDB, as
-    // we need to be able to convert old, corrupt changes. However, those tests
-    // don't necessarily need to pass.
-    assume().that(notesMigration.enabled()).isFalse();
     // Ignore client clone of project; repurpose as server-side TestRepository.
     testRepo = new TestRepository<>(
         (InMemoryRepository) repoManager.openRepository(project));
@@ -88,62 +119,56 @@
 
   @Test
   public void validNewChange() throws Exception {
-    Change c = insertChange();
-    insertPatchSet(c);
-    incrementPatchSet(c);
-    insertPatchSet(c);
-    assertProblems(c);
+    assertNoProblems(insertChange(), null);
   }
 
   @Test
   public void validMergedChange() throws Exception {
-    Change c = insertChange();
-    c.setStatus(Change.Status.MERGED);
-    insertPatchSet(c);
-    incrementPatchSet(c);
-
-    incrementPatchSet(c);
-    RevCommit commit2 = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit2, adminId);
-    db.patchSets().insert(singleton(ps2));
-
-    testRepo.branch(c.getDest().get()).update(commit2);
-    assertProblems(c);
+    ChangeControl ctl = mergeChange(incrementPatchSet(insertChange()));
+    assertNoProblems(ctl, null);
   }
 
   @Test
   public void missingOwner() throws Exception {
-    Change c = newChange(project, new Account.Id(2));
-    db.changes().insert(singleton(c));
-    RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
-    db.patchSets().insert(singleton(ps));
+    TestAccount owner = accounts.create("missing");
+    ChangeControl ctl = insertChange(owner);
+    db.accounts().deleteKeys(singleton(owner.getId()));
 
-    assertProblems(c, "Missing change owner: 2");
+    assertProblems(ctl, null,
+        problem("Missing change owner: " + owner.getId()));
   }
 
   @Test
   public void missingRepo() throws Exception {
-    Change c = newChange(new Project.NameKey("otherproject"), adminId);
-    db.changes().insert(singleton(c));
-    insertMissingPatchSet(c, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    assertProblems(c, "Destination repository not found: otherproject");
+    // NoteDb can't have a change without a repo.
+    assume().that(notesMigration.enabled()).isFalse();
+
+    ChangeControl ctl = insertChange();
+    Project.NameKey name = ctl.getProject().getNameKey();
+    ((InMemoryRepositoryManager) repoManager).deleteRepository(name);
+
+    assertProblems(
+        ctl, null,
+        problem("Destination repository not found: " + name));
   }
 
   @Test
   public void invalidRevision() throws Exception {
-    Change c = insertChange();
+    // NoteDb always parses the revision when inserting a patch set, so we can't
+    // create an invalid patch set.
+    assume().that(notesMigration.enabled()).isFalse();
 
-    db.patchSets().insert(singleton(newPatchSet(c.currentPatchSetId(),
-            "fooooooooooooooooooooooooooooooooooooooo", adminId)));
-    incrementPatchSet(c);
-    insertPatchSet(c);
+    ChangeControl ctl = insertChange();
+    PatchSet ps = newPatchSet(
+        ctl.getChange().currentPatchSetId(),
+        "fooooooooooooooooooooooooooooooooooooooo",
+        adminId);
+    db.patchSets().update(singleton(ps));
 
-    assertProblems(c,
-        "Invalid revision on patch set 1:"
-        + " fooooooooooooooooooooooooooooooooooooooo");
+    assertProblems(
+        ctl, null,
+        problem("Invalid revision on patch set 1:"
+            + " fooooooooooooooooooooooooooooooooooooooo"));
   }
 
   // No test for ref existing but object missing; InMemoryRepository won't let
@@ -151,414 +176,526 @@
 
   @Test
   public void patchSetObjectAndRefMissing() throws Exception {
-    Change c = insertChange();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(),
-        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), adminId);
-    db.patchSets().insert(singleton(ps));
-
-    assertProblems(c,
-        "Ref missing: " + ps.getId().toRefName(),
-        "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    ChangeControl ctl = insertChange();
+    PatchSet ps = insertMissingPatchSet(ctl, rev);
+    ctl = reload(ctl);
+    assertProblems(
+        ctl, null,
+        problem("Ref missing: " + ps.getId().toRefName()),
+        problem(
+            "Object missing: patch set 2:"
+            + " deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
   }
 
   @Test
   public void patchSetObjectAndRefMissingWithFix() throws Exception {
-    Change c = insertChange();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(),
-        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), adminId);
-    db.patchSets().insert(singleton(ps));
+    String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    ChangeControl ctl = insertChange();
+    PatchSet ps = insertMissingPatchSet(ctl, rev);
+    ctl = reload(ctl);
 
     String refName = ps.getId().toRefName();
-    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo("Ref missing: " + refName);
-    assertThat(p.status).isNull();
+    assertProblems(
+        ctl, new FixInput(),
+        problem("Ref missing: " + refName),
+        problem("Object missing: patch set 2: " + rev));
   }
 
   @Test
   public void patchSetRefMissing() throws Exception {
-    Change c = insertChange();
-    PatchSet ps = insertPatchSet(c);
-    String refName = ps.getId().toRefName();
-    testRepo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
+    ChangeControl ctl = insertChange();
+    testRepo.update(
+        "refs/other/foo",
+        ObjectId.fromString(
+            psUtil.current(db, ctl.getNotes()).getRevision().get()));
+    String refName = ctl.getChange().currentPatchSetId().toRefName();
     deleteRef(refName);
 
-    assertProblems(c, "Ref missing: " + refName);
+    assertProblems(ctl, null, problem("Ref missing: " + refName));
   }
 
   @Test
   public void patchSetRefMissingWithFix() throws Exception {
-    Change c = insertChange();
-    PatchSet ps = insertPatchSet(c);
-    String refName = ps.getId().toRefName();
-    testRepo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
+    ChangeControl ctl = insertChange();
+    String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+    testRepo.update("refs/other/foo", ObjectId.fromString(rev));
+    String refName = ctl.getChange().currentPatchSetId().toRefName();
     deleteRef(refName);
 
-    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo("Ref missing: " + refName);
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(p.outcome).isEqualTo("Repaired patch set ref");
-
+    assertProblems(
+        ctl, new FixInput(),
+        problem("Ref missing: " + refName, FIXED, "Repaired patch set ref"));
     assertThat(testRepo.getRepository().exactRef(refName).getObjectId().name())
-        .isEqualTo(ps.getRevision().get());
+        .isEqualTo(rev);
   }
 
   @Test
   public void patchSetObjectAndRefMissingWithDeletingPatchSet()
       throws Exception {
-    Change c = insertChange();
-    PatchSet ps1 = insertPatchSet(c);
-    incrementPatchSet(c);
-    PatchSet ps2 = insertMissingPatchSet(c,
-        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    ChangeControl ctl = insertChange();
+    PatchSet ps1 = psUtil.current(db, ctl.getNotes());
+
+    String rev2 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    PatchSet ps2 = insertMissingPatchSet(ctl, rev2);
+    ctl = reload(ctl);
 
     FixInput fix = new FixInput();
     fix.deletePatchSetIfCommitMissing = true;
-    List<ProblemInfo> problems = checker.check(c, fix).problems();
-    assertThat(problems).hasSize(2);
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo("Ref missing: " + ps2.getId().toRefName());
-    assertThat(p.status).isNull();
-    p = problems.get(1);
-    assertThat(p.message).isEqualTo(
-        "Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(p.outcome).isEqualTo("Deleted patch set");
+    assertProblems(
+        ctl, fix,
+        problem("Ref missing: " + ps2.getId().toRefName()),
+        problem("Object missing: patch set 2: " + rev2,
+            FIXED, "Deleted patch set"));
 
-    c = notesFactory.createChecked(db, c).getChange();
-    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
-    assertThat(getPatchSet(ps1.getId())).isNotNull();
-    assertThat(getPatchSet(ps2.getId())).isNull();
+    ctl = reload(ctl);
+    assertThat(ctl.getChange().currentPatchSetId().get()).isEqualTo(1);
+    assertThat(psUtil.get(db, ctl.getNotes(), ps1.getId())).isNotNull();
+    assertThat(psUtil.get(db, ctl.getNotes(), ps2.getId())).isNull();
   }
 
   @Test
   public void patchSetMultipleObjectsMissingWithDeletingPatchSets()
       throws Exception {
-    Change c = insertChange();
-    PatchSet ps1 = insertPatchSet(c);
+    ChangeControl ctl = insertChange();
+    PatchSet ps1 = psUtil.current(db, ctl.getNotes());
 
-    incrementPatchSet(c);
-    PatchSet ps2 = insertMissingPatchSet(c,
-        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    String rev2 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    PatchSet ps2 = insertMissingPatchSet(ctl, rev2);
 
-    incrementPatchSet(c);
-    PatchSet ps3 = insertPatchSet(c);
+    ctl = incrementPatchSet(reload(ctl));
+    PatchSet ps3 = psUtil.current(db, ctl.getNotes());
 
-    incrementPatchSet(c);
-    PatchSet ps4 = insertMissingPatchSet(c,
-        "c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee");
+    String rev4 = "c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee";
+    PatchSet ps4 = insertMissingPatchSet(ctl, rev4);
+    ctl = reload(ctl);
 
     FixInput fix = new FixInput();
     fix.deletePatchSetIfCommitMissing = true;
-    List<ProblemInfo> problems = checker.check(c, fix).problems();
-    assertThat(problems).hasSize(4);
+    assertProblems(
+        ctl, fix,
+        problem("Ref missing: " + ps2.getId().toRefName()),
+        problem("Object missing: patch set 2: " + rev2,
+            FIXED, "Deleted patch set"),
+        problem("Ref missing: " + ps4.getId().toRefName()),
+        problem("Object missing: patch set 4: " + rev4,
+            FIXED, "Deleted patch set"));
 
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo("Ref missing: " + ps4.getId().toRefName());
-    assertThat(p.status).isNull();
-
-    p = problems.get(1);
-    assertThat(p.message).isEqualTo(
-        "Object missing: patch set 4: c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee");
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(p.outcome).isEqualTo("Deleted patch set");
-
-    p = problems.get(2);
-    assertThat(p.message).isEqualTo("Ref missing: " + ps2.getId().toRefName());
-    assertThat(p.status).isNull();
-
-    p = problems.get(3);
-    assertThat(p.message).isEqualTo(
-        "Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(p.outcome).isEqualTo("Deleted patch set");
-
-    c = notesFactory.createChecked(db, c).getChange();
-    assertThat(c.currentPatchSetId().get()).isEqualTo(3);
-    assertThat(getPatchSet(ps1.getId())).isNotNull();
-    assertThat(getPatchSet(ps2.getId())).isNull();
-    assertThat(getPatchSet(ps3.getId())).isNotNull();
-    assertThat(getPatchSet(ps4.getId())).isNull();
+    ctl = reload(ctl);
+    assertThat(ctl.getChange().currentPatchSetId().get()).isEqualTo(3);
+    assertThat(psUtil.get(db, ctl.getNotes(), ps1.getId())).isNotNull();
+    assertThat(psUtil.get(db, ctl.getNotes(), ps2.getId())).isNull();
+    assertThat(psUtil.get(db, ctl.getNotes(), ps3.getId())).isNotNull();
+    assertThat(psUtil.get(db, ctl.getNotes(), ps4.getId())).isNull();
   }
 
   @Test
   public void onlyPatchSetObjectMissingWithFix() throws Exception {
-    Change c = insertChange();
-    PatchSet ps1 = insertMissingPatchSet(c,
-        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    Change c = TestChanges.newChange(
+        project, admin.getId(), sequences.nextChangeId());
+    PatchSet.Id psId = c.currentPatchSetId();
+    String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    PatchSet ps = newPatchSet(psId, rev, adminId);
+
+    db.changes().insert(singleton(c));
+    db.patchSets().insert(singleton(ps));
+    addNoteDbCommit(
+        c.getId(),
+        "Create change\n"
+            + "\n"
+            + "Patch-set: 1\n"
+            + "Branch: " + c.getDest().get() + "\n"
+            + "Change-id: " + c.getKey().get() + "\n"
+            + "Subject: Bogus subject\n"
+            + "Commit: " + rev + "\n"
+            + "Groups: " + rev + "\n");
+    indexer.index(db, c.getProject(), c.getId());
+    IdentifiedUser user = userFactory.create(admin.getId());
+    ChangeControl ctl = changeControlFactory.controlFor(
+        db, c.getProject(), c.getId(), user);
 
     FixInput fix = new FixInput();
     fix.deletePatchSetIfCommitMissing = true;
-    List<ProblemInfo> problems = checker.check(c, fix).problems();
-    assertThat(problems).hasSize(2);
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo("Ref missing: " + ps1.getId().toRefName());
-    assertThat(p.status).isNull();
-    p = problems.get(1);
-    assertThat(p.message).isEqualTo(
-        "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIX_FAILED);
-    assertThat(p.outcome)
-        .isEqualTo("Cannot delete patch set; no patch sets would remain");
+    assertProblems(
+        ctl, fix,
+        problem("Ref missing: " + ps.getId().toRefName()),
+        problem("Object missing: patch set 1: " + rev,
+            FIX_FAILED, "Cannot delete patch set; no patch sets would remain"));
 
-    c = notesFactory.createChecked(db, c).getChange();
-    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
-    assertThat(getPatchSet(ps1.getId())).isNotNull();
+    ctl = reload(ctl);
+    assertThat(ctl.getChange().currentPatchSetId().get()).isEqualTo(1);
+    assertThat(psUtil.current(db, ctl.getNotes())).isNotNull();
   }
 
   @Test
   public void currentPatchSetMissing() throws Exception {
-    Change c = insertChange();
-    assertProblems(c, "Current patch set 1 not found");
+    // NoteDb can't create a change without a patch set.
+    assume().that(notesMigration.enabled()).isFalse();
+
+    ChangeControl ctl = insertChange();
+    db.patchSets().deleteKeys(singleton(ctl.getChange().currentPatchSetId()));
+    assertProblems(ctl, null, problem("Current patch set 1 not found"));
   }
 
   @Test
   public void duplicatePatchSetRevisions() throws Exception {
-    Change c = insertChange();
-    PatchSet ps1 = insertPatchSet(c);
+    ChangeControl ctl = insertChange();
+    PatchSet ps1 = psUtil.current(db, ctl.getNotes());
     String rev = ps1.getRevision().get();
-    incrementPatchSet(c);
-    PatchSet ps2 = insertMissingPatchSet(c, rev);
-    updatePatchSetRef(ps2);
 
-    assertProblems(c,
-        "Multiple patch sets pointing to " + rev + ": [1, 2]");
+    ctl = incrementPatchSet(
+        ctl, testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
+
+    assertProblems(
+        ctl, null,
+        problem("Multiple patch sets pointing to " + rev + ": [1, 2]"));
   }
 
   @Test
   public void missingDestRef() throws Exception {
+    ChangeControl ctl = insertChange();
+
     String ref = "refs/heads/master";
     // Detach head so we're allowed to delete ref.
     testRepo.reset(testRepo.getRepository().exactRef(ref).getObjectId());
     RefUpdate ru = testRepo.getRepository().updateRef(ref);
     ru.setForceUpdate(true);
     assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
-    Change c = insertChange();
-    RevCommit commit = testRepo.commit().create();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
-    updatePatchSetRef(ps);
-    db.patchSets().insert(singleton(ps));
 
-    assertProblems(c, "Destination ref not found (may be new branch): " + ref);
+    assertProblems(
+        ctl, null,
+        problem("Destination ref not found (may be new branch): " + ref));
   }
 
   @Test
   public void mergedChangeIsNotMerged() throws Exception {
-    Change c = insertChange();
-    c.setStatus(Change.Status.MERGED);
-    PatchSet ps = insertPatchSet(c);
-    String rev = ps.getRevision().get();
+    ChangeControl ctl = insertChange();
 
-    assertProblems(c,
-        "Patch set 1 (" + rev + ") is not merged into destination ref"
-        + " refs/heads/master (" + tip.name()
-        + "), but change status is MERGED");
+    try (BatchUpdate bu = newUpdate(adminId)) {
+      bu.addOp(ctl.getId(), new BatchUpdate.Op() {
+        @Override
+        public boolean updateChange(ChangeContext ctx) throws OrmException {
+          ctx.getChange().setStatus(Change.Status.MERGED);
+          ctx.getUpdate(ctx.getChange().currentPatchSetId())
+            .fixStatus(Change.Status.MERGED);
+          return true;
+        }
+      });
+      bu.execute();
+    }
+    ctl = reload(ctl);
+
+    String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+    ObjectId tip = getDestRef(ctl);
+    assertProblems(
+        ctl, null,
+        problem(
+            "Patch set 1 (" + rev + ") is not merged into destination ref"
+                + " refs/heads/master (" + tip.name()
+                + "), but change status is MERGED"));
   }
 
   @Test
   public void newChangeIsMerged() throws Exception {
-    Change c = insertChange();
-    RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
-    db.patchSets().insert(singleton(ps));
-    testRepo.branch(c.getDest().get()).update(commit);
+    ChangeControl ctl = insertChange();
+    String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+    testRepo.branch(ctl.getChange().getDest().get())
+        .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
 
-    assertProblems(c,
-        "Patch set 1 (" + commit.name() + ") is merged into destination ref"
-        + " refs/heads/master (" + commit.name()
-        + "), but change status is NEW");
+    assertProblems(
+        ctl, null,
+        problem(
+            "Patch set 1 (" + rev + ") is merged into destination ref"
+                + " refs/heads/master (" + rev
+                + "), but change status is NEW"));
   }
 
   @Test
   public void newChangeIsMergedWithFix() throws Exception {
-    Change c = insertChange();
-    RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
-    db.patchSets().insert(singleton(ps));
-    testRepo.branch(c.getDest().get()).update(commit);
+    ChangeControl ctl = insertChange();
+    String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+    testRepo.branch(ctl.getChange().getDest().get())
+        .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
 
-    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
-    assertThat(problems).hasSize(1);
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo(
-        "Patch set 1 (" + commit.name() + ") is merged into destination ref"
-        + " refs/heads/master (" + commit.name()
-        + "), but change status is NEW");
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(p.outcome).isEqualTo("Marked change as merged");
+    assertProblems(
+        ctl, new FixInput(),
+        problem(
+            "Patch set 1 (" + rev + ") is merged into destination ref"
+                + " refs/heads/master (" + rev
+                + "), but change status is NEW",
+            FIXED, "Marked change as merged"));
 
-    c = notesFactory.createChecked(db, c).getChange();
-    assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED);
-    assertProblems(c);
+    ctl = reload(ctl);
+    assertThat(ctl.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertNoProblems(ctl, null);
   }
 
   @Test
   public void extensionApiReturnsUpdatedValueAfterFix() throws Exception {
-    Change c = insertChange();
-    RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
-    db.patchSets().insert(singleton(ps));
-    testRepo.branch(c.getDest().get()).update(commit);
+    ChangeControl ctl = insertChange();
+    String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+    testRepo.branch(ctl.getChange().getDest().get())
+        .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
 
     ChangeInfo info = gApi.changes()
-        .id(c.getChangeId())
+        .id(ctl.getId().get())
         .info();
     assertThat(info.status).isEqualTo(ChangeStatus.NEW);
 
     info = gApi.changes()
-        .id(c.getChangeId())
+        .id(ctl.getId().get())
         .check(new FixInput());
     assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
   }
 
   @Test
   public void expectedMergedCommitIsLatestPatchSet() throws Exception {
-    Change c = insertChange();
-    c.setStatus(Change.Status.MERGED);
-    PatchSet ps = insertPatchSet(c);
-    RevCommit commit = parseCommit(ps);
-    testRepo.update(c.getDest().get(), commit);
+    ChangeControl ctl = insertChange();
+    String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+    testRepo.branch(ctl.getChange().getDest().get())
+        .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
 
     FixInput fix = new FixInput();
-    fix.expectMergedAs = commit.name();
-    assertThat(checker.check(c, fix).problems()).isEmpty();
+    fix.expectMergedAs = rev;
+    assertProblems(
+        ctl, fix,
+        problem(
+            "Patch set 1 (" + rev + ") is merged into destination ref"
+                + " refs/heads/master (" + rev + "), but change status is NEW",
+            FIXED, "Marked change as merged"));
+
+    ctl = reload(ctl);
+    assertThat(ctl.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertNoProblems(ctl, null);
   }
 
   @Test
   public void expectedMergedCommitNotMergedIntoDestination() throws Exception {
-    Change c = insertChange();
-    c.setStatus(Change.Status.MERGED);
-    PatchSet ps = insertPatchSet(c);
-    RevCommit commit = parseCommit(ps);
-    testRepo.update(c.getDest().get(), commit);
+    ChangeControl ctl = insertChange();
+    String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+    RevCommit commit =
+        testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
+    testRepo.branch(ctl.getChange().getDest().get()).update(commit);
 
     FixInput fix = new FixInput();
     RevCommit other =
         testRepo.commit().message(commit.getFullMessage()).create();
     fix.expectMergedAs = other.name();
-    List<ProblemInfo> problems = checker.check(c, fix).problems();
-    assertThat(problems).hasSize(1);
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo(
-        "Expected merged commit " + other.name()
-        + " is not merged into destination ref refs/heads/master"
-        + " (" + commit.name() + ")");
+    assertProblems(
+        ctl, fix,
+        problem(
+            "Expected merged commit " + other.name()
+                + " is not merged into destination ref refs/heads/master"
+                + " (" + commit.name() + ")"));
   }
 
   @Test
   public void createNewPatchSetForExpectedMergeCommitWithNoChangeId()
       throws Exception {
-    Change c = insertChange();
-    c.setStatus(Change.Status.MERGED);
-    RevCommit parent =
-        testRepo.branch(c.getDest().get()).commit().message("parent").create();
-    PatchSet ps = insertPatchSet(c);
-    RevCommit commit = parseCommit(ps);
+    ChangeControl ctl = insertChange();
+    String dest = ctl.getChange().getDest().get();
+    String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+    RevCommit commit =
+        testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
 
-    RevCommit mergedAs = testRepo.commit().parent(parent)
+    RevCommit mergedAs = testRepo.commit().parent(commit.getParent(0))
         .message(commit.getShortMessage()).create();
     testRepo.getRevWalk().parseBody(mergedAs);
     assertThat(mergedAs.getFooterLines(FooterConstants.CHANGE_ID)).isEmpty();
-    testRepo.update(c.getDest().get(), mergedAs);
+    testRepo.update(dest, mergedAs);
 
-    assertProblems(c, "Patch set 1 (" + commit.name() + ") is not merged into"
-        + " destination ref refs/heads/master (" + mergedAs.name()
-        + "), but change status is MERGED");
+    assertNoProblems(ctl, null);
 
     FixInput fix = new FixInput();
     fix.expectMergedAs = mergedAs.name();
-    List<ProblemInfo> problems = checker.check(c, fix).problems();
-    assertThat(problems).hasSize(1);
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo(
-        "No patch set found for merged commit " + mergedAs.name());
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(p.outcome).isEqualTo("Inserted as patch set 2");
+    assertProblems(
+        ctl, fix,
+        problem(
+            "No patch set found for merged commit " + mergedAs.name(),
+            FIXED, "Marked change as merged"),
+        problem(
+            "Expected merged commit " + mergedAs.name()
+                + " has no associated patch set",
+            FIXED, "Inserted as patch set 2"));
 
-    c = notesFactory.createChecked(db, c).getChange();
-    PatchSet.Id psId2 = new PatchSet.Id(c.getId(), 2);
-    assertThat(c.currentPatchSetId()).isEqualTo(psId2);
-    assertThat(getPatchSet(psId2).getRevision().get())
+    ctl = reload(ctl);
+    PatchSet.Id psId2 = new PatchSet.Id(ctl.getId(), 2);
+    assertThat(ctl.getChange().currentPatchSetId()).isEqualTo(psId2);
+    assertThat(psUtil.get(db, ctl.getNotes(), psId2).getRevision().get())
         .isEqualTo(mergedAs.name());
 
-    assertProblems(c);
+    assertNoProblems(ctl, null);
   }
 
   @Test
   public void createNewPatchSetForExpectedMergeCommitWithChangeId()
       throws Exception {
-    Change c = insertChange();
-    c.setStatus(Change.Status.MERGED);
-    RevCommit parent =
-        testRepo.branch(c.getDest().get()).commit().message("parent").create();
-    PatchSet ps = insertPatchSet(c);
-    RevCommit commit = parseCommit(ps);
+    ChangeControl ctl = insertChange();
+    String dest = ctl.getChange().getDest().get();
+    String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+    RevCommit commit =
+        testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
 
-    RevCommit mergedAs = testRepo.commit().parent(parent)
+    RevCommit mergedAs = testRepo.commit().parent(commit.getParent(0))
         .message(commit.getShortMessage() + "\n"
             + "\n"
-            + "Change-Id: " + c.getKey().get() + "\n").create();
+            + "Change-Id: " + ctl.getChange().getKey().get() + "\n").create();
     testRepo.getRevWalk().parseBody(mergedAs);
     assertThat(mergedAs.getFooterLines(FooterConstants.CHANGE_ID))
-        .containsExactly(c.getKey().get());
-    testRepo.update(c.getDest().get(), mergedAs);
+        .containsExactly(ctl.getChange().getKey().get());
+    testRepo.update(dest, mergedAs);
 
-    assertProblems(c, "Patch set 1 (" + commit.name() + ") is not merged into"
-        + " destination ref refs/heads/master (" + mergedAs.name()
-        + "), but change status is MERGED");
+    assertNoProblems(ctl, null);
 
     FixInput fix = new FixInput();
     fix.expectMergedAs = mergedAs.name();
-    List<ProblemInfo> problems = checker.check(c, fix).problems();
-    assertThat(problems).hasSize(1);
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo(
-        "No patch set found for merged commit " + mergedAs.name());
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(p.outcome).isEqualTo("Inserted as patch set 2");
+    assertProblems(
+        ctl, fix,
+        problem(
+            "No patch set found for merged commit " + mergedAs.name(),
+            FIXED, "Marked change as merged"),
+        problem(
+            "Expected merged commit " + mergedAs.name()
+                + " has no associated patch set",
+            FIXED, "Inserted as patch set 2"));
 
-    c = notesFactory.createChecked(db, c).getChange();
-    PatchSet.Id psId2 = new PatchSet.Id(c.getId(), 2);
-    assertThat(c.currentPatchSetId()).isEqualTo(psId2);
-    assertThat(getPatchSet(psId2).getRevision().get())
+    ctl = reload(ctl);
+    PatchSet.Id psId2 = new PatchSet.Id(ctl.getId(), 2);
+    assertThat(ctl.getChange().currentPatchSetId()).isEqualTo(psId2);
+    assertThat(psUtil.get(db, ctl.getNotes(), psId2).getRevision().get())
         .isEqualTo(mergedAs.name());
 
-    assertProblems(c);
+    assertNoProblems(ctl, null);
   }
 
   @Test
   public void expectedMergedCommitIsOldPatchSetOfSameChange()
       throws Exception {
-    Change c = insertChange();
-    c.setStatus(Change.Status.MERGED);
-    PatchSet ps1 = insertPatchSet(c);
+    ChangeControl ctl = insertChange();
+    PatchSet ps1 = psUtil.current(db, ctl.getNotes());
     String rev1 = ps1.getRevision().get();
-    incrementPatchSet(c);
-    PatchSet ps2 = insertPatchSet(c);
-    testRepo.branch(c.getDest().get()).update(parseCommit(ps1));
+    ctl = incrementPatchSet(ctl);
+    PatchSet ps2 = psUtil.current(db, ctl.getNotes());
+    testRepo.branch(ctl.getChange().getDest().get())
+        .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev1)));
 
     FixInput fix = new FixInput();
     fix.expectMergedAs = rev1;
-    List<ProblemInfo> problems = checker.check(c, fix).problems();
-    assertThat(problems).hasSize(1);
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo(
-        "Expected merged commit " + rev1 + " corresponds to patch set "
-        + ps1.getId() + ", which is not the current patch set " + ps2.getId());
+    assertProblems(
+        ctl, fix,
+        problem(
+            "No patch set found for merged commit " + rev1,
+            FIXED, "Marked change as merged"),
+        problem(
+            "Expected merge commit " + rev1 + " corresponds to patch set 1,"
+                + " not the current patch set 2",
+            FIXED, "Deleted patch set"),
+        problem(
+            "Expected merge commit " + rev1 + " corresponds to patch set 1,"
+                + " not the current patch set 2",
+            FIXED, "Inserted as patch set 3"));
+
+    ctl = reload(ctl);
+    PatchSet.Id psId3 = new PatchSet.Id(ctl.getId(), 3);
+    assertThat(ctl.getChange().currentPatchSetId()).isEqualTo(psId3);
+    assertThat(ctl.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(psUtil.byChangeAsMap(db, ctl.getNotes()).keySet())
+        .containsExactly(ps2.getId(), psId3);
+    assertThat(psUtil.get(db, ctl.getNotes(), psId3).getRevision().get())
+        .isEqualTo(rev1);
+  }
+
+  @Test
+  public void expectedMergedCommitIsDanglingPatchSetOlderThanCurrent()
+      throws Exception {
+    ChangeControl ctl = insertChange();
+    PatchSet ps1 = psUtil.current(db, ctl.getNotes());
+
+    // Create dangling ref so next ID in the database becomes 3.
+    PatchSet.Id psId2 = new PatchSet.Id(ctl.getId(), 2);
+    RevCommit commit2 = patchSetCommit(psId2);
+    String rev2 = commit2.name();
+    testRepo.branch(psId2.toRefName()).update(commit2);
+
+    ctl = incrementPatchSet(ctl);
+    PatchSet ps3 = psUtil.current(db, ctl.getNotes());
+    assertThat(ps3.getId().get()).isEqualTo(3);
+
+    testRepo.branch(ctl.getChange().getDest().get())
+        .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev2)));
+
+    FixInput fix = new FixInput();
+    fix.expectMergedAs = rev2;
+    assertProblems(
+        ctl, fix,
+        problem(
+            "No patch set found for merged commit " + rev2,
+            FIXED, "Marked change as merged"),
+        problem(
+            "Expected merge commit " + rev2 + " corresponds to patch set 2,"
+                + " not the current patch set 3",
+            FIXED, "Deleted patch set"),
+        problem(
+            "Expected merge commit " + rev2 + " corresponds to patch set 2,"
+                + " not the current patch set 3",
+            FIXED, "Inserted as patch set 4"));
+
+    ctl = reload(ctl);
+    PatchSet.Id psId4 = new PatchSet.Id(ctl.getId(), 4);
+    assertThat(ctl.getChange().currentPatchSetId()).isEqualTo(psId4);
+    assertThat(ctl.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(psUtil.byChangeAsMap(db, ctl.getNotes()).keySet())
+        .containsExactly(ps1.getId(), ps3.getId(), psId4);
+    assertThat(psUtil.get(db, ctl.getNotes(), psId4).getRevision().get())
+        .isEqualTo(rev2);
+  }
+
+  @Test
+  public void expectedMergedCommitIsDanglingPatchSetNewerThanCurrent()
+      throws Exception {
+    ChangeControl ctl = insertChange();
+    PatchSet ps1 = psUtil.current(db, ctl.getNotes());
+
+    // Create dangling ref with no patch set.
+    PatchSet.Id psId2 = new PatchSet.Id(ctl.getId(), 2);
+    RevCommit commit2 = patchSetCommit(psId2);
+    String rev2 = commit2.name();
+    testRepo.branch(psId2.toRefName()).update(commit2);
+
+    testRepo.branch(ctl.getChange().getDest().get())
+        .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev2)));
+
+    FixInput fix = new FixInput();
+    fix.expectMergedAs = rev2;
+    assertProblems(
+        ctl, fix,
+        problem(
+            "No patch set found for merged commit " + rev2,
+            FIXED, "Marked change as merged"),
+        problem(
+            "Expected merge commit " + rev2 + " corresponds to patch set 2,"
+                + " not the current patch set 1",
+            FIXED, "Inserted as patch set 2"));
+
+    ctl = reload(ctl);
+    assertThat(ctl.getChange().currentPatchSetId()).isEqualTo(psId2);
+    assertThat(ctl.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(psUtil.byChangeAsMap(db, ctl.getNotes()).keySet())
+        .containsExactly(ps1.getId(), psId2);
+    assertThat(psUtil.get(db, ctl.getNotes(), psId2).getRevision().get())
+        .isEqualTo(rev2);
   }
 
   @Test
   public void expectedMergedCommitWithMismatchedChangeId() throws Exception {
-    Change c = insertChange();
-    c.setStatus(Change.Status.MERGED);
+    ChangeControl ctl = insertChange();
+    String dest = ctl.getChange().getDest().get();
     RevCommit parent =
-        testRepo.branch(c.getDest().get()).commit().message("parent").create();
-    PatchSet ps = insertPatchSet(c);
-    RevCommit commit = parseCommit(ps);
+        testRepo.branch(dest).commit().message("parent").create();
+    String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+    RevCommit commit =
+        testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
+    testRepo.branch(dest).update(commit);
 
     String badId = "I0000000000000000000000000000000000000000";
     RevCommit mergedAs = testRepo.commit().parent(parent)
@@ -567,85 +704,141 @@
             + "Change-Id: " + badId + "\n")
         .create();
     testRepo.getRevWalk().parseBody(mergedAs);
-    testRepo.update(c.getDest().get(), mergedAs);
+    assertThat(mergedAs.getFooterLines(FooterConstants.CHANGE_ID))
+        .containsExactly(badId);
+    testRepo.update(dest, mergedAs);
+
+    assertNoProblems(ctl, null);
 
     FixInput fix = new FixInput();
     fix.expectMergedAs = mergedAs.name();
-    List<ProblemInfo> problems = checker.check(c, fix).problems();
-    assertThat(problems).hasSize(1);
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo(
-        "Expected merged commit " + mergedAs.name() + " has Change-Id: "
-        + badId + ", but expected " + c.getKey().get());
+    assertProblems(
+        ctl, fix,
+        problem(
+            "Expected merged commit " + mergedAs.name() + " has Change-Id: "
+                + badId + ", but expected " + ctl.getChange().getKey().get()));
   }
 
   @Test
   public void expectedMergedCommitMatchesMultiplePatchSets()
       throws Exception {
-    Change c1 = insertChange();
-    c1.setStatus(Change.Status.MERGED);
-    insertPatchSet(c1);
+    ChangeControl ctl1 = insertChange();
+    PatchSet.Id psId1 = psUtil.current(db, ctl1.getNotes()).getId();
+    String dest = ctl1.getChange().getDest().get();
+    String rev = psUtil.current(db, ctl1.getNotes()).getRevision().get();
+    RevCommit commit =
+        testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
+    testRepo.branch(dest).update(commit);
 
-    RevCommit commit = testRepo.branch(c1.getDest().get()).commit().create();
-    Change c2 = insertChange();
-    PatchSet ps2 = newPatchSet(c2.currentPatchSetId(), commit, adminId);
-    updatePatchSetRef(ps2);
-    db.patchSets().insert(singleton(ps2));
+    ChangeControl ctl2 = insertChange();
+    ctl2 = incrementPatchSet(ctl2, commit);
+    PatchSet.Id psId2 = psUtil.current(db, ctl2.getNotes()).getId();
 
-    Change c3 = insertChange();
-    PatchSet ps3 = newPatchSet(c3.currentPatchSetId(), commit, adminId);
-    updatePatchSetRef(ps3);
-    db.patchSets().insert(singleton(ps3));
+    ChangeControl ctl3 = insertChange();
+    ctl3 = incrementPatchSet(ctl3, commit);
+    PatchSet.Id psId3 = psUtil.current(db, ctl3.getNotes()).getId();
 
     FixInput fix = new FixInput();
     fix.expectMergedAs = commit.name();
-    List<ProblemInfo> problems = checker.check(c1, fix).problems();
-    assertThat(problems).hasSize(1);
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo(
-        "Multiple patch sets for expected merged commit " + commit.name()
-        + ": [" + ps2.getId() + ", " + ps3.getId() + "]");
+    assertProblems(
+        ctl1, fix,
+        problem(
+            "Multiple patch sets for expected merged commit " + commit.name()
+                + ": [" + psId1 + ", " + psId2 + ", " + psId3 + "]"));
   }
 
-  private Change insertChange() throws Exception {
-    Change c = newChange(project, adminId);
-    db.changes().insert(singleton(c));
-    indexer.index(db, c);
-
-    ChangeUpdate u = changeUpdateFactory.create(
-        changeControlFactory.controlFor(db, c, userFactory.create(adminId)));
-    u.setBranch(c.getDest().get());
-    u.setChangeId(c.getKey().get());
-    u.commit();
-
-    return c;
+  private BatchUpdate newUpdate(Account.Id owner) {
+    return updateFactory.create(
+        db, project, userFactory.create(owner), TimeUtil.nowTs());
   }
 
-  private void incrementPatchSet(Change c) throws Exception {
-    TestChanges.incrementPatchSet(c);
-    db.changes().upsert(singleton(c));
+  private ChangeControl insertChange() throws Exception {
+    return insertChange(admin);
   }
 
-  private PatchSet insertPatchSet(Change c) throws Exception {
-    db.changes().upsert(singleton(c));
-    RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).message("Change " + c.getId().get()).create();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
-    updatePatchSetRef(ps);
+
+  private ChangeControl insertChange(TestAccount owner) throws Exception {
+    return insertChange(owner, "refs/heads/master");
+  }
+
+  private ChangeControl insertChange(TestAccount owner, String dest)
+      throws Exception {
+    Change.Id id = new Change.Id(sequences.nextChangeId());
+    ChangeInserter ins;
+    try (BatchUpdate bu = newUpdate(owner.getId())) {
+      RevCommit commit = patchSetCommit(new PatchSet.Id(id, 1));
+      ins = changeInserterFactory
+          .create(id, commit, dest)
+          .setValidatePolicy(CommitValidators.Policy.NONE)
+          .setNotify(NotifyHandling.NONE)
+          .setFireRevisionCreated(false)
+          .setSendMail(false);
+      bu.insertChange(ins).execute();
+    }
+    // Return control for admin regardless of owner.
+    return changeControlFactory.controlFor(
+        db, ins.getChange(), userFactory.create(adminId));
+  }
+
+  private PatchSet.Id nextPatchSetId(ChangeControl ctl) throws Exception {
+    return ChangeUtil.nextPatchSetId(
+        testRepo.getRepository(), ctl.getChange().currentPatchSetId());
+  }
+
+  private ChangeControl incrementPatchSet(ChangeControl ctl) throws Exception {
+    return incrementPatchSet(ctl, patchSetCommit(nextPatchSetId(ctl)));
+  }
+
+  private ChangeControl incrementPatchSet(ChangeControl ctl,
+      RevCommit commit) throws Exception {
+    PatchSetInserter ins;
+    try (BatchUpdate bu = newUpdate(ctl.getChange().getOwner())) {
+      ins = patchSetInserterFactory.create(ctl, nextPatchSetId(ctl), commit)
+          .setValidatePolicy(CommitValidators.Policy.NONE)
+          .setFireRevisionCreated(false)
+          .setSendMail(false);
+      bu.addOp(ctl.getId(), ins).execute();
+    }
+    return reload(ctl);
+  }
+
+  private ChangeControl reload(ChangeControl ctl) throws Exception {
+    return changeControlFactory.controlFor(
+        db, ctl.getChange().getProject(), ctl.getId(), ctl.getUser());
+  }
+
+  private RevCommit patchSetCommit(PatchSet.Id psId) throws Exception {
+    RevCommit c = testRepo
+        .commit()
+        .parent(tip)
+        .message("Change " + psId)
+        .create();
+    return testRepo.parseBody(c);
+  }
+
+  private PatchSet insertMissingPatchSet(ChangeControl ctl, String rev)
+      throws Exception {
+    // Don't use BatchUpdate since we're manually updating the meta ref rather
+    // than using ChangeUpdate.
+    String subject = "Subject for missing commit";
+    Change c = new Change(ctl.getChange());
+    PatchSet.Id psId = nextPatchSetId(ctl);
+    c.setCurrentPatchSet(psId, subject, c.getOriginalSubject());
+
+    PatchSet ps = newPatchSet(psId, rev, adminId);
     db.patchSets().insert(singleton(ps));
-    return ps;
-  }
+    db.changes().update(singleton(c));
 
-  private PatchSet insertMissingPatchSet(Change c, String id) throws Exception {
-    PatchSet ps = newPatchSet(c.currentPatchSetId(),
-        ObjectId.fromString(id), adminId);
-    db.patchSets().insert(singleton(ps));
-    return ps;
-  }
+    addNoteDbCommit(
+        c.getId(),
+        "Update patch set " + psId.get() + "\n"
+            + "\n"
+            + "Patch-set: " + psId.get() + "\n"
+            + "Commit: " + rev + "\n"
+            + "Subject: " + subject + "\n");
+    indexer.index(db, c.getProject(), c.getId());
 
-  private void updatePatchSetRef(PatchSet ps) throws Exception {
-    testRepo.update(ps.getId().toRefName(),
-        ObjectId.fromString(ps.getRevision().get()));
+    return ps;
   }
 
   private void deleteRef(String refName) throws Exception {
@@ -654,22 +847,82 @@
     assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
   }
 
-  private RevCommit parseCommit(PatchSet ps) throws Exception {
-    RevCommit commit = testRepo.getRevWalk()
-        .parseCommit(ObjectId.fromString(ps.getRevision().get()));
-    testRepo.getRevWalk().parseBody(commit);
-    return commit;
+  private void addNoteDbCommit(Change.Id id, String commitMessage)
+      throws Exception {
+    if (!notesMigration.commitChangeWrites()) {
+      return;
+    }
+    PersonIdent committer = serverIdent.get();
+    PersonIdent author = noteUtil.newIdent(
+        accountCache.get(admin.getId()).getAccount(),
+        committer.getWhen(),
+        committer,
+        anonymousCowardName);
+    testRepo.branch(RefNames.changeMetaRef(id))
+        .commit()
+        .author(author)
+        .committer(committer)
+        .message(commitMessage)
+        .create();
   }
 
-  private void assertProblems(Change c, String... expected) {
-    assertThat(Lists.transform(checker.check(c).problems(),
-          new Function<ProblemInfo, String>() {
-            @Override
-            public String apply(ProblemInfo in) {
-              checkArgument(in.status == null,
-                  "Status is not null: " + in.message);
-              return in.message;
-            }
-          })).containsExactly((Object[]) expected);
+  private ObjectId getDestRef(ChangeControl ctl) throws Exception {
+    return testRepo.getRepository()
+        .exactRef(ctl.getChange().getDest().get())
+        .getObjectId();
+  }
+
+  private ChangeControl mergeChange(ChangeControl ctl) throws Exception {
+    final ObjectId oldId = getDestRef(ctl);
+    final ObjectId newId = ObjectId.fromString(
+        psUtil.current(db, ctl.getNotes()).getRevision().get());
+    final String dest = ctl.getChange().getDest().get();
+
+    try (BatchUpdate bu = newUpdate(adminId)) {
+      bu.addOp(ctl.getId(), new BatchUpdate.Op() {
+        @Override
+        public void updateRepo(RepoContext ctx) throws IOException {
+          ctx.addRefUpdate(new ReceiveCommand(oldId, newId, dest));
+        }
+
+        @Override
+        public boolean updateChange(ChangeContext ctx) throws OrmException {
+          ctx.getChange().setStatus(Change.Status.MERGED);
+          ctx.getUpdate(ctx.getChange().currentPatchSetId())
+            .fixStatus(Change.Status.MERGED);
+          return true;
+        }
+      });
+      bu.execute();
+    }
+    return reload(ctl);
+  }
+
+  private static ProblemInfo problem(String message) {
+    ProblemInfo p = new ProblemInfo();
+    p.message = message;
+    return p;
+  }
+
+  private static ProblemInfo problem(String message,
+      ProblemInfo.Status status, String outcome) {
+    ProblemInfo p = problem(message);
+    p.status = checkNotNull(status);
+    p.outcome = checkNotNull(outcome);
+    return p;
+  }
+
+  private void assertProblems(ChangeControl ctl, @Nullable FixInput fix,
+      ProblemInfo first, ProblemInfo... rest) {
+    List<ProblemInfo> expected = new ArrayList<>(1 + rest.length);
+    expected.add(first);
+    expected.addAll(Arrays.asList(rest));
+    assertThat(checker.check(ctl, fix).problems())
+        .containsExactlyElementsIn(expected)
+        .inOrder();
+  }
+
+  private void assertNoProblems(ChangeControl ctl, @Nullable FixInput fix) {
+    assertThat(checker.check(ctl, fix).problems()).isEmpty();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index d8f2885..b443e66 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.change.PostReview;
 import com.google.gerrit.server.change.Rebuild;
 import com.google.gerrit.server.change.RevisionResource;
@@ -58,6 +59,7 @@
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.notedb.ChangeBundle;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeRebuilder.NoPatchSetsException;
 import com.google.gerrit.server.notedb.NoteDbChangeState;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager;
 import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
@@ -118,8 +120,11 @@
   @Inject
   private BatchUpdate.Factory batchUpdateFactory;
 
+  @Inject
+  private Sequences seq;
+
   @Before
-  public void setUp() {
+  public void setUp() throws Exception {
     assume().that(NoteDbMode.readWrite()).isFalse();
     TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
     setNotesMigration(false, false);
@@ -130,10 +135,20 @@
     TestTimeUtil.useSystemTime();
   }
 
-  private void setNotesMigration(boolean writeChanges, boolean readChanges) {
+  @SuppressWarnings("deprecation")
+  private void setNotesMigration(boolean writeChanges, boolean readChanges)
+      throws Exception {
     notesMigration.setWriteChanges(writeChanges);
     notesMigration.setReadChanges(readChanges);
     db = atrScope.reopenDb().getReviewDbProvider().get();
+
+    if (notesMigration.readChangeSequence()) {
+      // Copy next ReviewDb ID to NoteDb.
+      seq.getChangeIdRepoSequence().set(db.nextChangeId());
+    } else {
+      // Copy next NoteDb ID to ReviewDb.
+      while (db.nextChangeId() < seq.getChangeIdRepoSequence().next()) {}
+    }
   }
 
   @Test
@@ -163,8 +178,7 @@
   @Test
   public void patchSetWithNullGroups() throws Exception {
     Timestamp ts = TimeUtil.nowTs();
-    @SuppressWarnings("deprecation")
-    Change c = TestChanges.newChange(project, user.getId(), db.nextChangeId());
+    Change c = TestChanges.newChange(project, user.getId(), seq.nextChangeId());
     c.setCreatedOn(ts);
     c.setLastUpdatedOn(ts);
     PatchSet ps = TestChanges.newPatchSet(
@@ -407,7 +421,7 @@
           PatchSet.Id psId = ctx.getChange().currentPatchSetId();
           ChangeMessage cm = new ChangeMessage(
               new ChangeMessage.Key(id, ChangeUtil.messageUUID(ctx.getDb())),
-                  ctx.getUser().getAccountId(), ctx.getWhen(), psId);
+                  ctx.getAccountId(), ctx.getWhen(), psId);
           cm.setMessage(msg);
           ctx.getDb().changeMessages().insert(Collections.singleton(cm));
           ctx.getUpdate(psId).setChangeMessage(msg);
@@ -941,6 +955,22 @@
     assertChangeUpToDate(false, id);
   }
 
+  @Test
+  public void rebuildChangeWithNoPatchSets() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    db.changes().beginTransaction(id);
+    try {
+      db.patchSets().delete(db.patchSets().byChange(id));
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+
+    exception.expect(NoPatchSetsException.class);
+    checker.rebuildAndCheckChanges(id);
+  }
+
   private void assertChangesReadOnly(RestApiException e) throws Exception {
     Throwable cause = e.getCause();
     assertThat(cause).isInstanceOf(UpdateException.class);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
deleted file mode 100644
index a69bb19..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
+++ /dev/null
@@ -1,446 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.server.project;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.extensions.api.changes.CherryPickInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.LabelInfo;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.Util;
-
-import org.eclipse.jgit.lib.Repository;
-import org.junit.Before;
-import org.junit.Test;
-
-@NoHttpd
-public class LabelTypeIT extends AbstractDaemonTest {
-  private LabelType codeReview;
-
-  @Before
-  public void setUp() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    codeReview = Util.codeReview();
-    codeReview.setDefaultValue((short)-1);
-    cfg.getLabelSections().put(codeReview.getName(), codeReview);
-    saveProjectConfig(cfg);
-  }
-
-  @Test
-  public void failChangedLabelValueOnClosedChange() throws Exception {
-    PushOneCommit.Result r = createChange();
-    merge(r);
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("change is closed");
-    revision(r).review(ReviewInput.reject());
-  }
-
-  @Test
-  public void noCopyMinScoreOnRework() throws Exception {
-    codeReview.setCopyMinScore(false);
-    saveLabelConfig();
-
-    PushOneCommit.Result r = createChange();
-    revision(r).review(ReviewInput.reject());
-    assertApproval(r, -2);
-    r = amendChange(r.getChangeId());
-    assertApproval(r, 0);
-  }
-
-  @Test
-  public void copyMinScoreOnRework() throws Exception {
-    codeReview.setCopyMinScore(true);
-    saveLabelConfig();
-    PushOneCommit.Result r = createChange();
-    revision(r).review(ReviewInput.reject());
-    assertApproval(r, -2);
-    r = amendChange(r.getChangeId());
-    assertApproval(r, -2);
-  }
-
-  @Test
-  public void noCopyMaxScoreOnRework() throws Exception {
-    PushOneCommit.Result r = createChange();
-    revision(r).review(ReviewInput.approve());
-    assertApproval(r, 2);
-    r = amendChange(r.getChangeId());
-    assertApproval(r, 0);
-  }
-
-  @Test
-  public void copyMaxScoreOnRework() throws Exception {
-    codeReview.setCopyMaxScore(true);
-    saveLabelConfig();
-    PushOneCommit.Result r = createChange();
-    revision(r).review(ReviewInput.approve());
-    assertApproval(r, 2);
-    r = amendChange(r.getChangeId());
-    assertApproval(r, 2);
-  }
-
-  @Test
-  public void noCopyNonMaxScoreOnRework() throws Exception {
-    codeReview.setCopyMinScore(true);
-    codeReview.setCopyMaxScore(true);
-    saveLabelConfig();
-
-    PushOneCommit.Result r = createChange();
-    revision(r).review(ReviewInput.recommend());
-    assertApproval(r, 1);
-    r = amendChange(r.getChangeId());
-    assertApproval(r, 0);
-  }
-
-  @Test
-  public void noCopyNonMinScoreOnRework() throws Exception {
-    codeReview.setCopyMinScore(true);
-    codeReview.setCopyMaxScore(true);
-    saveLabelConfig();
-
-    PushOneCommit.Result r = createChange();
-    revision(r).review(ReviewInput.dislike());
-    assertApproval(r, -1);
-    r = amendChange(r.getChangeId());
-    assertApproval(r, 0);
-  }
-
-  @Test
-  public void noCopyAllScoresIfNoChange() throws Exception {
-    codeReview.setCopyAllScoresIfNoChange(false);
-    saveLabelConfig();
-    PushOneCommit.Result patchSet = readyPatchSetForNoChangeRebase();
-    rebase(patchSet);
-    assertApproval(patchSet, 0);
-  }
-
-  @Test
-  public void copyAllScoresIfNoCodeChangeAppliesToNoChange() throws Exception {
-    codeReview.setCopyAllScoresIfNoCodeChange(true);
-    codeReview.setCopyAllScoresIfNoChange(false);
-    saveLabelConfig();
-
-    PushOneCommit.Result patchSet = readyPatchSetForNoChangeRebase();
-    rebase(patchSet);
-    assertApproval(patchSet, 1);
-  }
-
-  @Test
-  public void copyAllScoresOnTrivialRebaseAppliesToNoChange() throws Exception {
-    codeReview.setCopyAllScoresOnTrivialRebase(true);
-    codeReview.setCopyAllScoresIfNoChange(false);
-    saveLabelConfig();
-
-    PushOneCommit.Result patchSet = readyPatchSetForNoChangeRebase();
-    rebase(patchSet);
-    assertApproval(patchSet, 1);
-  }
-
-  @Test
-  public void copyAllScoresOnMergeFirstParentUpdateAppliesToNoChange()
-      throws Exception {
-    codeReview.setCopyAllScoresOnMergeFirstParentUpdate(true);
-    codeReview.setCopyAllScoresIfNoChange(false);
-    saveLabelConfig();
-
-    PushOneCommit.Result patchSet = readyPatchSetForNoChangeRebase();
-    rebase(patchSet);
-    assertApproval(patchSet, 1);
-  }
-
-  @Test
-  public void copyAllScoresOnMergeFirstParentUpdateAppliesToMergeParentUpdate()
-      throws Exception {
-    codeReview.setCopyAllScoresOnMergeFirstParentUpdate(true);
-    saveLabelConfig();
-
-    PushOneCommit.Result merge = createMergeCommitAndUpdateFirstParent();
-    assertApproval(merge, 1);
-  }
-
-  @Test
-  public void copyAllScoresOnMergeFirstParentUpdateNotAppliesIfNotSet()
-      throws Exception {
-    codeReview.setCopyAllScoresOnMergeFirstParentUpdate(false);
-    saveLabelConfig();
-
-    PushOneCommit.Result merge = createMergeCommitAndUpdateFirstParent();
-    assertApproval(merge, 0);
-  }
-
-  @Test
-  public void copyAllScoresIfNoChange() throws Exception {
-    PushOneCommit.Result patchSet = readyPatchSetForNoChangeRebase();
-    rebase(patchSet);
-    assertApproval(patchSet, 1);
-  }
-
-  @Test
-  public void noCopyAllScoresIfNoCodeChange() throws Exception {
-    String file = "a.txt";
-    String contents = "contents";
-
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
-        "first subject", file, contents);
-    PushOneCommit.Result r = push.to("refs/for/master");
-    revision(r).review(ReviewInput.recommend());
-    assertApproval(r, 1);
-
-    push = pushFactory.create(db, admin.getIdent(), testRepo,
-        "second subject", file, contents, r.getChangeId());
-    r = push.to("refs/for/master");
-    assertApproval(r, 0);
-  }
-
-  @Test
-  public void copyAllScoresIfNoCodeChange() throws Exception {
-    String file = "a.txt";
-    String contents = "contents";
-    codeReview.setCopyAllScoresIfNoCodeChange(true);
-    saveLabelConfig();
-
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
-        "first subject", file, contents);
-    PushOneCommit.Result r = push.to("refs/for/master");
-    revision(r).review(ReviewInput.recommend());
-    assertApproval(r, 1);
-
-    push = pushFactory.create(db, admin.getIdent(), testRepo,
-        "second subject", file, contents, r.getChangeId());
-    r = push.to("refs/for/master");
-    assertApproval(r, 1);
-  }
-
-  @Test
-  public void noCopyAllScoresOnTrivialRebase() throws Exception {
-    String subject = "test commit";
-    String file = "a.txt";
-    String contents = "contents";
-
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
-    PushOneCommit.Result r1 = push.to("refs/for/master");
-    merge(r1);
-
-    push = pushFactory.create(db, admin.getIdent(), testRepo,
-        "non-conflicting", "b.txt", "other contents");
-    PushOneCommit.Result r2 = push.to("refs/for/master");
-    merge(r2);
-
-    testRepo.reset(r1.getCommit());
-    push = pushFactory.create(db, admin.getIdent(), testRepo, subject, file, contents);
-    PushOneCommit.Result r3 = push.to("refs/for/master");
-    revision(r3).review(ReviewInput.recommend());
-    assertApproval(r3, 1);
-
-    rebase(r3);
-    assertApproval(r3, 0);
-  }
-
-  @Test
-  public void copyAllScoresOnTrivialRebase() throws Exception {
-    String subject = "test commit";
-    String file = "a.txt";
-    String contents = "contents";
-    codeReview.setCopyAllScoresOnTrivialRebase(true);
-    saveLabelConfig();
-
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
-    PushOneCommit.Result r1 = push.to("refs/for/master");
-    merge(r1);
-
-    push = pushFactory.create(db, admin.getIdent(), testRepo,
-        "non-conflicting", "b.txt", "other contents");
-    PushOneCommit.Result r2 = push.to("refs/for/master");
-    merge(r2);
-
-    testRepo.reset(r1.getCommit());
-    push = pushFactory.create(db, admin.getIdent(), testRepo, subject, file, contents);
-    PushOneCommit.Result r3 = push.to("refs/for/master");
-    revision(r3).review(ReviewInput.recommend());
-    assertApproval(r3, 1);
-
-    rebase(r3);
-    assertApproval(r3, 1);
-  }
-
-  @Test
-  public void copyAllScoresOnTrivialRebaseAndCherryPick() throws Exception {
-    codeReview.setCopyAllScoresOnTrivialRebase(true);
-    saveLabelConfig();
-
-    PushOneCommit.Result r1 = createChange();
-    testRepo.reset(r1.getCommit());
-
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
-        PushOneCommit.SUBJECT, "b.txt", "other contents");
-    PushOneCommit.Result r2 = push.to("refs/for/master");
-
-    revision(r2).review(ReviewInput.recommend());
-
-    CherryPickInput in = new CherryPickInput();
-    in.destination = "master";
-    in.message = String.format("%s\n\nChange-Id: %s",
-        PushOneCommit.SUBJECT,
-        r2.getChangeId());
-
-    doAssertApproval(1,
-        gApi.changes()
-            .id(r2.getChangeId())
-            .revision(r2.getCommit().name())
-            .cherryPick(in)
-            .get());
-  }
-
-  @Test
-  public void copyNoScoresOnReworkAndCherryPick()
-      throws Exception {
-    codeReview.setCopyAllScoresOnTrivialRebase(true);
-    saveLabelConfig();
-
-    PushOneCommit.Result r1 = createChange();
-
-    testRepo.reset(r1.getCommit());
-
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
-        PushOneCommit.SUBJECT, "b.txt", "other contents");
-    PushOneCommit.Result r2 = push.to("refs/for/master");
-
-    revision(r2).review(ReviewInput.recommend());
-
-    CherryPickInput in = new CherryPickInput();
-    in.destination = "master";
-    in.message = String.format("Cherry pick\n\nChange-Id: %s",
-        r2.getChangeId());
-
-    doAssertApproval(0,
-        gApi.changes()
-            .id(r2.getChangeId())
-            .revision(r2.getCommit().name())
-            .cherryPick(in)
-            .get());
-  }
-
-  private PushOneCommit.Result createMergeCommitAndUpdateFirstParent()
-      throws Exception {
-    String file = "m.txt";
-    String contents = "contents";
-    PushOneCommit.Result base = pushToMaster(file, contents);
-
-    // create feature commit and push it to feature branch
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
-        PushOneCommit.SUBJECT, file, contents + "feature");
-    PushOneCommit.Result feature = push.to("refs/heads/feature");
-
-    // create merge commit and push it for review
-    testRepo.reset(base.getCommit());
-    push = pushFactory.create(db, admin.getIdent(), testRepo,
-        PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT);
-    push.setParents(ImmutableList.of(base.getCommit(), feature.getCommit()));
-    PushOneCommit.Result merge = push.to("refs/for/master");
-    revision(merge).review(ReviewInput.recommend());
-
-    // advance master
-    testRepo.reset(base.getCommit());
-    PushOneCommit.Result advanced = pushToMaster(file, contents + "master_advances");
-
-    // update first parent of merge commit
-    push = pushFactory.create(db, admin.getIdent(), testRepo,
-        PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT,
-        merge.getChangeId());
-    push.setParents(ImmutableList.of(advanced.getCommit(), feature.getCommit()));
-    merge = push.to("refs/for/master");
-    return merge;
-  }
-
-  private PushOneCommit.Result pushToMaster(String file, String contents)
-      throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
-        PushOneCommit.SUBJECT, file, contents);
-    PushOneCommit.Result base = push.to("refs/for/master");
-    merge(base);
-    return base;
-  }
-
-  private PushOneCommit.Result readyPatchSetForNoChangeRebase()
-      throws Exception {
-    String file = "a.txt";
-    String contents = "contents";
-
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
-        PushOneCommit.SUBJECT, file, contents);
-    PushOneCommit.Result base = push.to("refs/for/master");
-    merge(base);
-
-    push = pushFactory.create(db, admin.getIdent(), testRepo,
-        PushOneCommit.SUBJECT, file, contents + "M");
-    PushOneCommit.Result basePlusM = push.to("refs/for/master");
-    merge(basePlusM);
-
-    push = pushFactory.create(db, admin.getIdent(), testRepo,
-        PushOneCommit.SUBJECT, file, contents);
-    PushOneCommit.Result basePlusMMinusM = push.to("refs/for/master");
-    merge(basePlusMMinusM);
-
-    testRepo.reset(base.getCommit());
-    push = pushFactory.create(db, admin.getIdent(), testRepo,
-        PushOneCommit.SUBJECT, file, contents + "MM");
-    PushOneCommit.Result patchSet = push.to("refs/for/master");
-    revision(patchSet).review(ReviewInput.recommend());
-    return patchSet;
-  }
-
-  private void saveLabelConfig() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    cfg.getLabelSections().clear();
-    cfg.getLabelSections().put(codeReview.getName(), codeReview);
-    saveProjectConfig(cfg);
-  }
-
-  @Override
-  protected void merge(PushOneCommit.Result r) throws Exception {
-    super.merge(r);
-    try (Repository repo = repoManager.openRepository(project)) {
-      assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(
-          r.getCommit());
-    }
-  }
-
-  private void rebase(PushOneCommit.Result r) throws Exception {
-    revision(r).rebase();
-  }
-
-  private void assertApproval(PushOneCommit.Result r, int expected)
-      throws Exception {
-    // Don't use asserts from PushOneCommit so we can test the round-trip
-    // through JSON instead of querying the DB directly.
-    ChangeInfo c = get(r.getChangeId());
-    doAssertApproval(expected, c);
-  }
-
-  private void doAssertApproval(int expected, ChangeInfo c) {
-    LabelInfo cr = c.labels.get("Code-Review");
-    assertThat((int) cr.defaultValue).isEqualTo(-1);
-    assertThat(cr.all).hasSize(1);
-    assertThat(cr.all.get(0).name).isEqualTo("Administrator");
-    assertThat(cr.all.get(0).value).isEqualTo(expected);
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index f33c223..c999c9c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -19,14 +19,20 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
 
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.junit.Test;
 
+import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.List;
 
@@ -69,5 +75,81 @@
     assertThat(m.body()).contains("Gerrit-PatchSet: 2\n");
   }
 
-  // TODO(anybody reading this): More tests.
+  @Test
+  public void watchProject() throws Exception {
+    // watch project
+    String watchedProject = createProject("watchedProject").get();
+    setApiUser(user);
+    watch(watchedProject, null);
+
+    // push a change to watched project -> should trigger email notification
+    setApiUser(admin);
+    TestRepository<InMemoryRepository> watchedRepo =
+        cloneProject(new Project.NameKey(watchedProject), admin);
+    PushOneCommit.Result r = pushFactory
+        .create(db, admin.getIdent(), watchedRepo, "TRIGGER", "a", "a1")
+        .to("refs/for/master");
+    r.assertOkStatus();
+
+    // push a change to non-watched project -> should not trigger email
+    // notification
+    String notWatchedProject = createProject("otherProject").get();
+    TestRepository<InMemoryRepository> notWatchedRepo =
+        cloneProject(new Project.NameKey(notWatchedProject), admin);
+    r = pushFactory.create(db, admin.getIdent(), notWatchedRepo,
+        "DONT_TRIGGER", "a", "a1").to("refs/for/master");
+    r.assertOkStatus();
+
+    // assert email notification
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.body()).contains("Change subject: TRIGGER\n");
+    assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
+  }
+
+  @Test
+  public void watchFile() throws Exception {
+    // watch file in project
+    String watchedProject = createProject("watchedProject").get();
+    setApiUser(user);
+    watch(watchedProject, "file:a.txt");
+
+    // push a change to watched file -> should trigger email notification
+    setApiUser(admin);
+    TestRepository<InMemoryRepository> watchedRepo =
+        cloneProject(new Project.NameKey(watchedProject), admin);
+    PushOneCommit.Result r = pushFactory
+        .create(db, admin.getIdent(), watchedRepo, "TRIGGER", "a.txt", "a1")
+        .to("refs/for/master");
+    r.assertOkStatus();
+
+    // push a change to non-watched file -> should not trigger email
+    // notification
+    r = pushFactory.create(db, admin.getIdent(), testRepo,
+        "DONT_TRIGGER", "b.txt", "b1").to("refs/for/master");
+    r.assertOkStatus();
+
+    // assert email notification
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.body()).contains("Change subject: TRIGGER\n");
+    assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
+  }
+
+  private void watch(String project, String filter)
+      throws RestApiException {
+    List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = project;
+    pwi.filter = filter;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateGroupIT.java
deleted file mode 100644
index 7f176cf..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateGroupIT.java
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.ssh;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-
-import org.junit.Test;
-
-public class CreateGroupIT extends AbstractDaemonTest {
-
-  @Test
-  public void withDuplicateInternalGroupCaseSensitiveName_Conflict()
-      throws Exception {
-    String newGroupName = "dupGroupA";
-    adminRestSession.put("/groups/" + newGroupName);
-    adminSshSession.exec("gerrit create-group " + newGroupName);
-    assert_().withFailureMessage(adminSshSession.getError())
-        .that(adminSshSession.hasError()).isTrue();
-  }
-
-  @Test
-  public void withDuplicateInternalGroupCaseInsensitiveName()
-      throws Exception {
-    String newGroupName = "dupGroupB";
-    String newGroupNameLowerCase = newGroupName.toLowerCase();
-
-    adminRestSession.put("/groups/" + newGroupName);
-    adminSshSession.exec("gerrit create-group " + newGroupNameLowerCase);
-    assert_().withFailureMessage(adminSshSession.getError())
-        .that(adminSshSession.hasError()).isFalse();
-    assertThat(groupCache.get(new AccountGroup.NameKey(newGroupName)))
-      .isNotNull();
-    assertThat(groupCache.get(new AccountGroup.NameKey(newGroupNameLowerCase)))
-      .isNotNull();
-  }
-
-  @Test
-  public void withDuplicateSystemGroupCaseSensitiveName_Conflict()
-      throws Exception {
-    String newGroupName = "Registered Users";
-    adminSshSession.exec("gerrit create-group '" + newGroupName + "'");
-    assert_().withFailureMessage(adminSshSession.getError())
-        .that(adminSshSession.hasError()).isTrue();
-    assertThat(adminSshSession.getError())
-        .isEqualTo("fatal: group '" + newGroupName + "' already exists\n");
-  }
-
-  @Test
-  public void withDuplicateSystemGroupCaseInsensitiveName_Conflict()
-      throws Exception {
-    String newGroupName = "Registered Users";
-    adminSshSession
-        .exec("gerrit create-group '" + newGroupName.toUpperCase() + "'");
-    assert_().withFailureMessage(adminSshSession.getError())
-        .that(adminSshSession.hasError()).isTrue();
-    assertThat(adminSshSession.getError())
-        .isEqualTo("fatal: group '" + newGroupName + "' already exists\n");
-  }
-
-  @Test
-  public void withNonDuplicateGroupName() throws Exception {
-    String newGroupName = "newGroupB";
-    adminSshSession.exec("gerrit create-group " + newGroupName);
-    assert_().withFailureMessage(adminSshSession.getError())
-        .that(adminSshSession.hasError()).isFalse();
-    AccountGroup accountGroup =
-        groupCache.get(new AccountGroup.NameKey(newGroupName));
-    assertThat(accountGroup).isNotNull();
-    assertThat(accountGroup.getName()).isEqualTo(newGroupName);
-  }
-}
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 0b18106..5009771 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -60,6 +60,7 @@
   private final ExecutorService executor;
   private final ScheduledExecutorService cleanup;
   private final long h2CacheSize;
+  private final boolean h2AutoServer;
 
   @Inject
   H2CacheFactory(
@@ -71,6 +72,7 @@
     config = cfg;
     cacheDir = getCacheDir(site, cfg.getString("cache", null, "directory"));
     h2CacheSize = cfg.getLong("cache", null, "h2CacheSize", -1);
+    h2AutoServer = cfg.getBoolean("cache", null, "h2AutoServer", false);
     caches = new LinkedList<>();
     this.cacheMap = cacheMap;
 
@@ -230,6 +232,9 @@
       // H2 CACHE_SIZE is always given in KB
       url.append(h2CacheSize / 1024);
     }
+    if (h2AutoServer) {
+      url.append(";AUTO_SERVER=TRUE");
+    }
     return new SqlStore<>(url.toString(), keyType, maxSize,
         expireAfterWrite == null ? 0 : expireAfterWrite.longValue());
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
index 752f0d2..afd6734 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -16,13 +16,11 @@
 
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.gwtjsonrpc.common.RpcImpl;
 import com.google.gwtjsonrpc.common.RpcImpl.Version;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.List;
 import java.util.Set;
@@ -36,14 +34,4 @@
   @SignInRequired
   void deleteExternalIds(Set<AccountExternalId.Key> keys,
       AsyncCallback<Set<AccountExternalId.Key>> callback);
-
-  @Audit
-  @SignInRequired
-  void updateContact(String fullName, String emailAddr,
-      AsyncCallback<Account> callback);
-
-  @Audit
-  @SignInRequired
-  void enterAgreement(String agreementName,
-      AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
deleted file mode 100644
index 22482c7..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2008 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.common.data;
-
-import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.RpcImpl.Version;
-
-@RpcImpl(version = Version.V2_0)
-public interface AccountService extends RemoteJsonService {
-  @SignInRequired
-  void myAgreements(AsyncCallback<AgreementInfo> callback);
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
index 16e7e61..04dcec4 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
@@ -40,6 +40,7 @@
   public List<Message> messages;
   public Integer pluginsLoadTimeout;
   public boolean isNoteDbEnabled;
+  public boolean canLoadInIFrame;
 
   public static class Theme {
     public String backgroundColor;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
index 6cee630..3fdc331 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
@@ -29,20 +29,28 @@
 @GwtIncompatible("Unemulated org.eclipse.jgit.transport.RefSpec")
 public class SubscribeSection {
 
-  private final List<RefSpec> refSpecs;
+  private final List<RefSpec> multiMatchRefSpecs;
+  private final List<RefSpec> matchingRefSpecs;
   private final Project.NameKey project;
 
   public SubscribeSection(Project.NameKey p) {
     project = p;
-    refSpecs = new ArrayList<>();
+    matchingRefSpecs = new ArrayList<>();
+    multiMatchRefSpecs = new ArrayList<>();
   }
 
-  public void addRefSpec(RefSpec spec) {
-    refSpecs.add(spec);
+  public void addMatchingRefSpec(RefSpec spec) {
+    matchingRefSpecs.add(spec);
   }
 
-  public void addRefSpec(String spec) {
-    refSpecs.add(new RefSpec(spec));
+  public void addMatchingRefSpec(String spec) {
+    RefSpec r = new RefSpec(spec);
+    matchingRefSpecs.add(r);
+  }
+
+  public void addMultiMatchRefSpec(String spec) {
+    RefSpec r = new RefSpec(spec, RefSpec.WildcardMode.ALLOW_MISMATCH);
+    multiMatchRefSpecs.add(r);
   }
 
   public Project.NameKey getProject() {
@@ -57,7 +65,12 @@
    * @return if the branch could trigger a superproject update
    */
   public boolean appliesTo(Branch.NameKey branch) {
-    for (RefSpec r : refSpecs) {
+    for (RefSpec r : matchingRefSpecs) {
+      if (r.matchSource(branch.get())) {
+        return true;
+      }
+    }
+    for (RefSpec r : multiMatchRefSpecs) {
       if (r.matchSource(branch.get())) {
         return true;
       }
@@ -65,8 +78,12 @@
     return false;
   }
 
-  public Collection<RefSpec> getRefSpecs() {
-    return Collections.unmodifiableCollection(refSpecs);
+  public Collection<RefSpec> getMatchingRefSpecs() {
+    return Collections.unmodifiableCollection(matchingRefSpecs);
+  }
+
+  public Collection<RefSpec> getMultiMatchRefSpecs() {
+    return Collections.unmodifiableCollection(multiMatchRefSpecs);
   }
 
   @Override
@@ -74,10 +91,19 @@
     StringBuilder ret = new StringBuilder();
     ret.append("[SubscribeSection, project=");
     ret.append(project);
-    ret.append(", refs=[");
-    for (RefSpec r : refSpecs) {
-      ret.append(r.toString());
-      ret.append(", ");
+    if (!matchingRefSpecs.isEmpty()) {
+      ret.append(", matching=[");
+      for (RefSpec r : matchingRefSpecs) {
+        ret.append(r.toString());
+        ret.append(", ");
+      }
+    }
+    if (!multiMatchRefSpecs.isEmpty()) {
+      ret.append(", all=[");
+      for (RefSpec r : multiMatchRefSpecs) {
+        ret.append(r.toString());
+        ret.append(", ");
+      }
     }
     ret.append("]");
     return ret.toString();
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
index 272801f..fb54ef1 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -29,8 +28,5 @@
   @AllowCrossSiteRequest
   void daemonHostKeys(AsyncCallback<List<SshHostKey>> callback);
 
-  @SignInRequired
-  void contributorAgreements(AsyncCallback<List<ContributorAgreement>> callback);
-
   void clientError(String message, AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index 9b83c5a..121d236 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -27,6 +27,7 @@
   name = 'lib',
   exported_deps = [
     ':api',
+    '//lib:guava',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
@@ -42,6 +43,7 @@
     '//gerrit-common:annotations',
   ],
   provided_deps = [
+    '//lib:guava',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
   ],
@@ -75,6 +77,7 @@
     '//lib/guice:javax-inject',
     '//lib/guice:guice_library',
     '//lib/guice:guice-assistedinject',
+    '//lib/jgit/org.eclipse.jgit:jgit',
     '//gerrit-common:annotations',
   ],
   visibility = ['PUBLIC'],
diff --git a/gerrit-extension-api/BUILD b/gerrit-extension-api/BUILD
index fd082f1..4a5cfe3 100644
--- a/gerrit-extension-api/BUILD
+++ b/gerrit-extension-api/BUILD
@@ -23,6 +23,7 @@
   name = 'lib',
   exports = [
     ':api',
+    '//lib:guava',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
@@ -37,6 +38,7 @@
   srcs = glob([SRC + '**/*.java']),
   deps = [
     '//gerrit-common:annotations',
+    '//lib:guava',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
   ],
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 023362f..7375893 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
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 6ea77ef..c1cb3ec 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.AgreementInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.SshKeyInfo;
@@ -70,6 +71,9 @@
       throws RestApiException;
   GpgKeyApi gpgKey(String id) throws RestApiException;
 
+  List<AgreementInfo> listAgreements() throws RestApiException;
+  void signAgreement(String agreementName) throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
@@ -197,5 +201,15 @@
     public Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public List<AgreementInfo> listAgreements() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void signAgreement(String agreementName) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountInput.java
new file mode 100644
index 0000000..33baf93
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountInput.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.accounts;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+import java.util.List;
+
+public class AccountInput {
+  @DefaultInput
+  public String username;
+  public String name;
+  public String email;
+  public String sshKey;
+  public String httpPassword;
+  public List<String> groups;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
index fe8a1d8..a697091 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -52,6 +52,12 @@
    */
   AccountApi self() throws RestApiException;
 
+  /** Create a new account with the given username and default options. */
+  AccountApi create(String username) throws RestApiException;
+
+  /** Create a new account. */
+  AccountApi create(AccountInput input) throws RestApiException;
+
   /**
    * Suggest users for a given query.
    * <p>
@@ -233,6 +239,16 @@
     }
 
     @Override
+    public AccountApi create(String username) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public AccountApi create(AccountInput input) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public SuggestAccountsRequest suggestAccounts() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AbandonInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AbandonInput.java
index 04a7bc7..34726a8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AbandonInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AbandonInput.java
@@ -19,5 +19,6 @@
 public class AbandonInput {
   @DefaultInput
   public String message;
+  public NotifyHandling notify = NotifyHandling.ALL;
 }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerInput.java
index 30a23bf..ca61b1d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerInput.java
@@ -14,15 +14,22 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+
+import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 
 public class AddReviewerInput {
   @DefaultInput
   public String reviewer;
   public Boolean confirmed;
+  public ReviewerState state;
 
   public boolean confirmed() {
     return (confirmed != null) ? confirmed : false;
   }
-}
 
+  public ReviewerState state() {
+    return (state != null) ? state : REVIEWER;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerResult.java
new file mode 100644
index 0000000..10f74ff84
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerResult.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+import java.util.List;
+
+/**
+ * Result object representing the outcome of a request to add a reviewer.
+ */
+public class AddReviewerResult {
+  /**
+   * The identifier of an account or group that was to be added as a reviewer.
+   */
+  public String input;
+
+  /**
+   * If non-null, a string describing why the reviewer could not be added.
+   */
+  @Nullable
+  public String error;
+
+  /**
+   * Non-null and true if the reviewer cannot be added without explicit
+   * confirmation. This may be the case for groups of a certain size.
+   */
+  @Nullable
+  public Boolean confirm;
+
+  /**
+   * List of individual reviewers added to the change. The size of this
+   * list may be greater than one (e.g. when a group is added). Null if no
+   * reviewers were added.
+   */
+  @Nullable
+  public List<ReviewerInfo> reviewers;
+
+  /**
+   * List of accounts CCed on the change. The size of this list may be
+   * greater than one (e.g. when a group is CCed). Null if no accounts were CCed
+   * or if reviewers is non-null.
+   */
+  @Nullable
+  public List<AccountInfo> ccs;
+
+  /**
+   * Constructs a partially initialized result for the given reviewer.
+   *
+   * @param input String identifier of an account or group, from user request
+   */
+  public AddReviewerResult(String input) {
+    this.input = input;
+  }
+
+  /**
+   * Constructs an error result for the given account.
+   *
+   * @param reviewer String identifier of an account or group
+   * @param error Error message
+   */
+  public AddReviewerResult(String reviewer, String error) {
+    this(reviewer);
+    this.error = error;
+  }
+
+  /**
+   * Constructs a needs-confirmation result for the given account.
+   *
+   * @param confirm Whether confirmation is needed.
+   */
+  public AddReviewerResult(String reviewer, boolean confirm) {
+    this(reviewer);
+    this.confirm = confirm;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java
index ea63743..671f43e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.extensions.api.changes;
 
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 
 /** Input passed to {@code DELETE /changes/[id]/reviewers/[id]/votes/[label]}. */
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java
index 8b626b7..9d94f50 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java
@@ -16,6 +16,22 @@
 
 import com.google.gerrit.extensions.client.Comment;
 
+import java.util.Objects;
+
 public class DraftInput extends Comment {
   public String tag;
+
+  @Override
+  public boolean equals(Object o) {
+    if (super.equals(o)) {
+      DraftInput di = (DraftInput) o;
+      return Objects.equals(tag, di.tag);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(super.hashCode(), tag);
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
index 3641ac5..2536c46 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
@@ -35,6 +35,11 @@
   DiffInfo diff(String base) throws RestApiException;
 
   /**
+   * @param parent 1-based parent number to diff against
+   */
+  DiffInfo diff(int parent) throws RestApiException;
+
+  /**
    * Creates a request to retrieve the diff. On the returned request formatting
    * options for the diff can be set.
    */
@@ -106,6 +111,11 @@
     }
 
     @Override
+    public DiffInfo diff(int parent) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public DiffRequest diffRequest() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/NotifyHandling.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/NotifyHandling.java
new file mode 100644
index 0000000..888e6bf
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/NotifyHandling.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+public enum NotifyHandling {
+  NONE, OWNER, OWNER_REVIEWERS, ALL
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 9429ed6..cbe16ed 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -14,9 +14,13 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+
 import com.google.gerrit.extensions.client.Comment;
+import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 
+import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -68,6 +72,11 @@
    */
   public String onBehalfOf;
 
+  /**
+   * Reviewers that should be added to this change.
+   */
+  public List<AddReviewerInput> reviewers;
+
   public enum DraftHandling {
     /** Delete pending drafts on this revision only. */
     DELETE,
@@ -82,10 +91,6 @@
     PUBLISH_ALL_REVISIONS
   }
 
-  public enum NotifyHandling {
-    NONE, OWNER, OWNER_REVIEWERS, ALL
-  }
-
   public static class CommentInput extends Comment {
   }
 
@@ -116,6 +121,23 @@
     return label(name, (short) 1);
   }
 
+  public ReviewInput reviewer(String reviewer) {
+    return reviewer(reviewer, REVIEWER, false);
+  }
+
+  public ReviewInput reviewer(String reviewer, ReviewerState state,
+      boolean confirmed) {
+    AddReviewerInput input = new AddReviewerInput();
+    input.reviewer = reviewer;
+    input.state = state;
+    input.confirmed = confirmed;
+    if (reviewers == null) {
+      reviewers = new ArrayList<>();
+    }
+    reviewers.add(input);
+    return this;
+  }
+
   public static ReviewInput recommend() {
     return new ReviewInput().label("Code-Review", 1);
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewResult.java
new file mode 100644
index 0000000..b9de2e1
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewResult.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.gerrit.common.Nullable;
+
+import java.util.Map;
+
+/**
+ * Result object representing the outcome of a review request.
+ */
+public class ReviewResult {
+  /**
+   * Map of labels to values after the review was posted. Null if any
+   * reviewer additions were rejected.
+   */
+  @Nullable
+  public Map<String, Short> labels;
+
+  /**
+   * Map of account or group identifier to outcome of adding as a reviewer.
+   * Null if no reviewer additions were requested.
+   */
+  @Nullable
+  public Map<String, AddReviewerResult> reviewers;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java
new file mode 100644
index 0000000..c81f8aa
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+import java.util.Map;
+
+/**
+ * Account and approval details for an added reviewer.
+ */
+public class ReviewerInfo extends AccountInfo {
+  /**
+   * {@link Map} of label name to initial value for each approval the reviewer
+   * is responsible for.
+   */
+  @Nullable
+  public Map<String, String> approvals;
+
+  public ReviewerInfo(Integer id) {
+    super(id);
+  }
+
+  @Override
+  public String toString() {
+    return username;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index b23c7f9..a71ab37 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -46,7 +46,8 @@
 
   Map<String, FileInfo> files() throws RestApiException;
   Map<String, FileInfo> files(String base) throws RestApiException;
-  FileApi file(String path);
+  Map<String, FileInfo> files(int parentNum) throws RestApiException;
+  FileApi file(String path) throws RestApiException;
   MergeableInfo mergeable() throws RestApiException;
   MergeableInfo mergeableOtherBranches() throws RestApiException;
 
@@ -147,6 +148,11 @@
     }
 
     @Override
+    public Map<String, FileInfo> files(int parentNum) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public Map<String, FileInfo> files() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
index 6abf83df..e415acb 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.extensions.api.changes;
 
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
-
 public class SubmitInput {
   /** Not used anymore, kept for backward compatibility */
   @Deprecated
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
index 6522223..1e5c95e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.extensions.api.config;
 
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
@@ -24,6 +26,11 @@
    */
   String getVersion() throws RestApiException;
 
+  ServerInfo getInfo() throws RestApiException;
+
+  GeneralPreferencesInfo getDefaultPreferences() throws RestApiException;
+  GeneralPreferencesInfo setDefaultPreferences(GeneralPreferencesInfo in)
+      throws RestApiException;
   DiffPreferencesInfo getDefaultDiffPreferences() throws RestApiException;
   DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in)
       throws RestApiException;
@@ -39,6 +46,23 @@
     }
 
     @Override
+    public ServerInfo getInfo() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GeneralPreferencesInfo getDefaultPreferences()
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GeneralPreferencesInfo setDefaultPreferences(
+        GeneralPreferencesInfo in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public DiffPreferencesInfo getDefaultDiffPreferences()
         throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupInput.java
index 28665fe..ab38434 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupInput.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.extensions.api.groups;
 
+import java.util.List;
+
 public class GroupInput {
   public String name;
   public String description;
   public Boolean visibleToAll;
   public String ownerId;
+  public List<String> members;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java
new file mode 100644
index 0000000..07d9f37
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.client;
+
+public enum AccountFieldName {
+  FULL_NAME, USER_NAME, REGISTER_NEW_EMAIL
+}
\ No newline at end of file
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
similarity index 98%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
index 38a78ba..004ef1c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.extensions.client;
 
 public enum AuthType {
   /** Login relies upon the OpenID standard: {@link "http://openid.net/"} */
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
index b9863d7..7c8a3e8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.client;
 
 import java.sql.Timestamp;
+import java.util.Objects;
 
 public abstract class Comment {
   /**
@@ -27,6 +28,7 @@
   public String id;
   public String path;
   public Side side;
+  public Integer parent;
   public Integer line;
   public Range range;
   public String inReplyTo;
@@ -38,5 +40,49 @@
     public int startCharacter;
     public int endLine;
     public int endCharacter;
+
+    @Override
+    public boolean equals(Object o) {
+      if (o instanceof Range) {
+        Range r = (Range) o;
+        return Objects.equals(startLine, r.startLine)
+            && Objects.equals(startCharacter, r.startCharacter)
+            && Objects.equals(endLine, r.endLine)
+            && Objects.equals(endCharacter, r.endCharacter);
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(startLine, startCharacter, endLine, endCharacter);
+    }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o != null && getClass() == o.getClass()) {
+      Comment c = (Comment) o;
+      return Objects.equals(patchSet, c.patchSet)
+          && Objects.equals(id, c.id)
+          && Objects.equals(path, c.path)
+          && Objects.equals(side, c.side)
+          && Objects.equals(parent, c.parent)
+          && Objects.equals(line, c.line)
+          && Objects.equals(range, c.range)
+          && Objects.equals(inReplyTo, c.inReplyTo)
+          && Objects.equals(updated, c.updated)
+          && Objects.equals(message, c.message);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(patchSet, id, path, side, parent, line, range,
+        inReplyTo, updated, message);
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListAccountsOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListAccountsOption.java
index bc3679c..b5e9004 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListAccountsOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListAccountsOption.java
@@ -20,7 +20,10 @@
 /** Output options available for retrieval of account details. */
 public enum ListAccountsOption {
   /** Return detailed account properties. */
-  DETAILS(0);
+  DETAILS(0),
+
+  /** Return all secondary emails. */
+  ALL_EMAILS(1);
 
   private final int value;
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
index 88c02b82..8b6c5e6 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -66,7 +66,10 @@
   COMMIT_FOOTERS(17),
 
   /** Include push certificate information along with any patch sets. */
-  PUSH_CERTIFICATES(18);
+  PUSH_CERTIFICATES(18),
+
+  /** Include change's reviewer updates. */
+  REVIEWER_UPDATES(19);
 
   private final int value;
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
index beb869e..556dddc 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
@@ -47,4 +47,30 @@
         .hash(project, filter, notifyNewChanges, notifyNewPatchSets,
             notifyAllComments, notifySubmittedChanges, notifyAbandonedChanges);
   }
+
+  @Override
+  public String toString() {
+    StringBuilder b = new StringBuilder();
+    b.append(project);
+    if (filter != null) {
+      b.append("%filter=")
+          .append(filter);
+    }
+    b.append("(notifyAbandonedChanges=")
+        .append(toBoolean(notifyAbandonedChanges))
+        .append(", notifyAllComments=")
+        .append(toBoolean(notifyAllComments))
+        .append(", notifyNewChanges=")
+        .append(toBoolean(notifyNewChanges))
+        .append(", notifyNewPatchSets=")
+        .append(toBoolean(notifyNewPatchSets))
+        .append(", notifySubmittedChanges=")
+        .append(toBoolean(notifySubmittedChanges))
+        .append(")");
+    return b.toString();
+  }
+
+  private boolean toBoolean(Boolean b) {
+    return b == null ? false : b;
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
index 3485b8b..e077df2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
@@ -19,11 +19,10 @@
   REVISION;
 
   public static Side fromShort(short s) {
-    switch (s) {
-      case 0:
-        return PARENT;
-      case 1:
-        return REVISION;
+    if (s <= 0) {
+      return PARENT;
+    } else if (s == 1) {
+      return REVISION;
     }
     return null;
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java
index c03a684..6408f9d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java
@@ -25,7 +25,7 @@
   NEAT,
   NEO,
   PARAISO_LIGHT,
-  SOLARIZED_LIGHT,
+  SOLARIZED,
   TTCN,
   XQ_LIGHT,
   YETI,
@@ -56,7 +56,6 @@
   RAILSCASTS,
   RUBYBLUE,
   SETI,
-  SOLARIZED_DARK,
   THE_MATRIX,
   TOMORROW_NIGHT_BRIGHT,
   TOMORROW_NIGHT_EIGHTIES,
@@ -92,7 +91,6 @@
       case RAILSCASTS:
       case RUBYBLUE:
       case SETI:
-      case SOLARIZED_DARK:
       case THE_MATRIX:
       case TOMORROW_NIGHT_BRIGHT:
       case TOMORROW_NIGHT_EIGHTIES:
@@ -110,7 +108,7 @@
       case NEAT:
       case NEO:
       case PARAISO_LIGHT:
-      case SOLARIZED_LIGHT:
+      case SOLARIZED:
       case TTCN:
       case XQ_LIGHT:
       case YETI:
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
index 8322eaf..2c35d5e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
@@ -20,6 +20,7 @@
   public Integer _accountId;
   public String name;
   public String email;
+  public List<String> secondaryEmails;
   public String username;
   public List<AvatarInfo> avatars;
   public Boolean _moreAccounts;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInfo.java
new file mode 100644
index 0000000..4242fcd
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInfo.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class AgreementInfo {
+  public String name;
+  public String description;
+  public String url;
+  public GroupInfo autoVerifyGroup;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInput.java
new file mode 100644
index 0000000..060367b
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInput.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+/** This entity contains information for registering a new contributor agreement. */
+public class AgreementInput {
+  /* The agreement name. */
+  @DefaultInput
+  public String name;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AuthInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AuthInfo.java
new file mode 100644
index 0000000..1000e9c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AuthInfo.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.client.AuthType;
+
+import java.util.List;
+
+public class AuthInfo {
+  public AuthType authType;
+  public Boolean useContributorAgreements;
+  public List<AgreementInfo> contributorAgreements;
+  public List<AccountFieldName> editableAccountFields;
+  public String loginUrl;
+  public String loginText;
+  public String switchAccountUrl;
+  public String registerUrl;
+  public String registerText;
+  public String editFullNameUrl;
+  public String httpPasswordUrl;
+  public Boolean isGitBasicAuth;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
new file mode 100644
index 0000000..206b2f0
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class ChangeConfigInfo {
+  public Boolean allowBlame;
+  public Boolean allowDrafts;
+  public int largeChange;
+  public String replyLabel;
+  public String replyTooltip;
+  public int updateDelay;
+  public Boolean submitWholeTopic;
+}
\ No newline at end of file
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 f2d2634..003ab24 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
@@ -53,6 +53,7 @@
   public Map<String, Collection<String>> permittedLabels;
   public Collection<AccountInfo> removableReviewers;
   public Map<ReviewerState, Collection<AccountInfo>> reviewers;
+  public Collection<ReviewerUpdateInfo> reviewerUpdates;
   public Collection<ChangeMessageInfo> messages;
 
   public String currentRevision;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
index 9f0df93..88c3ea8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
@@ -25,4 +25,5 @@
   public ChangeStatus status;
   public String baseChange;
   public Boolean newBranch;
+  public MergeInput merge;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
index b7535e1..166aaa2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
@@ -16,7 +16,24 @@
 
 import com.google.gerrit.extensions.client.Comment;
 
+import java.util.Objects;
+
 public class CommentInfo extends Comment {
   public AccountInfo author;
   public String tag;
+
+  @Override
+  public boolean equals(Object o) {
+    if (super.equals(o)) {
+      CommentInfo ci = (CommentInfo) o;
+      return Objects.equals(author, ci.author)
+          && Objects.equals(tag, ci.tag);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(super.hashCode(), author, tag);
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java
new file mode 100644
index 0000000..180e2d2
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.List;
+import java.util.Map;
+
+public class DownloadInfo {
+  public Map<String, DownloadSchemeInfo> schemes;
+  public List<String> archives;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java
new file mode 100644
index 0000000..0e8ad65
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.Map;
+
+public class DownloadSchemeInfo {
+  public String url;
+  public Boolean isAuthRequired;
+  public Boolean isAuthSupported;
+  public Map<String, String> commands;
+  public Map<String, String> cloneCommands;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GerritInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GerritInfo.java
new file mode 100644
index 0000000..72c474f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GerritInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class GerritInfo {
+  public String allProjects;
+  public String allUsers;
+  public Boolean docSearch;
+  public String docUrl;
+  public Boolean editGpgKeys;
+  public String reportBugUrl;
+  public String reportBugText;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeInput.java
new file mode 100644
index 0000000..598d618
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeInput.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class MergeInput {
+  /**
+   * {@code source} can be any Git object reference expression.
+   *
+   * @see <a href="https://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html">gitrevisions(7)</a>
+   */
+  public String source;
+
+  /**
+   * {@code strategy} name of the merge strategy.
+   *
+   * @see org.eclipse.jgit.merge.MergeStrategy
+   */
+  public String strategy;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java
index 9c38055..50de74a 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java
@@ -20,6 +20,10 @@
 
 public class MergeableInfo {
   public SubmitType submitType;
+  public String strategy;
   public boolean mergeable;
+  public boolean commitMerged;
+  public boolean contentMerged;
+  public List<String> conflicts;
   public List<String> mergeableInto;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java
new file mode 100644
index 0000000..845f7cb
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.List;
+
+public class PluginConfigInfo {
+  public Boolean hasAvatars;
+  public List<String> jsResourcePaths;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
index 4dd910d..ff04fdc 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.common;
 
+import java.util.Objects;
+
 public class ProblemInfo {
   public enum Status {
     FIXED, FIX_FAILED
@@ -24,6 +26,22 @@
   public String outcome;
 
   @Override
+  public int hashCode() {
+    return Objects.hash(message, status, outcome);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof ProblemInfo)) {
+      return false;
+    }
+    ProblemInfo p = (ProblemInfo) o;
+    return Objects.equals(message, p.message)
+        && Objects.equals(status, p.status)
+        && Objects.equals(outcome, p.outcome);
+  }
+
+  @Override
   public String toString() {
     StringBuilder sb = new StringBuilder(getClass().getSimpleName())
         .append('[').append(message);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java
new file mode 100644
index 0000000..e66c242
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class ReceiveInfo {
+  public Boolean enableSignedPush;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
new file mode 100644
index 0000000..b3c9cb6
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import com.google.gerrit.extensions.client.ReviewerState;
+
+import java.sql.Timestamp;
+
+public class ReviewerUpdateInfo {
+  public Timestamp updated;
+  public AccountInfo updatedBy;
+  public AccountInfo reviewer;
+  public ReviewerState state;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java
new file mode 100644
index 0000000..3dd8368
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.Map;
+
+public class ServerInfo {
+  public AuthInfo auth;
+  public ChangeConfigInfo change;
+  public DownloadInfo download;
+  public GerritInfo gerrit;
+  public Boolean noteDbEnabled;
+  public PluginConfigInfo plugin;
+  public SshdInfo sshd;
+  public SuggestInfo suggest;
+  public Map<String, String> urlAliases;
+  public UserConfigInfo user;
+  public ReceiveInfo receive;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java
new file mode 100644
index 0000000..98d650c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class SshdInfo {
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java
new file mode 100644
index 0000000..5b0dcbe
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class SuggestInfo {
+  public int from;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestedReviewerInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestedReviewerInfo.java
index d371f35..697caf1 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestedReviewerInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestedReviewerInfo.java
@@ -17,4 +17,6 @@
 public class SuggestedReviewerInfo {
   public AccountInfo account;
   public GroupBaseInfo group;
-}
+  public int count;
+  public Boolean confirm;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java
new file mode 100644
index 0000000..5010689
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class UserConfigInfo {
+  public String anonymousCowardName;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java
index 072799f..d78fa63 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java
@@ -14,16 +14,18 @@
 
 package com.google.gerrit.extensions.config;
 
+import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 
 import java.util.Collection;
-import java.util.List;
 
 @ExtensionPoint
 public interface ExternalIncludedIn {
 
   /**
-   * Returns a list of systems that include the given commit.
+   * Returns additional entries for IncludedInInfo as multimap where the
+   * key is the row title and the the values are a list of systems that include
+   * the given commit (e.g. names of servers on which this commit is deployed).
    *
    * The tags and branches in which the commit is included are provided so that
    * a RevWalk can be avoided when a system runs a certain tag or branch.
@@ -33,9 +35,8 @@
    *        included
    * @param tags the tags that include the commit
    * @param branches the branches that include the commit
-   * @return a list of systems that contain the given commit, e.g. names of
-   *         servers on which this commit is deployed
+   * @return additional entries for IncludedInInfo
    */
-  List<String> getIncludedIn(String project, String commit,
+  Multimap<String, String> getIncludedIn(String project, String commit,
       Collection<String> tags, Collection<String> branches);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AgreementSignupListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AgreementSignupListener.java
index e638264..5abfc38 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AgreementSignupListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AgreementSignupListener.java
@@ -20,7 +20,7 @@
 /** Notified whenever a user signed up for a Contributor License Agreement. */
 @ExtensionPoint
 public interface AgreementSignupListener {
-  interface Event {
+  interface Event extends GerritEvent {
     AccountInfo getAccount();
     String getAgreementName();
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java
index 621c605..40b84a3 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java
@@ -21,6 +21,7 @@
 @ExtensionPoint
 public interface ChangeAbandonedListener {
   interface Event extends RevisionEvent {
+    @Deprecated
     AccountInfo getAbandoner();
     String getReason();
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeEvent.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeEvent.java
index 92b04c1..f012710 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeEvent.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeEvent.java
@@ -14,10 +14,14 @@
 
 package com.google.gerrit.extensions.events;
 
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 
-/** Interface to be extended by Events with a Change. */
-public interface ChangeEvent {
-  ChangeInfo getChange();
-}
+import java.sql.Timestamp;
 
+/** Interface to be extended by Events with a Change. */
+public interface ChangeEvent extends GerritEvent {
+  ChangeInfo getChange();
+  AccountInfo getWho();
+  Timestamp getWhen();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java
index 8b55af3..d0ca6d6 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java
@@ -21,6 +21,7 @@
 @ExtensionPoint
 public interface ChangeMergedListener {
   interface Event extends RevisionEvent {
+    @Deprecated
     AccountInfo getMerger();
     /**
      * Represents the merged Revision when the submit strategy is cherry-pick or
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java
index 6e9e26b..e5f3330 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java
@@ -21,6 +21,7 @@
 @ExtensionPoint
 public interface ChangeRestoredListener {
   interface Event extends RevisionEvent {
+    @Deprecated
     AccountInfo getRestorer();
     String getReason();
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRevertedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRevertedListener.java
new file mode 100644
index 0000000..99904c7
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRevertedListener.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.ChangeInfo;
+
+/** Notified whenever a Change is reverted via the UI or REST API. */
+@ExtensionPoint
+public interface ChangeRevertedListener {
+  interface Event extends ChangeEvent {
+    /** The revert change that was created. */
+    ChangeInfo getRevertChange();
+  }
+
+  void onChangeReverted(Event event);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java
index 3e6a9c7..6c82034 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java
@@ -24,6 +24,7 @@
 @ExtensionPoint
 public interface CommentAddedListener {
   interface Event extends RevisionEvent {
+    @Deprecated
     AccountInfo getAuthor();
     String getComment();
     Map<String, ApprovalInfo> getApprovals();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java
index 82e81a5..3857468 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java
@@ -21,6 +21,7 @@
 @ExtensionPoint
 public interface DraftPublishedListener {
   interface Event extends RevisionEvent {
+    @Deprecated
     AccountInfo getPublisher();
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java
index 6138a86..93d81f7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java
@@ -23,10 +23,7 @@
  */
 @ExtensionPoint
 public interface GarbageCollectorListener {
-  interface Event {
-    /** @return The name of the project that has been garbage collected. */
-    String getProjectName();
-
+  interface Event extends ProjectEvent {
     /**
      * @return Properties describing the result of the garbage collection
      *         performed by JGit.
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GerritEvent.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GerritEvent.java
new file mode 100644
index 0000000..e43a981
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GerritEvent.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+
+/** Base interface to be extended by Events. */
+public interface GerritEvent {
+  NotifyHandling getNotify();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
index 4c7b3be..3f7dfbe 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
@@ -21,9 +21,7 @@
 /** Notified when one or more references are modified. */
 @ExtensionPoint
 public interface GitReferenceUpdatedListener {
-
-  interface Event {
-    String getProjectName();
+  interface Event extends ProjectEvent {
     String getRefName();
     String getOldObjectId();
     String getNewObjectId();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java
index eba146f..c49b0f3 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java
@@ -23,6 +23,7 @@
 @ExtensionPoint
 public interface HashtagsEditedListener {
   interface Event extends ChangeEvent {
+    @Deprecated
     AccountInfo getEditor();
     Collection<String> getHashtags();
     Collection<String> getAddedHashtags();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HeadUpdatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HeadUpdatedListener.java
index bb6beeb..e11c857 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HeadUpdatedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HeadUpdatedListener.java
@@ -19,8 +19,7 @@
 /** Notified whenever the HEAD of a project is updated. */
 @ExtensionPoint
 public interface HeadUpdatedListener {
-  interface Event {
-    String getProjectName();
+  interface Event extends ProjectEvent {
     String getOldHeadName();
     String getNewHeadName();
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java
index 1781cde..07c0bf6 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java
@@ -20,8 +20,7 @@
 /** Notified whenever a project is created on the master. */
 @ExtensionPoint
 public interface NewProjectCreatedListener {
-  interface Event {
-    String getProjectName();
+  interface Event extends ProjectEvent {
     String getHeadName();
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/PluginEventListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/PluginEventListener.java
index f58acc3..dfcbdee 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/PluginEventListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/PluginEventListener.java
@@ -16,7 +16,7 @@
 
 /** Notified when a plugin fires an event. */
 public interface PluginEventListener {
-  interface Event {
+  interface Event extends GerritEvent {
     String pluginName();
     String getType();
     String getData();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectDeletedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectDeletedListener.java
index e373110..468950f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectDeletedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectDeletedListener.java
@@ -19,8 +19,7 @@
 /** Notified whenever a project is deleted on the master. */
 @ExtensionPoint
 public interface ProjectDeletedListener {
-  interface Event {
-    String getProjectName();
+  interface Event extends ProjectEvent {
   }
 
   void onProjectDeleted(Event event);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectEvent.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectEvent.java
new file mode 100644
index 0000000..f36ad31
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectEvent.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+/** Interface to be extended by Events with a Project. */
+public interface ProjectEvent extends GerritEvent {
+  String getProjectName();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java
index 3cc3fdc..bb4ac9d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java
@@ -17,12 +17,14 @@
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.extensions.common.AccountInfo;
 
-/** Notified whenever a Reviewer is added to a change. */
+import java.util.List;
+
+/** Notified whenever one or more Reviewers are added to a change. */
 @ExtensionPoint
 public interface ReviewerAddedListener {
   interface Event extends ChangeEvent {
-    AccountInfo getReviewer();
+    List<AccountInfo> getReviewers();
   }
 
-  void onReviewerAdded(Event event);
+  void onReviewersAdded(Event event);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java
index e400b7e1..5e4e095 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java
@@ -21,6 +21,7 @@
 @ExtensionPoint
 public interface RevisionCreatedListener {
   interface Event extends RevisionEvent {
+    @Deprecated
     AccountInfo getUploader();
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java
index ce210dc..68ba22c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java
@@ -21,6 +21,7 @@
 @ExtensionPoint
 public interface TopicEditedListener {
   interface Event extends ChangeEvent {
+    @Deprecated
     AccountInfo getEditor();
     String getOldTopic();
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/VoteDeletedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/VoteDeletedListener.java
new file mode 100644
index 0000000..01a83e3
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/VoteDeletedListener.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+
+import java.util.Map;
+
+/** Notified whenever a vote is removed from a change. */
+@ExtensionPoint
+public interface VoteDeletedListener {
+  interface Event extends RevisionEvent {
+    Map<String, ApprovalInfo> getOldApprovals();
+    Map<String, ApprovalInfo> getApprovals();
+    Map<String, ApprovalInfo> getRemoved();
+    String getMessage();
+  }
+
+  void onVoteDeleted(Event event);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
index 803a94a..633efea 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
@@ -52,6 +52,11 @@
     return new Redirect(location);
   }
 
+  /** Arbitrary status code with wrapped result. */
+  public static <T> Response<T> withStatusCode(int statusCode, T value) {
+    return new Impl<>(statusCode, value);
+  }
+
   @SuppressWarnings({"unchecked", "rawtypes"})
   public static <T> T unwrap(T obj) {
     while (obj instanceof Response) {
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index 32ce7bb..db6cb7a 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -28,8 +28,11 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -47,6 +50,7 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -63,6 +67,8 @@
   @Singleton
   public static class Factory {
     private final Provider<ReviewDb> db;
+    private final AccountIndexCollection accountIndexes;
+    private final Provider<InternalAccountQuery> accountQueryProvider;
     private final String webUrl;
     private final IdentifiedUser.GenericFactory userFactory;
     private final int maxTrustDepth;
@@ -71,9 +77,13 @@
     @Inject
     Factory(@GerritServerConfig Config cfg,
         Provider<ReviewDb> db,
+        AccountIndexCollection accountIndexes,
+        Provider<InternalAccountQuery> accountQueryProvider,
         IdentifiedUser.GenericFactory userFactory,
         @CanonicalWebUrl String webUrl) {
       this.db = db;
+      this.accountIndexes = accountIndexes;
+      this.accountQueryProvider = accountQueryProvider;
       this.webUrl = webUrl;
       this.userFactory = userFactory;
       this.maxTrustDepth = cfg.getInt("receive", null, "maxTrustDepth", 0);
@@ -107,6 +117,8 @@
   }
 
   private final Provider<ReviewDb> db;
+  private final AccountIndexCollection accountIndexes;
+  private final Provider<InternalAccountQuery> accountQueryProvider;
   private final String webUrl;
   private final IdentifiedUser.GenericFactory userFactory;
 
@@ -114,6 +126,8 @@
 
   private GerritPublicKeyChecker(Factory factory) {
     this.db = factory.db;
+    this.accountIndexes = factory.accountIndexes;
+    this.accountQueryProvider = factory.accountQueryProvider;
     this.webUrl = factory.webUrl;
     this.userFactory = factory.userFactory;
     if (factory.trusted != null) {
@@ -163,12 +177,26 @@
 
   private CheckResult checkIdsForArbitraryUser(PGPPublicKey key)
       throws PGPException, OrmException {
-    AccountExternalId extId = db.get().accountExternalIds().get(
-        toExtIdKey(key));
-    if (extId == null) {
-      return CheckResult.bad("Key is not associated with any users");
+    IdentifiedUser user;
+    if (accountIndexes.getSearchIndex() != null) {
+      List<AccountState> accountStates =
+          accountQueryProvider.get().byExternalId(toExtIdKey(key).get());
+      if (accountStates.isEmpty()) {
+        return CheckResult.bad("Key is not associated with any users");
+      }
+      if (accountStates.size() > 1) {
+        return CheckResult.bad("Key is associated with multiple users");
+      }
+      user = userFactory.create(accountStates.get(0));
+    } else {
+      AccountExternalId extId = db.get().accountExternalIds().get(
+          toExtIdKey(key));
+      if (extId == null) {
+        return CheckResult.bad("Key is not associated with any users");
+      }
+      user = userFactory.create(extId.getAccountId());
     }
-    IdentifiedUser user = userFactory.create(extId.getAccountId());
+
     Set<String> allowedUserIds = getAllowedUserIds(user);
     if (allowedUserIds.isEmpty()) {
       return CheckResult.bad("No identities found for user");
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
index e6720db..6809234 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
@@ -57,6 +57,11 @@
   }
 
   @Override
+  public boolean isEnabled() {
+    return true;
+  }
+
+  @Override
   public Map<String, GpgKeyInfo> listGpgKeys(AccountResource account)
       throws RestApiException, GpgException {
     try {
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiModule.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiModule.java
index 932f439..e65ebf2 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiModule.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiModule.java
@@ -62,6 +62,11 @@
     private static final String MSG = "GPG key APIs disabled";
 
     @Override
+    public boolean isEnabled() {
+      return false;
+    }
+
+    @Override
     public Map<String, GpgKeyInfo> listGpgKeys(AccountResource account) {
       throw new NotImplementedException(MSG);
     }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
index a16351e..cac0e72 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -45,14 +46,17 @@
   private final Provider<PersonIdent> serverIdent;
   private final Provider<ReviewDb> db;
   private final Provider<PublicKeyStore> storeProvider;
+  private final AccountCache accountCache;
 
   @Inject
   DeleteGpgKey(@GerritPersonIdent Provider<PersonIdent> serverIdent,
       Provider<ReviewDb> db,
-      Provider<PublicKeyStore> storeProvider) {
+      Provider<PublicKeyStore> storeProvider,
+      AccountCache accountCache) {
     this.serverIdent = serverIdent;
     this.db = db;
     this.storeProvider = storeProvider;
+    this.accountCache = accountCache;
   }
 
   @Override
@@ -64,6 +68,7 @@
         AccountExternalId.SCHEME_GPGKEY,
         BaseEncoding.base16().encode(key.getFingerprint()));
     db.get().accountExternalIds().deleteKeys(Collections.singleton(extIdKey));
+    accountCache.evict(rsrc.getUser().getAccountId());
 
     try (PublicKeyStore store = storeProvider.get()) {
       store.remove(rsrc.getKeyRing().getPublicKey().getFingerprint());
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 7e55d45..2deae3f 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -39,13 +39,18 @@
 import com.google.gerrit.gpg.PublicKeyChecker;
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.gpg.server.PostGpgKeys.Input;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.mail.AddKeySender;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -85,6 +90,9 @@
   private final Provider<PublicKeyStore> storeProvider;
   private final GerritPublicKeyChecker.Factory checkerFactory;
   private final AddKeySender.Factory addKeyFactory;
+  private final AccountCache accountCache;
+  private final AccountIndexCollection accountIndexes;
+  private final Provider<InternalAccountQuery> accountQueryProvider;
 
   @Inject
   PostGpgKeys(@GerritPersonIdent Provider<PersonIdent> serverIdent,
@@ -92,13 +100,19 @@
       Provider<CurrentUser> self,
       Provider<PublicKeyStore> storeProvider,
       GerritPublicKeyChecker.Factory checkerFactory,
-      AddKeySender.Factory addKeyFactory) {
+      AddKeySender.Factory addKeyFactory,
+      AccountCache accountCache,
+      AccountIndexCollection accountIndexes,
+      Provider<InternalAccountQuery> accountQueryProvider) {
     this.serverIdent = serverIdent;
     this.db = db;
     this.self = self;
     this.storeProvider = storeProvider;
     this.checkerFactory = checkerFactory;
     this.addKeyFactory = addKeyFactory;
+    this.accountCache = accountCache;
+    this.accountIndexes = accountIndexes;
+    this.accountQueryProvider = accountQueryProvider;
   }
 
   @Override
@@ -118,15 +132,28 @@
       for (PGPPublicKeyRing keyRing : newKeys) {
         PGPPublicKey key = keyRing.getPublicKey();
         AccountExternalId.Key extIdKey = toExtIdKey(key.getFingerprint());
-        AccountExternalId existing = db.get().accountExternalIds().get(extIdKey);
-        if (existing != null) {
-          if (!existing.getAccountId().equals(rsrc.getUser().getAccountId())) {
-            throw new ResourceConflictException(
-                "GPG key already associated with another account");
+        if (accountIndexes.getSearchIndex() != null) {
+          Account account = getAccountByExternalId(extIdKey.get());
+          if (account != null) {
+            if (!account.getId().equals(rsrc.getUser().getAccountId())) {
+              throw new ResourceConflictException(
+                  "GPG key already associated with another account");
+            }
+          } else {
+            newExtIds.add(
+                new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey));
           }
         } else {
-          newExtIds.add(
-              new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey));
+          AccountExternalId existing = db.get().accountExternalIds().get(extIdKey);
+          if (existing != null) {
+            if (!existing.getAccountId().equals(rsrc.getUser().getAccountId())) {
+              throw new ResourceConflictException(
+                  "GPG key already associated with another account");
+            }
+          } else {
+            newExtIds.add(
+                new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey));
+          }
         }
       }
 
@@ -141,6 +168,7 @@
               return toExtIdKey(fp.get());
             }
           }));
+      accountCache.evict(rsrc.getUser().getAccountId());
       return toJson(newKeys, toRemove, store, rsrc.getUser());
     }
   }
@@ -252,6 +280,28 @@
         BaseEncoding.base16().encode(fp));
   }
 
+  private Account getAccountByExternalId(String externalId)
+      throws OrmException {
+    List<AccountState> accountStates =
+        accountQueryProvider.get().byExternalId(externalId);
+
+    if (accountStates.isEmpty()) {
+      return null;
+    }
+
+    if (accountStates.size() > 1) {
+      StringBuilder msg = new StringBuilder();
+      msg.append("GPG key ").append(externalId)
+          .append(" associated with multiple accounts: ");
+      Joiner.on(", ").appendTo(msg,
+          Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION));
+      log.error(msg.toString());
+      throw new IllegalStateException(msg.toString());
+    }
+
+    return accountStates.get(0).getAccount();
+  }
+
   private Map<String, GpgKeyInfo> toJson(
       Collection<PGPPublicKeyRing> keys,
       Set<Fingerprint> deleted, PublicKeyStore store, IdentifiedUser user)
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
index 5a1cd45..11e9768 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
@@ -242,7 +242,7 @@
       actual.add(userIds.next());
     }
 
-    assertEquals(actual, Arrays.asList(expected));
+    assertEquals(Arrays.asList(expected), actual);
   }
 
   private CommitBuilder newCommitBuilder() {
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
index 1ca688b..f4ff9ea4 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
@@ -17,6 +17,10 @@
 import com.google.gwt.user.client.ui.SuggestOracle;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
 
 /**
  * A suggestion oracle that tries to highlight the matched text.
@@ -56,7 +60,7 @@
   }
 
   protected String getQueryPattern(final String query) {
-    return "(" + escape(query) + ")";
+    return query;
   }
 
   /**
@@ -84,19 +88,52 @@
         ds = escape(ds);
       }
 
-      // We now surround qstr by <strong>. But the chosen approach is not too
-      // smooth, if qstr is small (e.g.: "t") and this small qstr may occur in
-      // escapes (e.g.: "Tim &lt;email@example.org&gt;"). Those escapes will
-      // get <strong>-ed as well (e.g.: "&lt;" -> "&<strong>l</strong>t;"). But
-      // as repairing those mangled escapes is easier than not mangling them in
-      // the first place, we repair them afterwards.
-      ds = sgi(ds, qstr, "<strong>$1</strong>");
+      for (String qterm : splitQuery(qstr)) {
+        qterm = "(" + escape(qterm) + ")";
+        // We now surround qstr by <strong>. But the chosen approach is not too
+        // smooth, if qstr is small (e.g.: "t") and this small qstr may occur in
+        // escapes (e.g.: "Tim &lt;email@example.org&gt;"). Those escapes will
+        // get <strong>-ed as well (e.g.: "&lt;" -> "&<strong>l</strong>t;"). But
+        // as repairing those mangled escapes is easier than not mangling them in
+        // the first place, we repair them afterwards.
+        ds = sgi(ds, qterm, "<strong>$1</strong>");
+      }
+
       // Repairing <strong>-ed escapes.
       ds = sgi(ds, "(&[a-z]*)<strong>([a-z]*)</strong>([a-z]*;)", "$1$2$3");
 
       displayString = ds;
     }
 
+    /**
+     * Split the query by whitespace and filter out query terms which are
+     * substrings of other query terms.
+     */
+    private static List<String> splitQuery(String query) {
+      List<String> queryTerms = Arrays.asList(query.split("\\s+"));
+      Collections.sort(queryTerms, new Comparator<String>() {
+        @Override
+        public int compare(String s1, String s2) {
+          return Integer.compare(s2.length(), s1.length());
+        }
+      });
+
+      List<String> result = new ArrayList<>();
+      for (String s : queryTerms) {
+        boolean add = true;
+        for (String queryTerm : result) {
+          if (queryTerm.toLowerCase().contains(s.toLowerCase())) {
+            add = false;
+            break;
+          }
+        }
+        if (add) {
+          result.add(s);
+        }
+      }
+      return result;
+    }
+
     private static native String sgi(String inString, String pat, String newHtml)
     /*-{ return inString.replace(RegExp(pat, 'gi'), newHtml); }-*/;
 
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
index b8f0800..10c2a78 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
@@ -138,7 +138,7 @@
           "(?:[(]" + part + "*" + "[)])*" +
           part + "*" +
         ")",
-        "<a href=\"$1\" target=\"_blank\">$1</a>");
+        "<a href=\"$1\" target=\"_blank\" rel=\"nofollow\">$1</a>");
   }
 
   /**
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
index bf96d77..8fe743e 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
@@ -25,7 +25,8 @@
     final SafeHtml n = o.linkify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a> B");
+        "A <a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
+        + ">http://go.here/</a> B");
   }
 
   @Test
@@ -34,7 +35,8 @@
     final SafeHtml n = o.linkify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "A <a href=\"https://go.here/\" target=\"_blank\">https://go.here/</a> B");
+        "A <a href=\"https://go.here/\" target=\"_blank\" rel=\"nofollow\""
+        + ">https://go.here/</a> B");
   }
 
   @Test
@@ -43,7 +45,8 @@
     final SafeHtml n = o.linkify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "A (<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>) B");
+        "A (<a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
+        + ">http://go.here/</a>) B");
   }
 
   @Test
@@ -52,7 +55,8 @@
     final SafeHtml n = o.linkify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "A <a href=\"http://go.here/#m()\" target=\"_blank\">http://go.here/#m()</a> B");
+        "A <a href=\"http://go.here/#m()\" target=\"_blank\" rel=\"nofollow\""
+        + ">http://go.here/#m()</a> B");
   }
 
   @Test
@@ -61,7 +65,8 @@
     final SafeHtml n = o.linkify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B");
+        "A &lt;<a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
+        + ">http://go.here/</a>&gt; B");
   }
 
   @Test
@@ -70,7 +75,8 @@
     final SafeHtml n = o.linkify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "A <a href=\"http://go.here/foo\" target=\"_blank\">http://go.here/foo</a> B");
+        "A <a href=\"http://go.here/foo\" target=\"_blank\" rel=\"nofollow\""
+        + ">http://go.here/foo</a> B");
   }
 
   @Test
@@ -79,7 +85,8 @@
     final SafeHtml n = o.linkify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>. B");
+        "A <a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
+        + ">http://go.here/</a>. B");
   }
 
   @Test
@@ -88,7 +95,8 @@
     final SafeHtml n = o.linkify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>, B");
+        "A <a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
+        + ">http://go.here/</a>, B");
   }
 
   @Test
@@ -97,7 +105,8 @@
     final SafeHtml n = o.linkify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "A <a href=\"http://go.here/.\" target=\"_blank\">http://go.here/.</a>. B");
+        "A <a href=\"http://go.here/.\" target=\"_blank\" rel=\"nofollow\""
+        + ">http://go.here/.</a>. B");
   }
 
   private static SafeHtml html(String text) {
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
index 41d6f37..8f6ff8d 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
@@ -65,7 +65,8 @@
     final SafeHtml n = o.wikify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "<p>A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a> B</p>");
+        "<p>A <a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
+        + ">http://go.here/</a> B</p>");
   }
 
   @Test
@@ -74,7 +75,8 @@
     final SafeHtml n = o.wikify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "<p>A <a href=\"https://go.here/\" target=\"_blank\">https://go.here/</a> B</p>");
+        "<p>A <a href=\"https://go.here/\" target=\"_blank\" rel=\"nofollow\""
+        + ">https://go.here/</a> B</p>");
   }
 
   @Test
@@ -83,7 +85,8 @@
     final SafeHtml n = o.wikify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "<p>A (<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>) B</p>");
+        "<p>A (<a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
+        + ">http://go.here/</a>) B</p>");
   }
 
   @Test
@@ -92,7 +95,8 @@
     final SafeHtml n = o.wikify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "<p>A <a href=\"http://go.here/#m()\" target=\"_blank\">http://go.here/#m()</a> B</p>");
+        "<p>A <a href=\"http://go.here/#m()\" target=\"_blank\" rel=\"nofollow\""
+        + ">http://go.here/#m()</a> B</p>");
   }
 
   @Test
@@ -101,7 +105,8 @@
     final SafeHtml n = o.wikify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString()).isEqualTo(
-        "<p>A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B</p>");
+        "<p>A &lt;<a href=\"http://go.here/\" target=\"_blank\" rel=\"nofollow\""
+        + ">http://go.here/</a>&gt; B</p>");
   }
 
   private static SafeHtml html(String text) {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/GerritGwtUICommon.gwt.xml b/gerrit-gwtui-common/src/main/java/com/google/gerrit/GerritGwtUICommon.gwt.xml
index c147195..c01dea1 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/GerritGwtUICommon.gwt.xml
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/GerritGwtUICommon.gwt.xml
@@ -22,5 +22,22 @@
   <inherits name='com.google.gwtexpui.globalkey.GlobalKey'/>
   <inherits name='com.google.gwtexpui.progress.Progress'/>
   <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
-  <source path='client' />
+  <source path='client'>
+    <include name='AccountFormatter.java'/>
+    <include name='CommonConstants.java'/>
+    <include name='CommonMessages.java'/>
+    <include name='DateFormatter.java'/>
+    <include name='GerritUiExtensionPoint.java'/>
+    <include name='RelativeDateFormatter.java'/>
+    <include name='Resources.java'/>
+    <include name='CommonConstants.properties'/>
+    <include name='CommonMessages.properties'/>
+    <include name='info/*.java'/>
+    <include name='rpc/NativeMap.java'/>
+    <include name='rpc/Natives.java'/>
+    <include name='rpc/NativeString.java'/>
+    <include name='rpc/TransformCallback.java'/>
+    <include name='ui/HighlightSuggestion.java'/>
+    <include name='ui/RemoteSuggestOracle.java'/>
+  </source>
 </module>
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
index 088b6fd..0a339a1 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
@@ -21,6 +21,7 @@
   CHANGE_SCREEN_HEADER_RIGHT_OF_POP_DOWNS,
   CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
   CHANGE_SCREEN_BELOW_RELATED_INFO_BLOCK,
+  CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK,
 
   /* MyPasswordScreen */
   PASSWORD_SCREEN_BOTTOM,
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
index 830dcb3..7679799 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
 import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
 
 import java.sql.Timestamp;
@@ -29,6 +30,8 @@
   public final native int _accountId() /*-{ return this._account_id || 0; }-*/;
   public final native String name() /*-{ return this.name; }-*/;
   public final native String email() /*-{ return this.email; }-*/;
+  public final native JsArrayString secondaryEmails()
+      /*-{ return this.secondary_emails; }-*/;
   public final native String username() /*-{ return this.username; }-*/;
 
   public final Timestamp registeredOn() {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AgreementInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AgreementInfo.java
new file mode 100644
index 0000000..5fb2f48
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AgreementInfo.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2016 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.client.info;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class AgreementInfo extends JavaScriptObject {
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String description() /*-{ return this.description; }-*/;
+  public final native String url() /*-{ return this.url; }-*/;
+  public final native GroupInfo autoVerifyGroup() /*-{ return this.auto_verify_group; }-*/;
+
+  protected AgreementInfo() {
+  }
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
index 0e3c32b..8669dd5 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.client.info;
 
 import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayString;
 
 import java.util.ArrayList;
@@ -52,22 +52,30 @@
     return authType() == AuthType.CUSTOM_EXTENSION;
   }
 
-  public final boolean canEdit(Account.FieldName f) {
+  public final boolean canEdit(AccountFieldName f) {
     return editableAccountFields().contains(f);
   }
 
-  public final List<Account.FieldName> editableAccountFields() {
-    List<Account.FieldName> fields = new ArrayList<>();
+  public final List<AccountFieldName> editableAccountFields() {
+    List<AccountFieldName> fields = new ArrayList<>();
     for (String f : Natives.asList(_editableAccountFields())) {
-      fields.add(Account.FieldName.valueOf(f));
+      fields.add(AccountFieldName.valueOf(f));
     }
     return fields;
   }
 
+  public final List<AgreementInfo> contributorAgreements() {
+    List<AgreementInfo> agreements = new ArrayList<>();
+    for (AgreementInfo a : Natives.asList(_contributorAgreements())) {
+      agreements.add(a);
+    }
+    return agreements;
+  }
+
   public final boolean siteHasUsernames() {
     if (isCustomExtension()
         && httpPasswordUrl() != null
-        && !canEdit(FieldName.USER_NAME)) {
+        && !canEdit(AccountFieldName.USER_NAME)) {
       return false;
     }
     return true;
@@ -93,6 +101,8 @@
   private native String authTypeRaw() /*-{ return this.auth_type; }-*/;
   private native JsArrayString _editableAccountFields()
   /*-{ return this.editable_account_fields; }-*/;
+  private native JsArray<AgreementInfo> _contributorAgreements()
+  /*-{ return this.contributor_agreements; }-*/;
 
   protected AuthInfo() {
   }
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 053b8c5..9eea93e 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
@@ -333,12 +333,22 @@
       revisionInfo.takeFromEdit(edit);
       return revisionInfo;
     }
+    public static RevisionInfo forParent(int number, CommitInfo commit) {
+      RevisionInfo revisionInfo = createObject().cast();
+      revisionInfo.takeFromParent(number, commit);
+      return revisionInfo;
+    }
     private native void takeFromEdit(EditInfo edit) /*-{
       this._number = 0;
       this.name = edit.name;
       this.commit = edit.commit;
       this.edit_base = edit.base_revision;
     }-*/;
+    private native void takeFromParent(int number, CommitInfo commit) /*-{
+      this._number = number;
+      this.commit = commit;
+      this.name = this._number;
+    }-*/;
     public final native int _number() /*-{ return this._number; }-*/;
     public final native String name() /*-{ return this.name; }-*/;
     public final native boolean draft() /*-{ return this.draft || false; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java
similarity index 95%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java
index 4811e59..deed44d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.groups;
+package com.google.gerrit.client.info;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.core.client.JavaScriptObject;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java
similarity index 95%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java
index c3fd4ed..fa051a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java
@@ -12,9 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.groups;
+package com.google.gerrit.client.info;
 
-import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index b7405c7..539d53b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -89,7 +89,7 @@
 import com.google.gerrit.client.documentation.DocScreen;
 import com.google.gerrit.client.editor.EditScreen;
 import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.Screen;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index d32b745..d280e07 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -405,7 +405,9 @@
 
   @Override
   public void onModuleLoad() {
-    UserAgent.assertNotInIFrame();
+    if (!canLoadInIFrame()) {
+      UserAgent.assertNotInIFrame();
+    }
     setXsrfToken();
 
     KeyUtil.setEncoderImpl(new KeyUtil.Encoder() {
@@ -507,6 +509,10 @@
     }));
   }
 
+  private native boolean canLoadInIFrame() /*-{
+    return $wnd.gerrit_hostpagedata.canLoadInIFrame || false;
+  }-*/;
+
   private static void initHostname() {
     myHost = Location.getHostName();
     final int d1 = myHost.indexOf('.');
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
index acd2e78..9aca859 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.AgreementInfo;
 import com.google.gerrit.client.info.GpgKeyInfo;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.NativeMap;
@@ -83,6 +84,14 @@
     new RestApi("/accounts/").id(account).view("name").get(cb);
   }
 
+  /** Set the account name */
+  public static void setName(String account, String name,
+      AsyncCallback<NativeString> cb) {
+    AccountNameInput input = AccountNameInput.create();
+    input.name(name);
+    new RestApi("/accounts/").id(account).view("name").put(input, cb);
+  }
+
   /** Retrieve email addresses */
   public static void getEmails(String account,
       AsyncCallback<JsArray<EmailInfo>> cb) {
@@ -97,6 +106,13 @@
         .ifNoneMatch().put(in, cb);
   }
 
+  /** Set preferred email address */
+  public static void setPreferredEmail(String account, String email,
+      AsyncCallback<NativeString> cb) {
+    new RestApi("/accounts/").id(account).view("emails")
+        .id(email).view("preferred").put(cb);
+  }
+
   /** Retrieve SSH keys */
   public static void getSshKeys(String account,
       AsyncCallback<JsArray<SshKeyInfo>> cb) {
@@ -196,6 +212,14 @@
     new RestApi("/accounts/").id(account).view("password.http").delete(cb);
   }
 
+  /** Enter a contributor agreement */
+  public static void enterAgreement(String account, String name,
+      AsyncCallback<NativeString> cb) {
+    AgreementInput in = AgreementInput.create();
+    in.name(name);
+    new RestApi("/accounts/").id(account).view("agreements").put(in, cb);
+  }
+
   private static JsArray<ProjectWatchInfo> projectWatchArrayFromSet(
       Set<ProjectWatchInfo> set) {
     JsArray<ProjectWatchInfo> jsArray = JsArray.createArray().cast();
@@ -205,6 +229,17 @@
     return jsArray;
   }
 
+  private static class AgreementInput extends JavaScriptObject {
+    final native void name(String n) /*-{ if(n)this.name=n; }-*/;
+
+    static AgreementInput create() {
+      return createObject().cast();
+    }
+
+    protected AgreementInput() {
+    }
+  }
+
   private static class HttpPasswordInput extends JavaScriptObject {
     final native void generate(boolean g) /*-{ if(g)this.generate=g; }-*/;
 
@@ -227,6 +262,17 @@
     }
   }
 
+  private static class AccountNameInput extends JavaScriptObject {
+    final native void name(String n) /*-{ if(n)this.name=n; }-*/;
+
+    static AccountNameInput create() {
+      return createObject().cast();
+    }
+
+    protected AccountNameInput() {
+    }
+  }
+
   public static void addGpgKey(String account, String armored,
       AsyncCallback<NativeMap<GpgKeyInfo>> cb) {
     new RestApi("/accounts/")
@@ -243,6 +289,12 @@
       .post(GpgKeysInput.delete(fingerprints), cb);
   }
 
+  /** List contributor agreements */
+  public static void getAgreements(String account,
+      AsyncCallback<JsArray<AgreementInfo>> cb) {
+    new RestApi("/accounts/").id(account).view("agreements").get(cb);
+  }
+
   private static class GpgKeysInput extends JavaScriptObject {
     static GpgKeysInput add(String key) {
       return createWithAdd(Natives.arrayOf(key));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index a084612..01b5af3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -141,11 +141,8 @@
   String errorDialogTitleRegisterNewEmail();
 
   String newAgreement();
-  String agreementStatus();
   String agreementName();
   String agreementDescription();
-  String agreementStatus_EXPIRED();
-  String agreementStatus_VERIFIED();
 
   String newAgreementSelectTypeHeading();
   String newAgreementNoneAvailable();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 8cd8dc7..f74d34b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -5,7 +5,7 @@
 preferredEmail = Email Address
 registeredOn = Registered
 accountId = Account ID
-showSiteHeader = Show Site Header
+showSiteHeader = Show Site Header / Footer
 useFlashClipboard = Use Flash Clipboard Widget
 reviewCategoryLabel = Display In Review Category
 messageShowInReviewCategoryNone = None (default)
@@ -151,10 +151,7 @@
 
 
 newAgreement = New Contributor Agreement
-agreementStatus = Status
 agreementName = Name
-agreementStatus_EXPIRED = Expired
-agreementStatus_VERIFIED = Verified
 agreementDescription = Description
 
 newAgreementSelectTypeHeading = Select an agreement type:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index 136de64..f5f38fb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.CallbackGroup;
@@ -25,8 +24,7 @@
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
@@ -46,7 +44,6 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtexpui.user.client.AutoCenterDialogBox;
-import com.google.gwtjsonrpc.common.AsyncCallback;
 
 class ContactPanelShort extends Composite {
   protected final FlowPanel body;
@@ -61,6 +58,7 @@
   NpTextBox nameTxt;
   private ListBox emailPick;
   private Button registerNewEmail;
+  private OnEditEnabler onEditEnabler;
 
   ContactPanelShort() {
     body = new FlowPanel();
@@ -103,7 +101,7 @@
     }
 
     int row = 0;
-    if (!Gerrit.info().auth().canEdit(FieldName.USER_NAME)
+    if (!Gerrit.info().auth().canEdit(AccountFieldName.USER_NAME)
         && Gerrit.info().auth().siteHasUsernames()) {
       infoPlainText.resizeRows(infoPlainText.getRowCount() + 1);
       row(infoPlainText, row++, Util.C.userName(), new UsernameField());
@@ -145,7 +143,7 @@
     save.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
-        doSave(null);
+        doSave();
       }
     });
 
@@ -167,14 +165,16 @@
         }
       }
     });
+
+    onEditEnabler = new OnEditEnabler(save, nameTxt);
   }
 
   private boolean canEditFullName() {
-    return Gerrit.info().auth().canEdit(Account.FieldName.FULL_NAME);
+    return Gerrit.info().auth().canEdit(AccountFieldName.FULL_NAME);
   }
 
   private boolean canRegisterNewEmail() {
-    return Gerrit.info().auth().canEdit(Account.FieldName.REGISTER_NEW_EMAIL);
+    return Gerrit.info().auth().canEdit(AccountFieldName.REGISTER_NEW_EMAIL);
   }
 
   void hideSaveButton() {
@@ -230,7 +230,7 @@
       updateEmailList();
       registerNewEmail.setEnabled(true);
       save.setEnabled(false);
-      new OnEditEnabler(save, nameTxt);
+      onEditEnabler.updateOriginalValue(nameTxt);
     }
     display();
   }
@@ -249,7 +249,7 @@
     currentEmail = account.email();
     nameTxt.setText(account.name());
     save.setEnabled(false);
-    new OnEditEnabler(save, nameTxt);
+    onEditEnabler.updateOriginalValue(nameTxt);
   }
 
   private void doRegisterNewEmail() {
@@ -344,10 +344,13 @@
     inEmail.setFocus(true);
   }
 
-  void doSave(final AsyncCallback<Account> onSave) {
-    String newName = canEditFullName() ? nameTxt.getText() : null;
-    if (newName != null && newName.trim().isEmpty()) {
+  void doSave() {
+    final String newName;
+    String name = canEditFullName() ? nameTxt.getText() : null;
+    if (name != null && name.trim().isEmpty()) {
       newName = null;
+    } else {
+      newName = name;
     }
 
     final String newEmail;
@@ -365,24 +368,40 @@
     save.setEnabled(false);
     registerNewEmail.setEnabled(false);
 
-    Util.ACCOUNT_SEC.updateContact(newName, newEmail,
-        new GerritCallback<Account>() {
-          @Override
-          public void onSuccess(Account result) {
-            registerNewEmail.setEnabled(true);
-            onSaveSuccess(FormatUtil.asInfo(result));
-            if (onSave != null) {
-              onSave.onSuccess(result);
-            }
-          }
+    CallbackGroup group = new CallbackGroup();
+    if (!newEmail.equals(currentEmail)) {
+      AccountApi.setPreferredEmail("self", newEmail,
+          group.add(new GerritCallback<NativeString>() {
+        @Override
+        public void onSuccess(NativeString result) {
+        }
+      }));
+    }
+    AccountApi.setName("self", newName,
+        group.add(new GerritCallback<NativeString>() {
+      @Override
+      public void onSuccess(NativeString result) {
+      }
 
-          @Override
-          public void onFailure(final Throwable caught) {
-            save.setEnabled(true);
-            registerNewEmail.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
+      @Override
+      public void onFailure(Throwable caught) {
+        save.setEnabled(true);
+        registerNewEmail.setEnabled(true);
+        super.onFailure(caught);
+      }
+    }));
+    group.done();
+    group.addListener(new GerritCallback<Void>() {
+      @Override
+      public void onSuccess(Void result) {
+        currentEmail = newEmail;
+        AccountInfo me = Gerrit.getUserAccount();
+        me.email(currentEmail);
+        me.name(newName);
+        onSaveSuccess(me);
+        registerNewEmail.setEnabled(true);
+      }
+    });
   }
 
   void onSaveSuccess(AccountInfo result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
index 308cf30..47aa1cd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
@@ -15,15 +15,19 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.info.AgreementInfo;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 
+import java.util.List;
+
 public class MyAgreementsScreen extends SettingsScreen {
   private AgreementTable agreements;
 
@@ -39,71 +43,54 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SVC.myAgreements(new ScreenLoadCallback<AgreementInfo>(this) {
+    AccountApi.getAgreements(
+        "self", new ScreenLoadCallback<JsArray<AgreementInfo>>(this) {
       @Override
-      public void preDisplay(final AgreementInfo result) {
-        agreements.display(result);
-      }
-    });
+      public void preDisplay(JsArray<AgreementInfo> result) {
+        agreements.display(Natives.asList(result));
+      }});
   }
 
   private static class AgreementTable extends FancyFlexTable<ContributorAgreement> {
     AgreementTable() {
       table.setWidth("");
-      table.setText(0, 1, Util.C.agreementStatus());
-      table.setText(0, 2, Util.C.agreementName());
-      table.setText(0, 3, Util.C.agreementDescription());
+      table.setText(0, 1, Util.C.agreementName());
+      table.setText(0, 2, Util.C.agreementDescription());
 
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      for (int c = 1; c < 4; c++) {
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      for (int c = 1; c < 3; c++) {
         fmt.addStyleName(0, c, Gerrit.RESOURCES.css().dataHeader());
       }
     }
 
-    void display(final AgreementInfo result) {
+    void display(List<AgreementInfo> result) {
       while (1 < table.getRowCount()) {
         table.removeRow(table.getRowCount() - 1);
       }
 
-      for (final String k : result.accepted) {
-        addOne(result, k);
+      for (AgreementInfo info : result) {
+        addOne(info);
       }
     }
 
-    void addOne(final AgreementInfo info, final String k) {
-      final int row = table.getRowCount();
+    void addOne(AgreementInfo info) {
+      int row = table.getRowCount();
       table.insertRow(row);
       applyDataRowStyle(row);
 
-      final ContributorAgreement cla = info.agreements.get(k);
-      final String statusName;
-      if (cla == null) {
-        statusName = Util.C.agreementStatus_EXPIRED();
+      String url = info.url();
+      if (url != null && url.length() > 0) {
+        Anchor a = new Anchor(info.name(), url);
+        a.setTarget("_blank");
+        table.setWidget(row, 1, a);
       } else {
-        statusName = Util.C.agreementStatus_VERIFIED();
+        table.setText(row, 1, info.name());
       }
-      table.setText(row, 1, statusName);
-
-      if (cla == null) {
-        table.setText(row, 2, "");
-        table.setText(row, 3, "");
-      } else {
-        final String url = cla.getAgreementUrl();
-        if (url != null && url.length() > 0) {
-          final Anchor a = new Anchor(cla.getName(), url);
-          a.setTarget("_blank");
-          table.setWidget(row, 2, a);
-        } else {
-          table.setText(row, 2, cla.getName());
-        }
-        table.setText(row, 3, cla.getDescription());
-      }
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      for (int c = 1; c < 4; c++) {
+      table.setText(row, 2, info.description());
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      for (int c = 1; c < 3; c++) {
         fmt.addStyleName(row, c, Gerrit.RESOURCES.css().dataCell());
       }
-
-      setRowItem(row, cla);
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
index 14f8e2f..a53ebea 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
@@ -16,14 +16,16 @@
 
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.info.AgreementInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountScreen;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AgreementInfo;
-import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.http.client.Request;
@@ -41,7 +43,6 @@
 import com.google.gwt.user.client.ui.RadioButton;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.HashSet;
 import java.util.List;
@@ -50,8 +51,8 @@
 public class NewAgreementScreen extends AccountScreen {
   private final String nextToken;
   private Set<String> mySigned;
-  private List<ContributorAgreement> available;
-  private ContributorAgreement current;
+  private List<AgreementInfo> available;
+  private AgreementInfo current;
 
   private VerticalPanel radios;
 
@@ -73,25 +74,21 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SVC.myAgreements(new GerritCallback<AgreementInfo>() {
+    AccountApi.getAgreements(
+        "self", new GerritCallback<JsArray<AgreementInfo>>() {
       @Override
-      public void onSuccess(AgreementInfo result) {
+      public void onSuccess(JsArray<AgreementInfo> result) {
         if (isAttached()) {
-          mySigned = new HashSet<>(result.accepted);
+          mySigned = new HashSet<>();
+          for (AgreementInfo info: Natives.asList(result)) {
+            mySigned.add(info.name());
+          }
           postRPC();
         }
-      }
-    });
-    Gerrit.SYSTEM_SVC
-        .contributorAgreements(new GerritCallback<List<ContributorAgreement>>() {
-          @Override
-          public void onSuccess(final List<ContributorAgreement> result) {
-            if (isAttached()) {
-              available = result;
-              postRPC();
-            }
-          }
-        });
+      }});
+
+    available = Gerrit.info().auth().contributorAgreements();
+    postRPC();
   }
 
   @Override
@@ -158,12 +155,12 @@
     }
     radios.add(hdr);
 
-    for (final ContributorAgreement cla : available) {
-      final RadioButton r = new RadioButton("cla_id", cla.getName());
+    for (final AgreementInfo cla : available) {
+      final RadioButton r = new RadioButton("cla_id", cla.name());
       r.addStyleName(Gerrit.RESOURCES.css().contributorAgreementButton());
       radios.add(r);
 
-      if (mySigned.contains(cla.getName())) {
+      if (mySigned.contains(cla.name())) {
         r.setEnabled(false);
         final Label l = new Label(Util.C.newAgreementAlreadySubmitted());
         l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementAlreadySubmitted());
@@ -177,8 +174,8 @@
         });
       }
 
-      if (cla.getDescription() != null && !cla.getDescription().equals("")) {
-        final Label l = new Label(cla.getDescription());
+      if (cla.description() != null && !cla.description().equals("")) {
+        final Label l = new Label(cla.description());
         l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementShortDescription());
         radios.add(l);
       }
@@ -199,24 +196,24 @@
   }
 
   private void doEnterAgreement() {
-    Util.ACCOUNT_SEC.enterAgreement(current.getName(),
-        new GerritCallback<VoidResult>() {
+    AccountApi.enterAgreement("self", current.name(),
+        new GerritCallback<NativeString>() {
           @Override
-          public void onSuccess(final VoidResult result) {
+          public void onSuccess(NativeString result) {
             Gerrit.display(nextToken);
           }
 
           @Override
-          public void onFailure(final Throwable caught) {
+          public void onFailure(Throwable caught) {
             yesIAgreeBox.setText("");
             super.onFailure(caught);
           }
         });
   }
 
-  private void showCLA(final ContributorAgreement cla) {
+  private void showCLA(AgreementInfo cla) {
     current = cla;
-    String url = cla.getAgreementUrl();
+    String url = cla.url();
     if (url != null && url.length() > 0) {
       agreementGroup.setVisible(true);
       agreementHtml.setText(Gerrit.C.rpcStatusWorking());
@@ -250,7 +247,7 @@
       agreementGroup.setVisible(false);
     }
 
-    finalGroup.setVisible(cla.getAutoVerify() != null);
+    finalGroup.setVisible(cla.autoVerifyGroup() != null);
     yesIAgreeBox.setText("");
     submit.setEnabled(false);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
index c32a846..73557aa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FormPanel;
@@ -70,7 +70,7 @@
     formBody.add(contactGroup);
 
     if (Gerrit.getUserAccount().username() == null
-        && Gerrit.info().auth().canEdit(FieldName.USER_NAME)) {
+        && Gerrit.info().auth().canEdit(AccountFieldName.USER_NAME)) {
       final FlowPanel fp = new FlowPanel();
       fp.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
       fp.add(new SmallHeading(Util.C.welcomeUsernameHeading()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
index f388436..d70121b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.OnEditEnabler;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -86,7 +87,7 @@
   }
 
   private boolean canEditUserName() {
-    return Gerrit.info().auth().canEdit(Account.FieldName.USER_NAME);
+    return Gerrit.info().auth().canEdit(AccountFieldName.USER_NAME);
   }
 
   private void confirmSetUserName() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
index a0f36b9..b4b4390 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.common.data.AccountSecurity;
-import com.google.gerrit.common.data.AccountService;
 import com.google.gerrit.common.data.ProjectAdminService;
 import com.google.gwt.core.client.GWT;
 import com.google.gwtjsonrpc.client.JsonUtil;
@@ -23,14 +22,10 @@
 public class Util {
   public static final AccountConstants C = GWT.create(AccountConstants.class);
   public static final AccountMessages M = GWT.create(AccountMessages.class);
-  public static final AccountService ACCOUNT_SVC;
   public static final AccountSecurity ACCOUNT_SEC;
   public static final ProjectAdminService PROJECT_SVC;
 
   static {
-    ACCOUNT_SVC = GWT.create(AccountService.class);
-    JsonUtil.bind(ACCOUNT_SVC, "rpc/AccountService");
-
     ACCOUNT_SEC = GWT.create(AccountSecurity.class);
     JsonUtil.bind(ACCOUNT_SEC, "rpc/AccountSecurity");
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
index 254d3e6..7a32f01 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
@@ -21,8 +21,8 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.groups.GroupApi;
 import com.google.gerrit.client.groups.GroupAuditEventInfo;
-import com.google.gerrit.client.groups.GroupInfo;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.FancyFlexTable;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
index a71dffe..22a57a4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
 import com.google.gerrit.client.ui.OnEditEnabler;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index 7c0c8f6..053e7e0c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -18,8 +18,8 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.groups.GroupInfo;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
index 8c00ba7..cbe8a06 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.client.Dispatcher.toGroup;
 
 import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.MenuScreen;
 import com.google.gerrit.reviewdb.client.AccountGroup;
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 7a8888c..3896cad 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
@@ -120,6 +120,7 @@
 # Permission Names
 permissionNames = \
 	abandon, \
+	addPatchSet, \
 	create, \
 	deleteDrafts, \
 	editHashtags, \
@@ -141,6 +142,7 @@
 	viewDrafts
 
 abandon = Abandon
+addPatchSet = Add Patch Set
 create = Create Reference
 deleteDrafts = Delete Drafts
 editHashtags = Edit Hashtags
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
index a2ba5cd..4efaa61 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.client.NotFoundScreen;
 import com.google.gerrit.client.account.AccountCapabilities;
 import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.Screen;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
index 64fc0e5..94d15bd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
@@ -18,9 +18,9 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.groups.GroupInfo;
 import com.google.gerrit.client.groups.GroupList;
 import com.google.gerrit.client.groups.GroupMap;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index 340460f..9b004f9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
@@ -190,6 +190,7 @@
   @UiField Element actionDate;
   @UiField SimplePanel changeExtension;
   @UiField SimplePanel relatedExtension;
+  @UiField SimplePanel commitExtension;
 
   @UiField Actions actions;
   @UiField Labels labels;
@@ -337,6 +338,9 @@
     addExtensionPoint(
         GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_RELATED_INFO_BLOCK,
         relatedExtension, change, rev);
+    addExtensionPoint(
+        GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK,
+        commitExtension, change, rev);
   }
 
   private void addExtensionPoint(GerritUiExtensionPoint extensionPoint,
@@ -554,35 +558,40 @@
   }
 
   private void initEditMode(ChangeInfo info, String revision) {
-    if (Gerrit.isSignedIn() && info.status().isOpen()) {
+    if (Gerrit.isSignedIn()) {
       RevisionInfo rev = info.revision(revision);
-      if (isEditModeEnabled(info, rev)) {
-        editMode.setVisible(fileTableMode == FileTable.Mode.REVIEW);
-        addFile.setVisible(!editMode.isVisible());
-        deleteFile.setVisible(!editMode.isVisible());
-        renameFile.setVisible(!editMode.isVisible());
-        reviewMode.setVisible(!editMode.isVisible());
-        addFileAction = new AddFileAction(
-            changeId, info.revision(revision),
-            style, addFile, files);
-        deleteFileAction = new DeleteFileAction(
-            changeId, info.revision(revision),
-            style, addFile);
-        renameFileAction = new RenameFileAction(
-            changeId, info.revision(revision),
-            style, addFile);
-      } else {
-        editMode.setVisible(false);
-        addFile.setVisible(false);
-        reviewMode.setVisible(false);
-      }
-
-      if (rev.isEdit()) {
-        if (info.hasEditBasedOnCurrentPatchSet()) {
-          publishEdit.setVisible(true);
+      if (info.status().isOpen()) {
+        if (isEditModeEnabled(info, rev)) {
+          editMode.setVisible(fileTableMode == FileTable.Mode.REVIEW);
+          addFile.setVisible(!editMode.isVisible());
+          deleteFile.setVisible(!editMode.isVisible());
+          renameFile.setVisible(!editMode.isVisible());
+          reviewMode.setVisible(!editMode.isVisible());
+          addFileAction = new AddFileAction(
+              changeId, info.revision(revision),
+              style, addFile, files);
+          deleteFileAction = new DeleteFileAction(
+              changeId, info.revision(revision),
+              style, addFile);
+          renameFileAction = new RenameFileAction(
+              changeId, info.revision(revision),
+              style, addFile);
         } else {
-          rebaseEdit.setVisible(true);
+          editMode.setVisible(false);
+          addFile.setVisible(false);
+          reviewMode.setVisible(false);
         }
+
+        if (rev.isEdit()) {
+          if (info.hasEditBasedOnCurrentPatchSet()) {
+            publishEdit.setVisible(true);
+          } else {
+            rebaseEdit.setVisible(true);
+          }
+          deleteEdit.setVisible(true);
+        }
+      } else if (rev.isEdit()) {
+        deleteEdit.setStyleName(style.highlight());
         deleteEdit.setVisible(true);
       }
     }
@@ -601,37 +610,39 @@
 
   @UiHandler("publishEdit")
   void onPublishEdit(@SuppressWarnings("unused") ClickEvent e) {
-    EditActions.publishEdit(changeId);
+    EditActions.publishEdit(changeId, publishEdit, rebaseEdit, deleteEdit);
   }
 
   @UiHandler("rebaseEdit")
   void onRebaseEdit(@SuppressWarnings("unused") ClickEvent e) {
-    EditActions.rebaseEdit(changeId);
+    EditActions.rebaseEdit(changeId, publishEdit, rebaseEdit, deleteEdit);
   }
 
   @UiHandler("deleteEdit")
   void onDeleteEdit(@SuppressWarnings("unused") ClickEvent e) {
     if (Window.confirm(Resources.C.deleteChangeEdit())) {
-      EditActions.deleteEdit(changeId);
+      EditActions.deleteEdit(changeId, publishEdit, rebaseEdit, deleteEdit);
     }
   }
 
   @UiHandler("publish")
   void onPublish(@SuppressWarnings("unused") ClickEvent e) {
-    DraftActions.publish(changeId, revision);
+    DraftActions.publish(changeId, revision, publish, deleteRevision,
+        deleteChange);
   }
 
   @UiHandler("deleteRevision")
   void onDeleteRevision(@SuppressWarnings("unused") ClickEvent e) {
     if (Window.confirm(Resources.C.deleteDraftRevision())) {
-      DraftActions.delete(changeId, revision);
+      DraftActions.delete(changeId, revision, publish, deleteRevision,
+          deleteChange);
     }
   }
 
   @UiHandler("deleteChange")
   void onDeleteChange(@SuppressWarnings("unused") ClickEvent e) {
     if (Window.confirm(Resources.C.deleteDraftChange())) {
-      DraftActions.delete(changeId);
+      DraftActions.delete(changeId, publish, deleteRevision, deleteChange);
     }
   }
 
@@ -981,24 +992,25 @@
       final List<NativeMap<JsArray<CommentInfo>>> comments,
       final List<NativeMap<JsArray<CommentInfo>>> drafts) {
     DiffApi.list(changeId.get(),
-      base != null ? base.name() : null,
-      rev.name(),
-      group.add(new AsyncCallback<NativeMap<FileInfo>>() {
-        @Override
-        public void onSuccess(NativeMap<FileInfo> m) {
-          files.set(
-              base != null ? new PatchSet.Id(changeId, base._number()) : null,
-              new PatchSet.Id(changeId, rev._number()),
-              style, reply, fileTableMode, edit != null);
-          files.setValue(m, myLastReply,
-              comments != null ? comments.get(0) : null,
-              drafts != null ? drafts.get(0) : null);
-        }
+        rev.name(),
+        base,
+        group.add(
+            new AsyncCallback<NativeMap<FileInfo>>() {
+              @Override
+              public void onSuccess(NativeMap<FileInfo> m) {
+                files.set(
+                    base != null ? new PatchSet.Id(changeId, base._number()) : null,
+                    new PatchSet.Id(changeId, rev._number()),
+                    style, reply, fileTableMode, edit != null);
+                files.setValue(m, myLastReply,
+                    comments != null ? comments.get(0) : null,
+                    drafts != null ? drafts.get(0) : null);
+              }
 
-        @Override
-        public void onFailure(Throwable caught) {
-        }
-      }));
+              @Override
+              public void onFailure(Throwable caught) {
+              }
+            }));
   }
 
   private List<NativeMap<JsArray<CommentInfo>>> loadComments(
@@ -1117,7 +1129,6 @@
   }
 
   /**
-   *
    * Resolve a revision or patch set id string to RevisionInfo.
    * When this view is created from the changes table, revision
    * is passed as a real revision.
@@ -1131,8 +1142,17 @@
    */
   private RevisionInfo resolveRevisionOrPatchSetId(ChangeInfo info,
       String revOrId, String defaultValue) {
+    int parentNum;
     if (revOrId == null) {
       revOrId = defaultValue;
+    } else if ((parentNum = toParentNum(revOrId)) > 0) {
+      CommitInfo commitInfo = info.revision(revision).commit();
+      if (commitInfo != null) {
+        JsArray<CommitInfo> parents = commitInfo.parents();
+        if (parents.length() >= parentNum) {
+          return RevisionInfo.forParent(-parentNum, parents.get(parentNum - 1));
+        }
+      }
     } else if (!info.revisions().containsKey(revOrId)) {
       JsArray<RevisionInfo> list = info.revisions().values();
       for (int i = 0; i < list.length(); i++) {
@@ -1389,9 +1409,20 @@
 
     RevisionInfo rev = info.revisions().get(revision);
     JsArray<CommitInfo> parents = rev.commit().parents();
-    diffBase.addItem(
-      parents.length() > 1 ? Util.C.autoMerge() : Util.C.baseDiffItem(),
-      "");
+    if (parents.length() > 1) {
+      diffBase.addItem(Util.C.autoMerge(), "");
+      for (int i = 0; i < parents.length(); i++) {
+        int parentNum = i + 1;
+        diffBase.addItem(Util.M.diffBaseParent(parentNum),
+            String.valueOf(-parentNum));
+      }
+      int parentNum = toParentNum(base);
+      if (parentNum > 0) {
+        selectedIdx = list.length() + parentNum;
+      }
+    } else {
+      diffBase.addItem(Util.C.baseDiffItem(), "");
+    }
 
     diffBase.setSelectedIndex(selectedIdx);
   }
@@ -1443,4 +1474,22 @@
   private static String normalize(String r) {
     return r != null && !r.isEmpty() ? r : null;
   }
+
+  /**
+   * @param parentToken
+   * @return 1-based parentNum if parentToken is a String which can be parsed as
+   *     a negative integer i.e. "-1", "-2", etc. If parentToken cannot be
+   *     parsed as a negative integer, return zero.
+   */
+  private static int toParentNum(String parentToken) {
+    try {
+      int n = Integer.parseInt(parentToken);
+      if (n < 0) {
+        return -n;
+      }
+      return 0;
+    } catch (NumberFormatException e) {
+      return 0;
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
index 0916c00..a0d5405 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
@@ -351,6 +351,10 @@
       padding-top: 5px;
     }
 
+    .commitExtension {
+      padding-top: 5px;
+    }
+
     .pushCertStatus {
       padding-left: 5px;
     }
@@ -434,6 +438,7 @@
       <tr>
         <td class='{style.commitColumn}'>
           <c:CommitBox ui:field='commit'/>
+          <g:SimplePanel ui:field='commitExtension' styleName='{style.commitExtension}'/>
         </td>
         <td class='{style.infoColumn}'>
           <table id='change_infoTable'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java
index 634190a2..6787576 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java
@@ -21,23 +21,25 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Button;
 
 public class DraftActions {
 
-  static void publish(Change.Id id, String revision) {
-    ChangeApi.publish(id.get(), revision, cs(id));
+  static void publish(Change.Id id, String revision, Button... draftButtons) {
+    ChangeApi.publish(id.get(), revision, cs(id, draftButtons));
   }
 
-  static void delete(Change.Id id, String revision) {
-    ChangeApi.deleteRevision(id.get(), revision, cs(id));
+  static void delete(Change.Id id, String revision, Button... draftButtons) {
+    ChangeApi.deleteRevision(id.get(), revision, cs(id, draftButtons));
   }
 
-  static void delete(Change.Id id) {
-    ChangeApi.deleteChange(id.get(), mine());
+  static void delete(Change.Id id, Button... draftButtons) {
+    ChangeApi.deleteChange(id.get(), mine(draftButtons));
   }
 
   public static GerritCallback<JavaScriptObject> cs(
-      final Change.Id id) {
+      final Change.Id id, final Button... draftButtons) {
+    setEnabled(false, draftButtons);
     return new GerritCallback<JavaScriptObject>() {
       @Override
       public void onSuccess(JavaScriptObject result) {
@@ -46,6 +48,7 @@
 
       @Override
       public void onFailure(Throwable err) {
+        setEnabled(true, draftButtons);
         if (SubmitFailureDialog.isConflict(err)) {
           new SubmitFailureDialog(err.getMessage()).center();
           Gerrit.display(PageLinks.toChange(id));
@@ -56,7 +59,9 @@
     };
   }
 
-  private static AsyncCallback<JavaScriptObject> mine() {
+  private static AsyncCallback<JavaScriptObject> mine(
+      final Button... draftButtons) {
+    setEnabled(false, draftButtons);
     return new GerritCallback<JavaScriptObject>() {
       @Override
       public void onSuccess(JavaScriptObject result) {
@@ -65,6 +70,7 @@
 
       @Override
       public void onFailure(Throwable err) {
+        setEnabled(true, draftButtons);
         if (SubmitFailureDialog.isConflict(err)) {
           new SubmitFailureDialog(err.getMessage()).center();
           Gerrit.display(PageLinks.MINE);
@@ -74,4 +80,12 @@
       }
     };
   }
+
+  private static void setEnabled(boolean enabled, Button... draftButtons) {
+    if (draftButtons != null) {
+      for (Button b : draftButtons) {
+        b.setEnabled(enabled);
+      }
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
index d11cf7e..97abddb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
@@ -20,23 +20,25 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.ui.Button;
 
 public class EditActions {
 
-  static void deleteEdit(Change.Id id) {
-    ChangeApi.deleteEdit(id.get(), cs(id));
+  static void deleteEdit(Change.Id id, Button... editButtons) {
+    ChangeApi.deleteEdit(id.get(), cs(id, editButtons));
   }
 
-  static void publishEdit(Change.Id id) {
-    ChangeApi.publishEdit(id.get(), cs(id));
+  static void publishEdit(Change.Id id, Button... editButtons) {
+    ChangeApi.publishEdit(id.get(), cs(id, editButtons));
   }
 
-  static void rebaseEdit(Change.Id id) {
-    ChangeApi.rebaseEdit(id.get(), cs(id));
+  static void rebaseEdit(Change.Id id, Button... editButtons) {
+    ChangeApi.rebaseEdit(id.get(), cs(id, editButtons));
   }
 
   public static GerritCallback<JavaScriptObject> cs(
-      final Change.Id id) {
+      final Change.Id id, final Button... editButtons) {
+    setEnabled(false, editButtons);
     return new GerritCallback<JavaScriptObject>() {
       @Override
       public void onSuccess(JavaScriptObject result) {
@@ -45,6 +47,7 @@
 
       @Override
       public void onFailure(Throwable err) {
+        setEnabled(true, editButtons);
         if (SubmitFailureDialog.isConflict(err)) {
           new SubmitFailureDialog(err.getMessage()).center();
           Gerrit.display(PageLinks.toChange(id));
@@ -54,4 +57,12 @@
       }
     };
   }
+
+  private static void setEnabled(boolean enabled, Button... editButtons) {
+    if (editButtons != null) {
+      for (Button b : editButtons) {
+        b.setEnabled(enabled);
+      }
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
index 60d66e0..f0a7ce3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -712,8 +713,8 @@
     }
 
     private void columnComments(SafeHtmlBuilder sb, FileInfo info) {
-      JsArray<CommentInfo> cList = get(info.path(), comments);
-      JsArray<CommentInfo> dList = get(info.path(), drafts);
+      JsArray<CommentInfo> cList = filterForParent(get(info.path(), comments));
+      JsArray<CommentInfo> dList = filterForParent(get(info.path(), drafts));
 
       sb.openTd().setStyleName(R.css().draftColumn());
       if (dList.length() > 0) {
@@ -747,6 +748,20 @@
       sb.closeTd();
     }
 
+    private JsArray<CommentInfo> filterForParent(JsArray<CommentInfo> list) {
+      JsArray<CommentInfo> result = JsArray.createArray().cast();
+      for (CommentInfo c : Natives.asList(list)) {
+        if (c.side() == Side.REVISION) {
+          result.push(c);
+        } else if (base == null && !c.hasParent()) {
+          result.push(c);
+        } else if (base != null && c.parent() == -base.get()) {
+          result.push(c);
+        }
+      }
+      return result;
+    }
+
     private JsArray<CommentInfo> get(String p, NativeMap<JsArray<CommentInfo>> m) {
       JsArray<CommentInfo> r = null;
       if (m != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java
index db78f39..f6022f9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.diff.CommentRange;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -126,18 +127,34 @@
     final StorageBackend storage = new StorageBackend();
     for (final String cookie : storage.getKeys()) {
       if (isInlineComment(cookie)) {
-        GerritCallback<CommentInfo> cb = new GerritCallback<CommentInfo>() {
-          @Override
-          public void onSuccess(CommentInfo result) {
-            storage.removeItem(cookie);
-          }
-        };
         InlineComment input = getInlineComment(cookie);
         if (input.commentInfo.id() == null) {
-          CommentApi.createDraft(input.psId, input.commentInfo, cb);
+          CommentApi.createDraft(input.psId, input.commentInfo,
+              new GerritCallback<CommentInfo>() {
+                @Override
+                public void onSuccess(CommentInfo result) {
+                  storage.removeItem(cookie);
+                }
+              });
         } else {
           CommentApi.updateDraft(input.psId, input.commentInfo.id(),
-              input.commentInfo, cb);
+              input.commentInfo, new GerritCallback<CommentInfo>() {
+                @Override
+                public void onSuccess(CommentInfo result) {
+                  storage.removeItem(cookie);
+                }
+
+                @Override
+                public void onFailure(Throwable caught) {
+                  if (RestApi.isNotFound(caught)) {
+                    // the draft comment, that was supposed to be updated,
+                    // was deleted in the meantime
+                    storage.removeItem(cookie);
+                  } else {
+                    super.onFailure(caught);
+                  }
+                }
+              });
         }
       }
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
index 9b3668c..791effc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
@@ -284,6 +284,8 @@
       } else {
         sb.openSpan().setStyleName(RelatedChanges.R.css().subject());
       }
+      sb.setAttribute("data-branch", info.branch());
+      sb.setAttribute("data-project", info.project());
       String url = url();
       if (url != null) {
         sb.openAnchor().setAttribute("href", url);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
index 1c0486d..2188c03 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.client.change;
 
-import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.admin.Util;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.groups.GroupBaseInfo;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GroupBaseInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.ui.AccountSuggestOracle;
 import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.JavaScriptObject;
@@ -42,7 +42,7 @@
           public void onSuccess(JsArray<SuggestReviewerInfo> result) {
             List<RestReviewerSuggestion> r = new ArrayList<>(result.length());
             for (SuggestReviewerInfo reviewer : Natives.asList(result)) {
-              r.add(new RestReviewerSuggestion(reviewer));
+              r.add(new RestReviewerSuggestion(reviewer, req.getQuery()));
             }
             cb.onSuggestionsReady(req, new Response(r));
           }
@@ -60,29 +60,29 @@
   }
 
   private static class RestReviewerSuggestion implements Suggestion {
-    private final SuggestReviewerInfo reviewer;
+    private final String displayString;
+    private final String replacementString;
 
-    RestReviewerSuggestion(final SuggestReviewerInfo reviewer) {
-      this.reviewer = reviewer;
+    RestReviewerSuggestion(SuggestReviewerInfo reviewer, String query) {
+      if (reviewer.account() != null) {
+        this.replacementString = AccountSuggestOracle.AccountSuggestion
+            .format(reviewer.account(), query);
+        this.displayString = replacementString;
+      } else {
+        this.replacementString = reviewer.group().name();
+        this.displayString =
+            replacementString + " (" + Util.C.suggestedGroupLabel() + ")";
+      }
     }
 
     @Override
     public String getDisplayString() {
-      if (reviewer.account() != null) {
-        return FormatUtil.nameEmail(reviewer.account());
-      }
-      return reviewer.group().name()
-          + " ("
-          + Util.C.suggestedGroupLabel()
-          + ")";
+      return displayString;
     }
 
     @Override
     public String getReplacementString() {
-      if (reviewer.account() != null) {
-        return FormatUtil.nameEmail(reviewer.account());
-      }
-      return reviewer.group().name();
+      return replacementString;
     }
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index c5397ee..b192bd5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -42,4 +42,6 @@
   String changeQueryPageTitle(String query);
 
   String insertionsAndDeletions(int insertions, int deletions);
+
+  String diffBaseParent(int parentNum);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index f0d7e59..2b68492 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -23,3 +23,5 @@
 changeQueryPageTitle = Search for {0}
 
 insertionsAndDeletions = +{0}, -{1}
+
+diffBaseParent = Parent {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
index 8e73f73..d42c344 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
@@ -25,9 +25,15 @@
 public class CommentInfo extends JavaScriptObject {
   public static CommentInfo create(String path, Side side,
       int line, CommentRange range) {
+    return create(path, side, 0, line, range);
+  }
+
+  public static CommentInfo create(String path, Side side, int parent,
+      int line, CommentRange range) {
     CommentInfo n = createObject().cast();
     n.path(path);
     n.side(side);
+    n.parent(parent);
     if (range != null) {
       n.line(range.endLine());
       n.range(range);
@@ -41,6 +47,7 @@
     CommentInfo n = createObject().cast();
     n.path(r.path());
     n.side(r.side());
+    n.parent(r.parent());
     n.inReplyTo(r.id());
     if (r.hasRange()) {
       n.line(r.range().endLine());
@@ -55,6 +62,7 @@
     CommentInfo n = createObject().cast();
     n.path(s.path());
     n.side(s.side());
+    n.parent(s.parent());
     n.id(s.id());
     n.inReplyTo(s.inReplyTo());
     n.message(s.message());
@@ -78,6 +86,8 @@
     sideRaw(side.toString());
   }
   private native void sideRaw(String s) /*-{ this.side = s }-*/;
+  public final native void parent(int n) /*-{ this.parent = n }-*/;
+  public final native boolean hasParent() /*-{ return this.hasOwnProperty('parent') }-*/;
 
   public final native String path() /*-{ return this.path }-*/;
   public final native String id() /*-{ return this.id }-*/;
@@ -91,6 +101,7 @@
         : Side.REVISION;
   }
   private native String sideRaw() /*-{ return this.side }-*/;
+  public final native int parent() /*-{ return this.parent }-*/;
 
   public final Timestamp updated() {
     Timestamp r = updatedTimestamp();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
index a26b1ce..2f3ead3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
@@ -129,16 +129,29 @@
   }
 
   Side getStoredSideFromDisplaySide(DisplaySide side) {
-    return side == DisplaySide.A && base == null ? Side.PARENT : Side.REVISION;
+    if (side == DisplaySide.A && (base == null || base.get() < 0)) {
+      return Side.PARENT;
+    }
+    return Side.REVISION;
+  }
+
+  int getParentNumFromDisplaySide(DisplaySide side) {
+    if (side == DisplaySide.A && base != null && base.get() < 0) {
+      return -base.get();
+    }
+    return 0;
   }
 
   PatchSet.Id getPatchSetIdFromSide(DisplaySide side) {
-    return side == DisplaySide.A && base != null ? base : revision;
+    if (side == DisplaySide.A && base != null && base.get() >= 0) {
+      return base;
+    }
+    return revision;
   }
 
   DisplaySide displaySide(CommentInfo info, DisplaySide forSide) {
     if (info.side() == Side.PARENT) {
-      return base == null ? DisplaySide.A : null;
+      return (base == null || base.get() < 0) ? DisplaySide.A : null;
     }
     return forSide;
   }
@@ -179,6 +192,7 @@
       addDraftBox(side, CommentInfo.create(
           getPath(),
           getStoredSideFromDisplaySide(side),
+          getParentNumFromDisplaySide(side),
           line,
           null)).setEdit(true);
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
index 83f74a3..ce1d294 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -46,13 +47,13 @@
   }
 
   void load(CallbackGroup group) {
-    if (base != null) {
+    if (base != null && base.get() > 0) {
       CommentApi.comments(base, group.add(publishedBase()));
     }
     CommentApi.comments(revision, group.add(publishedRevision()));
 
     if (Gerrit.isSignedIn()) {
-      if (base != null) {
+      if (base != null && base.get() > 0) {
         CommentApi.drafts(base, group.add(draftsBase()));
       }
       CommentApi.drafts(revision, group.add(draftsRevision()));
@@ -60,7 +61,7 @@
   }
 
   boolean hasCommentForPath(String filePath) {
-    if (base != null) {
+    if (base != null && base.get() > 0) {
       JsArray<CommentInfo> forBase = publishedBaseAll.get(filePath);
       if (forBase != null && forBase.length() > 0) {
         return true;
@@ -91,6 +92,9 @@
     return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
       @Override
       public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+        for (String k : result.keySet()) {
+          result.put(k, filterForParent(result.get(k)));
+        }
         publishedRevisionAll = result;
         publishedRevision = sort(result.get(path));
       }
@@ -101,6 +105,20 @@
     };
   }
 
+    private JsArray<CommentInfo> filterForParent(JsArray<CommentInfo> list) {
+      JsArray<CommentInfo> result = JsArray.createArray().cast();
+      for (CommentInfo c : Natives.asList(list)) {
+        if (c.side() == Side.REVISION) {
+          result.push(c);
+        } else if (base == null && !c.hasParent()) {
+          result.push(c);
+        } else if (base != null && c.parent() == -base.get()) {
+          result.push(c);
+        }
+      }
+      return result;
+    }
+
   private AsyncCallback<NativeMap<JsArray<CommentInfo>>> draftsBase() {
     return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
       @Override
@@ -118,6 +136,9 @@
     return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
       @Override
       public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+        for (String k : result.keySet()) {
+          result.put(k, filterForParent(result.get(k)));
+        }
         draftsRevision = sort(result.get(path));
       }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
index bc5a305..e3720cc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_ALL;
 
 import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.info.FileInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.RestApi;
@@ -25,11 +26,15 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 public class DiffApi {
-  public static void list(int id, String base, String revision,
+  public static void list(int id, String revision, RevisionInfo base,
       AsyncCallback<NativeMap<FileInfo>> cb) {
     RestApi api = ChangeApi.revision(id, revision).view("files");
     if (base != null) {
-      api.addParameter("base", base);
+      if (base._number() < 0) {
+        api.addParameter("parent", -base._number());
+      } else {
+        api.addParameter("base", base.name());
+      }
     }
     api.get(NativeMap.copyKeysIntoChildren("path", cb));
   }
@@ -38,7 +43,11 @@
       AsyncCallback<NativeMap<FileInfo>> cb) {
     RestApi api = ChangeApi.revision(id).view("files");
     if (base != null) {
-      api.addParameter("base", base.get());
+      if (base.get() < 0) {
+        api.addParameter("parent", -base.get());
+      } else {
+        api.addParameter("base", base.get());
+      }
     }
     api.get(NativeMap.copyKeysIntoChildren("path", cb));
   }
@@ -57,7 +66,11 @@
 
   public DiffApi base(PatchSet.Id id) {
     if (id != null) {
-      call.addParameter("base", id.get());
+      if (id.get() < 0) {
+        call.addParameter("parent", -id.get());
+      } else {
+        call.addParameter("base", id.get());
+      }
     }
     return this;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
index 2264871..8935e36 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.client.diff.DiffInfo.FileMeta;
 import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
 import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
 import com.google.gerrit.client.info.ChangeInfo.EditInfo;
 import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.info.FileInfo;
@@ -116,6 +117,7 @@
   private List<HandlerRegistration> handlers;
   private PreferencesAction prefsAction;
   private int reloadVersionId;
+  private int parents;
 
   @UiField(provided = true)
   Header header;
@@ -213,6 +215,8 @@
         new CommentsCollections(base, revision, path);
     comments.load(group2);
 
+    countParents(group2);
+
     RestApi call = ChangeApi.detail(changeId.get());
     ChangeList.addOptions(call, EnumSet.of(
         ListChangesOption.ALL_REVISIONS));
@@ -231,7 +235,7 @@
             revision.get() == info.revision(currentRevision)._number();
         JsArray<RevisionInfo> list = info.revisions().values();
         RevisionInfo.sortRevisionInfoByNumber(list);
-        getDiffTable().set(prefs, list, diff, edit != null, current,
+        getDiffTable().set(prefs, list, parents, diff, edit != null, current,
             changeStatus.isOpen(), diff.binary());
         header.setChangeInfo(info);
       }
@@ -245,6 +249,22 @@
         getScreenLoadCallback(comments)));
   }
 
+  private void countParents(CallbackGroup cbg) {
+    ChangeApi.revision(changeId.get(), revision.getId())
+        .view("commit")
+        .get(cbg.add(new AsyncCallback<CommitInfo>() {
+          @Override
+          public void onSuccess(CommitInfo info) {
+            parents = info.parents().length();
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            parents = 0;
+          }
+        }));
+  }
+
   @Override
   public void onShowView() {
     super.onShowView();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
index 4374986..392ad2f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
@@ -108,12 +108,12 @@
     patchSetSelectBoxB.setUpBlame(cm, false, rev, path);
   }
 
-  void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info,
+  void set(DiffPreferences prefs, JsArray<RevisionInfo> list, int parents, DiffInfo info,
       boolean editExists, boolean current, boolean open, boolean binary) {
     this.changeType = info.changeType();
-    patchSetSelectBoxA.setUpPatchSetNav(list, info.metaA(), editExists,
+    patchSetSelectBoxA.setUpPatchSetNav(list, parents, info.metaA(), editExists,
         current, open, binary);
-    patchSetSelectBoxB.setUpPatchSetNav(list, info.metaB(), editExists,
+    patchSetSelectBoxB.setUpPatchSetNav(list, parents, info.metaB(), editExists,
         current, open, binary);
 
     JsArrayString hdr = info.diffHeader();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
index 39b85cf..bc37abb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.blame.BlameInfo;
 import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.patches.PatchUtil;
@@ -87,13 +88,29 @@
     this.path = path;
   }
 
-  void setUpPatchSetNav(JsArray<RevisionInfo> list, DiffInfo.FileMeta meta,
+  void setUpPatchSetNav(JsArray<RevisionInfo> list, int parents, DiffInfo.FileMeta meta,
       boolean editExists, boolean current, boolean open, boolean binary) {
-    InlineHyperlink baseLink = null;
     InlineHyperlink selectedLink = null;
     if (sideA) {
-      baseLink = createLink(PatchUtil.C.patchBase(), null);
-      linkPanel.add(baseLink);
+      if (parents <= 1) {
+        InlineHyperlink link = createLink(PatchUtil.C.patchBase(), null);
+        linkPanel.add(link);
+        selectedLink = link;
+      } else {
+        for (int i = parents; i > 0; i--) {
+          PatchSet.Id id = new PatchSet.Id(changeId, -i);
+          InlineHyperlink link = createLink(Util.M.diffBaseParent(i), id);
+          linkPanel.add(link);
+          if (revision != null && id.equals(revision)) {
+            selectedLink = link;
+          }
+        }
+        InlineHyperlink link = createLink(Util.C.autoMerge(), null);
+        linkPanel.add(link);
+        if (selectedLink == null) {
+          selectedLink = link;
+        }
+      }
     }
     for (int i = 0; i < list.length(); i++) {
       RevisionInfo r = list.get(i);
@@ -106,8 +123,6 @@
     }
     if (selectedLink != null) {
       selectedLink.setStyleName(style.selected());
-    } else if (sideA) {
-      baseLink.setStyleName(style.selected());
     }
 
     if (meta == null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
index 2b83b71..bcb7dac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
@@ -84,6 +84,7 @@
       addDraftBox(cm.side(), CommentInfo.create(
               getPath(),
               getStoredSideFromDisplaySide(cm.side()),
+              getParentNumFromDisplaySide(cm.side()),
               line,
               CommentRange.create(fromTo))).setEdit(true);
       cm.setCursor(fromTo.to());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
index 93be87b..760f06d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.RestApi;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
index ed41b65..5bcdc6b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.groups;
 
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
index a24e1dc..f51ecb8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.groups;
 
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.core.client.JsArray;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
index 5532285..5e23049 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.groups;
 
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gwt.user.client.rpc.AsyncCallback;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
index a0e25ad..111f19a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -89,13 +89,13 @@
   public static boolean isExpected(int statusCode) {
     switch (statusCode) {
       case SC_UNAVAILABLE:
-      case 400: // Bad Request
-      case 401: // Unauthorized
-      case 403: // Forbidden
-      case 404: // Not Found
-      case 405: // Method Not Allowed
-      case 409: // Conflict
-      case 412: // Precondition Failed
+      case Response.SC_BAD_REQUEST:
+      case Response.SC_UNAUTHORIZED:
+      case Response.SC_FORBIDDEN:
+      case Response.SC_NOT_FOUND:
+      case Response.SC_METHOD_NOT_ALLOWED:
+      case Response.SC_CONFLICT:
+      case Response.SC_PRECONDITION_FAILED:
       case 422: // Unprocessable Entity
       case 429: // Too Many Requests (RFC 6585)
         return true;
@@ -251,7 +251,7 @@
   }
 
   public RestApi id(String id) {
-    return idRaw(URL.encodeQueryString(id));
+    return idRaw(URL.encodePathSegment(id));
   }
 
   public RestApi id(int id) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
index a96624a..983d48c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.client.ui;
 
-import com.google.gerrit.client.groups.GroupInfo;
 import com.google.gerrit.client.groups.GroupMap;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.reviewdb.client.AccountGroup;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
index 60c23df..3702e68 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
@@ -35,28 +35,53 @@
           public void onSuccess(JsArray<AccountInfo> in) {
             List<AccountSuggestion> r = new ArrayList<>(in.length());
             for (AccountInfo p : Natives.asList(in)) {
-              r.add(new AccountSuggestion(p));
+              r.add(new AccountSuggestion(p, req.getQuery()));
             }
             cb.onSuggestionsReady(req, new Response(r));
           }
         });
   }
 
-  private static class AccountSuggestion implements SuggestOracle.Suggestion {
-    private final AccountInfo info;
+  public static class AccountSuggestion implements SuggestOracle.Suggestion {
+    private final String suggestion;
 
-    AccountSuggestion(final AccountInfo k) {
-      info = k;
+    AccountSuggestion(AccountInfo info, String query) {
+      this.suggestion = format(info, query);
     }
 
     @Override
     public String getDisplayString() {
-      return FormatUtil.nameEmail(info);
+      return suggestion;
     }
 
     @Override
     public String getReplacementString() {
-      return FormatUtil.nameEmail(info);
+      return suggestion;
+    }
+
+    public static String format(AccountInfo info, String query) {
+      String s = FormatUtil.nameEmail(info);
+      if (!containsQuery(s, query) && info.secondaryEmails() != null) {
+        for (String email : Natives.asList(info.secondaryEmails())) {
+          AccountInfo info2 = AccountInfo.create(info._accountId(), info.name(),
+              email, info.username());
+          String s2 = FormatUtil.nameEmail(info2);
+          if (containsQuery(s2, query)) {
+            s = s2;
+            break;
+          }
+        }
+      }
+      return s;
+    }
+
+    private static boolean containsQuery(String s, String query) {
+      for (String qterm : query.split("\\s+")) {
+        if (!s.toLowerCase().contains(qterm.toLowerCase())) {
+          return false;
+        }
+      }
+      return true;
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
index 819a11f..4391477 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
@@ -73,6 +73,9 @@
     widget = w;
   }
 
+  public void updateOriginalValue(final TextBoxBase tb) {
+    originalValue = tb.getValue().trim();
+  }
 
   // Register input widgets to be listened to
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
index 5146b31..7935bb6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd;
 
-import static com.google.gerrit.reviewdb.client.AuthType.OAUTH;
+import static com.google.gerrit.extensions.client.AuthType.OAUTH;
 
 import com.google.gerrit.reviewdb.client.CoreDownloadSchemes;
 import com.google.gerrit.server.config.AuthConfig;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index b1db772..bae6d19 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
-import com.google.gerrit.server.git.UploadPackMetricsHook;
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -51,6 +50,8 @@
 import org.eclipse.jgit.http.server.resolver.AsIsFileService;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PostUploadHook;
+import org.eclipse.jgit.transport.PostUploadHookChain;
 import org.eclipse.jgit.transport.PreUploadHook;
 import org.eclipse.jgit.transport.PreUploadHookChain;
 import org.eclipse.jgit.transport.ReceivePack;
@@ -198,16 +199,16 @@
 
   static class UploadFactory implements UploadPackFactory<HttpServletRequest> {
     private final TransferConfig config;
-    private final UploadPackMetricsHook uploadMetrics;
     private final DynamicSet<PreUploadHook> preUploadHooks;
+    private final DynamicSet<PostUploadHook> postUploadHooks;
 
     @Inject
     UploadFactory(TransferConfig tc,
-        UploadPackMetricsHook uploadMetrics,
-        DynamicSet<PreUploadHook> preUploadHooks) {
+        DynamicSet<PreUploadHook> preUploadHooks,
+        DynamicSet<PostUploadHook> postUploadHooks) {
       this.config = tc;
-      this.uploadMetrics = uploadMetrics;
       this.preUploadHooks = preUploadHooks;
+      this.postUploadHooks = postUploadHooks;
     }
 
     @Override
@@ -217,7 +218,8 @@
       up.setTimeout(config.getTimeout());
       up.setPreUploadHook(PreUploadHookChain.newChain(
           Lists.newArrayList(preUploadHooks)));
-      up.setPostUploadHook(uploadMetrics);
+      up.setPostUploadHook(
+          PostUploadHookChain.newChain(Lists.newArrayList(postUploadHooks)));
       return up;
     }
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index fae429d..b06f370 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -20,6 +20,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
@@ -153,17 +154,11 @@
 
     try {
       AuthResult whoAuthResult = accountManager.authenticate(whoAuth);
-      WebSession ws = session.get();
-      ws.setUserAccountId(whoAuthResult.getAccountId());
-      ws.setAccessPathOk(AccessPath.GIT, true);
-      ws.setAccessPathOk(AccessPath.REST_API, true);
+      setUserIdentified(whoAuthResult.getAccountId());
       return true;
     } catch (NoSuchUserException e) {
       if (password.equals(who.getPassword(who.getUserName()))) {
-        WebSession ws = session.get();
-        ws.setUserAccountId(who.getAccount().getId());
-        ws.setAccessPathOk(AccessPath.GIT, true);
-        ws.setAccessPathOk(AccessPath.REST_API, true);
+        setUserIdentified(who.getAccount().getId());
         return true;
       }
       log.warn("Authentication failed for " + username, e);
@@ -180,6 +175,13 @@
     }
   }
 
+  private void setUserIdentified(Account.Id id) {
+    WebSession ws = session.get();
+    ws.setUserAccountId(id);
+    ws.setAccessPathOk(AccessPath.GIT, true);
+    ws.setAccessPathOk(AccessPath.REST_API, true);
+  }
+
   private boolean passwordMatchesTheUserGeneratedOne(AccountState who,
       String username, String password) {
     String accountPassword = who.getPassword(username);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index f6b79e5..210800d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -20,11 +20,13 @@
 
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.servlet.ServletModule;
 
@@ -55,14 +57,17 @@
     }
   }
 
+  private final Provider<ReviewDb> db;
   private final boolean enabled;
   private final DynamicItem<WebSession> session;
   private final AccountResolver accountResolver;
 
   @Inject
-  RunAsFilter(AuthConfig config,
+  RunAsFilter(Provider<ReviewDb> db,
+      AuthConfig config,
       DynamicItem<WebSession> session,
       AccountResolver accountResolver) {
+    this.db = db;
     this.enabled = config.isRunAsEnabled();
     this.session = session;
     this.accountResolver = accountResolver;
@@ -95,7 +100,7 @@
 
       Account target;
       try {
-        target = accountResolver.find(runas);
+        target = accountResolver.find(db.get(), runas);
       } catch (OrmException e) {
         log.warn("cannot resolve account for " + RUN_AS, e);
         replyError(req, res,
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 2c67182..fff43d3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.httpd.raw.CatServlet;
 import com.google.gerrit.httpd.raw.HostPageServlet;
 import com.google.gerrit.httpd.raw.LegacyGerritServlet;
@@ -30,7 +31,6 @@
 import com.google.gerrit.httpd.restapi.GroupsRestApiServlet;
 import com.google.gerrit.httpd.restapi.ProjectsRestApiServlet;
 import com.google.gerrit.httpd.rpc.doc.QueryDocumentationFilter;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AuthConfig;
@@ -64,6 +64,7 @@
 
     if (options.enableDefaultUi()) {
       filter("/").through(XsrfCookieFilter.class);
+      filter("/accounts/self/detail").through(XsrfCookieFilter.class);
       serve("/").with(HostPageServlet.class);
       serve("/Gerrit").with(LegacyGerritServlet.class);
       serve("/Gerrit/*").with(legacyGerritScreen());
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 8ede324..8d5aff6 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
@@ -27,8 +27,10 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -58,16 +60,19 @@
   private final DynamicItem<WebSession> webSession;
   private final AccountManager accountManager;
   private final SiteHeaderFooter headers;
+  private final InternalAccountQuery accountQuery;
 
   @Inject
-  BecomeAnyAccountLoginServlet(final DynamicItem<WebSession> ws,
-      final SchemaFactory<ReviewDb> sf,
-      final AccountManager am,
-      SiteHeaderFooter shf) {
+  BecomeAnyAccountLoginServlet(DynamicItem<WebSession> ws,
+      SchemaFactory<ReviewDb> sf,
+      AccountManager am,
+      SiteHeaderFooter shf,
+      InternalAccountQuery aq) {
     webSession = ws;
     schema = sf;
     accountManager = am;
     headers = shf;
+    accountQuery = aq;
   }
 
   @Override
@@ -184,12 +189,25 @@
   }
 
   private AuthResult byUserName(final String userName) {
-    try (ReviewDb db = schema.open()) {
-      AccountExternalId.Key key =
+    try {
+      AccountExternalId.Key extKey =
           new AccountExternalId.Key(SCHEME_USERNAME, userName);
-      return auth(db.accountExternalIds().get(key));
+      List<AccountState> accountStates =
+          accountQuery.byExternalId(extKey.get());
+      if (accountStates.isEmpty()) {
+        getServletContext()
+            .log("No accounts with username " + userName + " found");
+        return null;
+      }
+      if (accountStates.size() > 1) {
+        getServletContext()
+            .log("Multiple accounts with username " + userName + " found");
+        return null;
+      }
+      return auth(new AccountExternalId(
+          accountStates.get(0).getAccount().getId(), extKey));
     } catch (OrmException e) {
-      getServletContext().log("cannot query database", e);
+      getServletContext().log("cannot query account index", e);
       return null;
     }
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 8594e30..27aff21 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -359,7 +359,6 @@
           if (Strings.isNullOrEmpty(entryTitle)) {
             entryTitle = rsrc.substring(nameOffset, rsrc.length() - 3).replace('-', ' ');
           }
-          rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
         } else {
           entryTitle = rsrc.substring(nameOffset).replace('-', ' ');
         }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
index af4776d..c01c489 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED;
 
 import com.google.gerrit.extensions.registration.RegistrationHandle;
@@ -31,9 +32,13 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.BufferedWriter;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
 
 import javax.servlet.FilterChain;
 import javax.servlet.ServletConfig;
@@ -55,36 +60,38 @@
   public static final String URL_REGEX =
       "^(?:/a)?(?:/p/|/)(.+)(?:/info/lfs/objects/batch)$";
 
+  private static final String CONTENTTYPE_VND_GIT_LFS_JSON =
+      "application/vnd.git-lfs+json; charset=utf-8";
+  private static final String MESSAGE_LFS_NOT_CONFIGURED =
+      "{\"message\":\"No LFS plugin is configured to handle LFS requests.\"}";
+
   private List<Plugin> pending = new ArrayList<>();
   private final String pluginName;
-  private GuiceFilter filter;
+  private final FilterChain chain;
+  private AtomicReference<GuiceFilter> filter;
 
   @Inject
   LfsPluginServlet(@GerritServerConfig Config cfg) {
     this.pluginName = cfg.getString("lfs", null, "plugin");
+    this.chain = new FilterChain() {
+      @Override
+      public void doFilter(ServletRequest req, ServletResponse res)
+          throws IOException {
+        Resource.NOT_FOUND.send(
+            (HttpServletRequest) req, (HttpServletResponse) res);
+      }
+    };
+    this.filter = new AtomicReference<>();
   }
 
   @Override
   protected void service(HttpServletRequest req, HttpServletResponse res)
       throws ServletException, IOException {
-    if (filter == null) {
-      CacheHeaders.setNotCacheable(res);
-      res.sendError(SC_NOT_IMPLEMENTED);
+    if (filter.get() == null) {
+      responseLfsNotConfigured(res);
       return;
     }
-
-    FilterChain chain = new FilterChain() {
-      @Override
-      public void doFilter(ServletRequest req, ServletResponse res)
-          throws IOException {
-        Resource.NOT_FOUND.send((HttpServletRequest) req, (HttpServletResponse) res);
-      }
-    };
-    if (filter != null) {
-      filter.doFilter(req, res, chain);
-    } else {
-      chain.doFilter(req, res);
-    }
+    filter.get().doFilter(req, res, chain);
   }
 
   @Override
@@ -111,25 +118,37 @@
     install(newPlugin);
   }
 
+  private void responseLfsNotConfigured(HttpServletResponse res)
+      throws IOException {
+    CacheHeaders.setNotCacheable(res);
+    res.setContentType(CONTENTTYPE_VND_GIT_LFS_JSON);
+    res.setStatus(SC_NOT_IMPLEMENTED);
+    Writer w = new BufferedWriter(
+        new OutputStreamWriter(res.getOutputStream(), UTF_8));
+    w.write(MESSAGE_LFS_NOT_CONFIGURED);
+    w.flush();
+  }
+
   private void install(Plugin plugin) {
     if (!plugin.getName().equals(pluginName)) {
       return;
     }
-    filter = load(plugin);
+    final GuiceFilter guiceFilter = load(plugin);
     plugin.add(new RegistrationHandle() {
       @Override
       public void remove() {
-        filter = null;
+        filter.compareAndSet(guiceFilter, null);
       }
     });
+    filter.set(guiceFilter);
   }
 
   private GuiceFilter load(Plugin plugin) {
     if (plugin.getHttpInjector() != null) {
       final String name = plugin.getName();
-      final GuiceFilter filter;
+      final GuiceFilter guiceFilter;
       try {
-        filter = plugin.getHttpInjector().getInstance(GuiceFilter.class);
+        guiceFilter = plugin.getHttpInjector().getInstance(GuiceFilter.class);
       } catch (RuntimeException e) {
         log.warn(String.format("Plugin %s cannot load GuiceFilter", name), e);
         return null;
@@ -138,7 +157,7 @@
       try {
         ServletContext ctx =
             PluginServletContext.create(plugin, "/");
-        filter.init(new WrappedFilterConfig(ctx));
+        guiceFilter.init(new WrappedFilterConfig(ctx));
       } catch (ServletException e) {
         log.warn(String.format("Plugin %s failed to initialize HTTP", name), e);
         return null;
@@ -147,10 +166,10 @@
       plugin.add(new RegistrationHandle() {
         @Override
         public void remove() {
-          filter.destroy();
+          guiceFilter.destroy();
         }
       });
-      return filter;
+      return guiceFilter;
     }
     return null;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
index 0889c85..476dba8 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
@@ -70,7 +70,7 @@
             method.getParameterTypes());
       } catch (NoSuchMethodException e) {
         throw new NoSuchMethodError(String.format(
-            "%s does not implement%s",
+            "%s does not implement %s",
             PluginServletContext.class,
             method.toGenericString()));
       }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index e8efd72..bb3eff7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -90,6 +90,7 @@
   private final SiteStaticDirectoryServlet staticServlet;
   private final boolean isNoteDbEnabled;
   private final Integer pluginsLoadTimeout;
+  private final boolean canLoadInIFrame;
   private final GetDiffPreferences getDiff;
   private volatile Page page;
 
@@ -116,6 +117,7 @@
     staticServlet = ss;
     isNoteDbEnabled = migration.readChanges();
     pluginsLoadTimeout = getPluginsLoadTimeout(cfg);
+    canLoadInIFrame = cfg.getBoolean("gerrit", "canLoadInIFrame", false);
     getDiff = diffPref;
 
     String pageName = "HostPage.html";
@@ -322,6 +324,7 @@
       pageData.version = Version.getVersion();
       pageData.isNoteDbEnabled = isNoteDbEnabled;
       pageData.pluginsLoadTimeout = pluginsLoadTimeout;
+      pageData.canLoadInIFrame = canLoadInIFrame;
 
       StringWriter w = new StringWriter();
       w.write("var " + HPD_ID + "=");
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 05990e9..4f07ac2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -98,17 +98,24 @@
 
   private final Cache<Path, Resource> cache;
   private final boolean refresh;
+  private final boolean cacheOnClient;
   private final int cacheFileSizeLimitBytes;
 
   protected ResourceServlet(Cache<Path, Resource> cache, boolean refresh) {
-    this(cache, refresh, CACHE_FILE_SIZE_LIMIT_BYTES);
+    this(cache, refresh, true, CACHE_FILE_SIZE_LIMIT_BYTES);
+  }
+
+  protected ResourceServlet(Cache<Path, Resource> cache, boolean refresh,
+      boolean cacheOnClient) {
+    this(cache, refresh, cacheOnClient, CACHE_FILE_SIZE_LIMIT_BYTES);
   }
 
   @VisibleForTesting
   ResourceServlet(Cache<Path, Resource> cache, boolean refresh,
-      int cacheFileSizeLimitBytes) {
+      boolean cacheOnClient, int cacheFileSizeLimitBytes) {
     this.cache = checkNotNull(cache, "cache");
     this.refresh = refresh;
+    this.cacheOnClient = cacheOnClient;
     this.cacheFileSizeLimitBytes = cacheFileSizeLimitBytes;
   }
 
@@ -173,7 +180,7 @@
       CacheHeaders.setNotCacheable(rsp);
       rsp.setStatus(SC_NOT_FOUND);
       return;
-    } else if (r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
+    } else if (cacheOnClient && r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
       rsp.setStatus(SC_NOT_MODIFIED);
       return;
     }
@@ -186,6 +193,12 @@
         tosend = gz;
       }
     }
+
+    if (cacheOnClient) {
+      rsp.setHeader(ETAG, r.etag);
+    } else {
+      CacheHeaders.setNotCacheable(rsp);
+    }
     if (!CacheHeaders.hasCacheHeader(rsp)) {
       if (e != null && r.etag.equals(e)) {
         CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
@@ -193,7 +206,6 @@
         CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
       }
     }
-    rsp.setHeader(ETAG, r.etag);
     rsp.setContentType(r.contentType);
     rsp.setContentLength(tosend.length);
     try (OutputStream out = rsp.getOutputStream()) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java
index 111145f..64dd862 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java
@@ -29,6 +29,12 @@
     this.path = path;
   }
 
+  SingleFileServlet(Cache<Path, Resource> cache, Path path, boolean refresh,
+      boolean cacheOnClient) {
+    super(cache, refresh, cacheOnClient);
+    this.path = path;
+  }
+
   @Override
   protected Path getResourcePath(String pathInfo) {
     return path;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
index 78832e5..7916ed0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -237,9 +237,10 @@
     @Named(POLYGERRIT_INDEX_SERVLET)
     HttpServlet getPolyGerritUiIndexServlet(
         @Named(CACHE) Cache<Path, Resource> cache) {
-      return new SingleFileServlet(
-          cache, polyGerritBasePath().resolve("index.html"),
-          getPaths().isDev());
+      return new SingleFileServlet(cache,
+          polyGerritBasePath().resolve("index.html"),
+          getPaths().isDev(),
+          false);
     }
 
     @Provides
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index 4dd21ae..f80cc49 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -66,7 +66,7 @@
     CmdLineParser clp = parserFactory.create(param);
     try {
       clp.parseOptionMap(in);
-    } catch (CmdLineException e) {
+    } catch (CmdLineException | NumberFormatException e) {
       if (!clp.wasHelpRequestedByOption()) {
         replyError(req, res, SC_BAD_REQUEST, e.getMessage(), e);
         return false;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index bcd936e..943d824 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -147,6 +147,10 @@
   private static final String JSON_TYPE = "application/json";
   private static final String FORM_TYPE = "application/x-www-form-urlencoded";
 
+  // HTTP 422 Unprocessable Entity.
+  // TODO: Remove when HttpServletResponse.SC_UNPROCESSABLE_ENTITY is available
+  private static final int SC_UNPROCESSABLE_ENTITY = 422;
+
   private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks.
 
   /**
@@ -326,8 +330,7 @@
         return;
       }
 
-      if (viewData.view instanceof RestReadView<?>
-          && "GET".equals(req.getMethod())) {
+      if (viewData.view instanceof RestReadView<?> && isGetOrHead(req)) {
         result = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
       } else if (viewData.view instanceof RestModifyView<?, ?>) {
         @SuppressWarnings("unchecked")
@@ -395,7 +398,7 @@
       responseBytes = replyError(req, res, status = SC_PRECONDITION_FAILED,
           messageOr(e, "Precondition Failed"), e.caching(), e);
     } catch (UnprocessableEntityException e) {
-      responseBytes = replyError(req, res, status = 422,
+      responseBytes = replyError(req, res, status = SC_UNPROCESSABLE_ENTITY,
           messageOr(e, "Unprocessable Entity"), e.caching(), e);
     } catch (NotImplementedException e) {
       responseBytes = replyError(req, res, status = SC_NOT_IMPLEMENTED,
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
index c0fb86b..bda2d91 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
@@ -14,11 +14,8 @@
 
 package com.google.gerrit.httpd.rpc;
 
-import com.google.common.collect.Lists;
-import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.SshHostKey;
 import com.google.gerrit.common.data.SystemInfoService;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
@@ -32,7 +29,6 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 
 import javax.servlet.http.HttpServletRequest;
@@ -45,28 +41,12 @@
 
   private final List<HostKey> hostKeys;
   private final Provider<HttpServletRequest> httpRequest;
-  private final ProjectCache projectCache;
 
   @Inject
   SystemInfoServiceImpl(SshInfo daemon,
-      Provider<HttpServletRequest> hsr,
-      ProjectCache pc) {
+      Provider<HttpServletRequest> hsr) {
     hostKeys = daemon.getHostKeys();
     httpRequest = hsr;
-    projectCache = pc;
-  }
-
-  @Override
-  public void contributorAgreements(
-      final AsyncCallback<List<ContributorAgreement>> callback) {
-    Collection<ContributorAgreement> agreements =
-        projectCache.getAllProjects().getConfig().getContributorAgreements();
-    List<ContributorAgreement> cas =
-        Lists.newArrayListWithCapacity(agreements.size());
-    for (ContributorAgreement ca : agreements) {
-      cas.add(ca.forUi());
-    }
-    callback.onSuccess(cas);
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
index 62778eb..d32fdaf 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
@@ -28,12 +28,10 @@
     install(new FactoryModule() {
       @Override
       protected void configure() {
-        factory(AgreementInfoFactory.Factory.class);
         factory(DeleteExternalIds.Factory.class);
         factory(ExternalIdDetailFactory.Factory.class);
       }
     });
     rpc(AccountSecurityImpl.class);
-    rpc(AccountServiceImpl.class);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index 8fcf9ea..3d05548 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -14,74 +14,31 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
-import com.google.common.base.Strings;
-import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.data.AccountSecurity;
-import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountByEmailCache;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.extensions.events.AgreementSignup;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.io.IOException;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
 class AccountSecurityImpl extends BaseServiceImplementation implements
     AccountSecurity {
-  private final Realm realm;
-  private final ProjectCache projectCache;
-  private final Provider<IdentifiedUser> user;
-  private final AccountByEmailCache byEmailCache;
-  private final AccountCache accountCache;
-
   private final DeleteExternalIds.Factory deleteExternalIdsFactory;
   private final ExternalIdDetailFactory.Factory externalIdDetailFactory;
 
-  private final GroupCache groupCache;
-  private final AuditService auditService;
-  private final AgreementSignup agreementSignup;
-
   @Inject
   AccountSecurityImpl(final Provider<ReviewDb> schema,
       final Provider<CurrentUser> currentUser,
-      final Realm r, final Provider<IdentifiedUser> u,
-      final ProjectCache pc,
-      final AccountByEmailCache abec, final AccountCache uac,
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
-      final ExternalIdDetailFactory.Factory externalIdDetailFactory,
-      final GroupCache groupCache,
-      final AuditService auditService,
-      AgreementSignup agreementSignup) {
+      final ExternalIdDetailFactory.Factory externalIdDetailFactory) {
     super(schema, currentUser);
-    realm = r;
-    user = u;
-    projectCache = pc;
-    byEmailCache = abec;
-    accountCache = uac;
-    this.auditService = auditService;
     this.deleteExternalIdsFactory = deleteExternalIdsFactory;
     this.externalIdDetailFactory = externalIdDetailFactory;
-    this.groupCache = groupCache;
-    this.agreementSignup = agreementSignup;
   }
 
   @Override
@@ -94,84 +51,4 @@
       final AsyncCallback<Set<AccountExternalId.Key>> callback) {
     deleteExternalIdsFactory.create(keys).to(callback);
   }
-
-  @Override
-  public void updateContact(final String name, final String emailAddr,
-      final AsyncCallback<Account> callback) {
-    run(callback, new Action<Account>() {
-      @Override
-      public Account run(ReviewDb db)
-          throws OrmException, Failure, IOException {
-        IdentifiedUser self = user.get();
-        final Account me = db.accounts().get(self.getAccountId());
-        final String oldEmail = me.getPreferredEmail();
-        if (realm.allowsEdit(Account.FieldName.FULL_NAME)) {
-          me.setFullName(Strings.emptyToNull(name));
-        }
-        if (!Strings.isNullOrEmpty(emailAddr)
-            && !self.hasEmailAddress(emailAddr)) {
-          throw new Failure(new PermissionDeniedException("Email address must be verified"));
-        }
-        me.setPreferredEmail(Strings.emptyToNull(emailAddr));
-        db.accounts().update(Collections.singleton(me));
-        if (!eq(oldEmail, me.getPreferredEmail())) {
-          byEmailCache.evict(oldEmail);
-          byEmailCache.evict(me.getPreferredEmail());
-        }
-        accountCache.evict(me.getId());
-        return me;
-      }
-    });
-  }
-
-  private static boolean eq(final String a, final String b) {
-    if (a == null && b == null) {
-      return true;
-    }
-    return a != null && a.equals(b);
-  }
-
-  @Override
-  public void enterAgreement(final String agreementName,
-      final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      @Override
-      public VoidResult run(final ReviewDb db)
-          throws OrmException, Failure, IOException {
-        ContributorAgreement ca = projectCache.getAllProjects().getConfig()
-            .getContributorAgreement(agreementName);
-        if (ca == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        if (ca.getAutoVerify() == null) {
-          throw new Failure(new IllegalStateException(
-              "cannot enter a non-autoVerify agreement"));
-        } else if (ca.getAutoVerify().getUUID() == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        AccountGroup group = groupCache.get(ca.getAutoVerify().getUUID());
-        if (group == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        Account account = user.get().getAccount();
-        agreementSignup.fire(account, ca.getName());
-
-        final AccountGroupMember.Key key =
-            new AccountGroupMember.Key(account.getId(), group.getId());
-        AccountGroupMember m = db.accountGroupMembers().get(key);
-        if (m == null) {
-          m = new AccountGroupMember(key);
-          auditService.dispatchAddAccountsToGroup(account.getId(), Collections
-              .singleton(m));
-          db.accountGroupMembers().insert(Collections.singleton(m));
-          accountCache.evict(m.getAccountId());
-        }
-
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
deleted file mode 100644
index 8fba47d..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2008 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.httpd.rpc.account;
-
-import com.google.gerrit.common.data.AccountService;
-import com.google.gerrit.common.data.AgreementInfo;
-import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-class AccountServiceImpl extends BaseServiceImplementation implements
-    AccountService {
-  private final AgreementInfoFactory.Factory agreementInfoFactory;
-
-  @Inject
-  AccountServiceImpl(final Provider<ReviewDb> schema,
-      final Provider<IdentifiedUser> identifiedUser,
-      final AgreementInfoFactory.Factory agreementInfoFactory) {
-    super(schema, identifiedUser);
-    this.agreementInfoFactory = agreementInfoFactory;
-  }
-
-  @Override
-  public void myAgreements(final AsyncCallback<AgreementInfo> callback) {
-    agreementInfoFactory.create().to(callback);
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
deleted file mode 100644
index 91afd97..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (C) 2009 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.httpd.rpc.account;
-
-import com.google.gerrit.common.data.AgreementInfo;
-import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.data.PermissionRule.Action;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-class AgreementInfoFactory extends Handler<AgreementInfo> {
-  private static final Logger log = LoggerFactory.getLogger(AgreementInfoFactory.class);
-
-  interface Factory {
-    AgreementInfoFactory create();
-  }
-
-  private final IdentifiedUser user;
-  private final ProjectCache projectCache;
-
-  private AgreementInfo info;
-
-  @Inject
-  AgreementInfoFactory(final IdentifiedUser user,
-      final ProjectCache projectCache) {
-    this.user = user;
-    this.projectCache = projectCache;
-  }
-
-  @Override
-  public AgreementInfo call() throws Exception {
-    List<String> accepted = new ArrayList<>();
-    Map<String, ContributorAgreement> agreements = new HashMap<>();
-    Collection<ContributorAgreement> cas =
-        projectCache.getAllProjects().getConfig().getContributorAgreements();
-    for (ContributorAgreement ca : cas) {
-      agreements.put(ca.getName(), ca.forUi());
-
-      List<AccountGroup.UUID> groupIds = new ArrayList<>();
-      for (PermissionRule rule : ca.getAccepted()) {
-        if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)) {
-          if (rule.getGroup().getUUID() == null) {
-            log.warn("group \"" + rule.getGroup().getName() + "\" does not " +
-                " exist, referenced in CLA \"" + ca.getName() + "\"");
-          } else {
-            groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
-          }
-        }
-      }
-      if (user.getEffectiveGroups().containsAnyOf(groupIds)) {
-        accepted.add(ca.getName());
-      }
-    }
-
-    info = new AgreementInfo();
-    info.setAccepted(accepted);
-    info.setAgreements(agreements);
-    return info;
-  }
-}
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/ResourceServletTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/ResourceServletTest.java
index 3eb3088..e8c835d 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/ResourceServletTest.java
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/ResourceServletTest.java
@@ -64,8 +64,20 @@
     }
 
     private Servlet(FileSystem fs, Cache<Path, Resource> cache,
+        boolean refresh, boolean cacheOnClient) {
+      super(cache, refresh, cacheOnClient);
+      this.fs = fs;
+    }
+
+    private Servlet(FileSystem fs, Cache<Path, Resource> cache,
         boolean refresh, int cacheFileSizeLimitBytes) {
-      super(cache, refresh, cacheFileSizeLimitBytes);
+      super(cache, refresh, true, cacheFileSizeLimitBytes);
+      this.fs = fs;
+    }
+
+    private Servlet(FileSystem fs, Cache<Path, Resource> cache,
+        boolean refresh, boolean cacheOnClient, int cacheFileSizeLimitBytes) {
+      super(cache, refresh, cacheOnClient, cacheFileSizeLimitBytes);
       this.fs = fs;
     }
 
@@ -156,6 +168,37 @@
   }
 
   @Test
+  public void smallFileWithoutClientCache() throws Exception {
+    Cache<Path, Resource> cache = newCache(1);
+    Servlet servlet = new Servlet(fs, cache, false, false);
+
+    writeFile("/foo", "foo1");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.doGet(request("/foo"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_OK);
+    assertThat(res.getActualBodyString()).isEqualTo("foo1");
+    assertNotCacheable(res);
+
+    // Miss on getIfPresent, miss on get.
+    assertCacheHits(cache, 0, 2);
+
+    res = new FakeHttpServletResponse();
+    servlet.doGet(request("/foo"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_OK);
+    assertThat(res.getActualBodyString()).isEqualTo("foo1");
+    assertNotCacheable(res);
+    assertCacheHits(cache, 1, 2);
+
+    writeFile("/foo", "foo2");
+    res = new FakeHttpServletResponse();
+    servlet.doGet(request("/foo"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_OK);
+    assertThat(res.getActualBodyString()).isEqualTo("foo1");
+    assertNotCacheable(res);
+    assertCacheHits(cache, 2, 2);
+  }
+
+  @Test
   public void smallFileWithoutRefresh() throws Exception {
     Cache<Path, Resource> cache = newCache(1);
     Servlet servlet = new Servlet(fs, cache, false);
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index b40d46b..eb0dfaa 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -19,7 +19,11 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.AbstractFuture;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.FieldDef;
@@ -54,8 +58,10 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -84,10 +90,12 @@
   private final SitePaths sitePaths;
   private final Directory dir;
   private final String name;
+  private final ListeningExecutorService writerThread;
   private final TrackingIndexWriter writer;
   private final ReferenceManager<IndexSearcher> searcherManager;
   private final ControlledRealTimeReopenThread<IndexSearcher> reopenThread;
   private final Set<NrtFuture> notDoneNrtFutures;
+  private ScheduledThreadPoolExecutor autoCommitExecutor;
 
   AbstractLuceneIndex(
       Schema<V> schema,
@@ -115,11 +123,11 @@
           new AutoCommitWriter(dir, writerConfig.getLuceneConfig());
       delegateWriter = autoCommitWriter;
 
-      new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder()
-          .setNameFormat("Commit-%d " + index)
+      autoCommitExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder()
+          .setNameFormat(index + " Commit-%d")
           .setDaemon(true)
-          .build())
-          .scheduleAtFixedRate(new Runnable() {
+          .build());
+      autoCommitExecutor.scheduleAtFixedRate(new Runnable() {
             @Override
             public void run() {
               try {
@@ -147,11 +155,18 @@
 
     notDoneNrtFutures = Sets.newConcurrentHashSet();
 
+    writerThread = MoreExecutors.listeningDecorator(
+        Executors.newFixedThreadPool(1,
+            new ThreadFactoryBuilder()
+              .setNameFormat(index + " Write-%d")
+              .setDaemon(true)
+              .build()));
+
     reopenThread = new ControlledRealTimeReopenThread<>(
         writer, searcherManager,
         0.500 /* maximum stale age (seconds) */,
         0.010 /* minimum stale age (seconds) */);
-    reopenThread.setName("NRT " + name);
+    reopenThread.setName(index + " NRT");
     reopenThread.setPriority(Math.min(
         Thread.currentThread().getPriority() + 2,
         Thread.MAX_PRIORITY));
@@ -188,6 +203,19 @@
 
   @Override
   public void close() {
+    if (autoCommitExecutor != null) {
+      autoCommitExecutor.shutdown();
+    }
+
+    writerThread.shutdown();
+    try {
+      if (!writerThread.awaitTermination(5, TimeUnit.SECONDS)) {
+        log.warn("shutting down " + name + " index with pending Lucene writes");
+      }
+    } catch (InterruptedException e) {
+      log.warn("interrupted waiting for pending Lucene writes of " + name +
+          " index", e);
+    }
     reopenThread.close();
 
     // Closing the reopen thread sets its generation to Long.MAX_VALUE, but we
@@ -217,16 +245,45 @@
     }
   }
 
-  ListenableFuture<?> insert(Document doc) throws IOException {
-    return new NrtFuture(writer.addDocument(doc));
+  ListenableFuture<?> insert(final Document doc) {
+    return submit(new Callable<Long>() {
+      @Override
+      public Long call() throws IOException, InterruptedException {
+        return writer.addDocument(doc);
+      }
+    });
   }
 
-  ListenableFuture<?> replace(Term term, Document doc) throws IOException {
-    return new NrtFuture(writer.updateDocument(term, doc));
+  ListenableFuture<?> replace(final Term term, final Document doc) {
+    return submit(new Callable<Long>() {
+      @Override
+      public Long call() throws IOException, InterruptedException {
+        return writer.updateDocument(term, doc);
+      }
+    });
   }
 
-  ListenableFuture<?> delete(Term term) throws IOException {
-    return new NrtFuture(writer.deleteDocuments(term));
+  ListenableFuture<?> delete(final Term term) {
+    return submit(new Callable<Long>() {
+      @Override
+      public Long call() throws IOException, InterruptedException {
+        return writer.deleteDocuments(term);
+      }
+    });
+  }
+
+  private ListenableFuture<?> submit(Callable<Long> task) {
+    ListenableFuture<Long> future =
+        Futures.nonCancellationPropagating(writerThread.submit(task));
+    return Futures.transformAsync(future, new AsyncFunction<Long, Void>() {
+      @Override
+      public ListenableFuture<Void> apply(Long gen) throws InterruptedException {
+        // Tell the reopen thread a future is waiting on this
+        // generation so it uses the min stale time when refreshing.
+        reopenThread.waitForGeneration(gen, 0);
+        return new NrtFuture(gen);
+      }
+    });
   }
 
   @Override
@@ -300,9 +357,6 @@
 
     NrtFuture(long gen) {
       this.gen = gen;
-      // Tell the reopen thread we are waiting on this generation so it uses the
-      // min stale time when refreshing.
-      isGenAvailableNowForCurrentSearcher();
     }
 
     @Override
@@ -318,12 +372,10 @@
     public Void get(long timeout, TimeUnit unit) throws InterruptedException,
         TimeoutException, ExecutionException {
       if (!isDone()) {
-        if (reopenThread.waitForGeneration(gen,
-            (int) MILLISECONDS.convert(timeout, unit))) {
-          set(null);
-        } else {
+        if (!reopenThread.waitForGeneration(gen, (int) unit.toMillis(timeout))) {
           throw new TimeoutException();
         }
+        set(null);
       }
       return super.get(timeout, unit);
     }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 4747455..c137d1e 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -25,10 +25,11 @@
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
 
 import com.google.common.base.Function;
+import com.google.common.base.Throwables;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Lists;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Futures;
@@ -58,6 +59,7 @@
 import com.google.gerrit.server.query.change.ChangeDataSource;
 import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -85,11 +87,14 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -240,7 +245,7 @@
   public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
       throws QueryParseException {
     Set<Change.Status> statuses = ChangeIndexRewriter.getPossibleStatus(p);
-    List<ChangeSubIndex> indexes = Lists.newArrayListWithCapacity(2);
+    List<ChangeSubIndex> indexes = new ArrayList<>(2);
     if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
       indexes.add(openIndex);
     }
@@ -298,6 +303,22 @@
 
     @Override
     public ResultSet<ChangeData> read() throws OrmException {
+      if (Thread.interrupted()) {
+        Thread.currentThread().interrupt();
+        throw new OrmException("interrupted");
+      }
+
+      final Set<String> fields = fields(opts);
+      return new ChangeDataResults(
+          executor.submit(new Callable<List<Document>>() {
+            @Override
+            public List<Document> call() throws IOException {
+              return doRead(fields);
+            }
+          }), fields);
+    }
+
+    private List<Document> doRead(Set<String> fields) throws IOException {
       IndexSearcher[] searchers = new IndexSearcher[indexes.size()];
       try {
         int realLimit = opts.start() + opts.limit();
@@ -308,35 +329,12 @@
         }
         TopDocs docs = TopDocs.merge(sort, realLimit, hits);
 
-        List<ChangeData> result =
-            Lists.newArrayListWithCapacity(docs.scoreDocs.length);
-        Set<String> fields = fields(opts);
-        String idFieldName = LEGACY_ID.getName();
+        List<Document> result = new ArrayList<>(docs.scoreDocs.length);
         for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
           ScoreDoc sd = docs.scoreDocs[i];
-          Document doc = searchers[sd.shardIndex].doc(sd.doc, fields);
-          result.add(toChangeData(doc, fields, idFieldName));
+          result.add(searchers[sd.shardIndex].doc(sd.doc, fields));
         }
-
-        final List<ChangeData> r = Collections.unmodifiableList(result);
-        return new ResultSet<ChangeData>() {
-          @Override
-          public Iterator<ChangeData> iterator() {
-            return r.iterator();
-          }
-
-          @Override
-          public List<ChangeData> toList() {
-            return r;
-          }
-
-          @Override
-          public void close() {
-            // Do nothing.
-          }
-        };
-      } catch (IOException e) {
-        throw new OrmException(e);
+        return result;
       } finally {
         for (int i = 0; i < indexes.size(); i++) {
           if (searchers[i] != null) {
@@ -351,6 +349,45 @@
     }
   }
 
+  private class ChangeDataResults implements ResultSet<ChangeData> {
+    private final Future<List<Document>> future;
+    private final Set<String> fields;
+
+    ChangeDataResults(Future<List<Document>> future, Set<String> fields) {
+      this.future = future;
+      this.fields = fields;
+    }
+
+    @Override
+    public Iterator<ChangeData> iterator() {
+      return toList().iterator();
+    }
+
+    @Override
+    public List<ChangeData> toList() {
+      try {
+        List<Document> docs = future.get();
+        List<ChangeData> result = new ArrayList<>(docs.size());
+        String idFieldName = LEGACY_ID.getName();
+        for (Document doc : docs) {
+          result.add(toChangeData(fields(doc, fields), fields, idFieldName));
+        }
+        return result;
+      } catch (InterruptedException e) {
+        close();
+        throw new OrmRuntimeException(e);
+      } catch (ExecutionException e) {
+        Throwables.throwIfUnchecked(e.getCause());
+        throw new OrmRuntimeException(e.getCause());
+      }
+    }
+
+    @Override
+    public void close() {
+      future.cancel(false /* do not interrupt Lucene */);
+    }
+  }
+
   private Set<String> fields(QueryOptions opts) {
     // Ensure we request enough fields to construct a ChangeData.
     Set<String> fs = opts.fields();
@@ -376,19 +413,33 @@
         ImmutableSet.of(LEGACY_ID.getName(), PROJECT.getName()));
   }
 
-  private ChangeData toChangeData(Document doc, Set<String> fields,
-      String idFieldName) {
+  private static Multimap<String, IndexableField> fields(Document doc,
+      Set<String> fields) {
+    Multimap<String, IndexableField> stored =
+        ArrayListMultimap.create(fields.size(), 4);
+    for (IndexableField f : doc) {
+      String name = f.name();
+      if (fields.contains(name)) {
+        stored.put(name, f);
+      }
+    }
+    return stored;
+  }
+
+  private ChangeData toChangeData(Multimap<String, IndexableField> doc,
+      Set<String> fields, String idFieldName) {
     ChangeData cd;
     // Either change or the ID field was guaranteed to be included in the call
     // to fields() above.
-    BytesRef cb = doc.getBinaryValue(CHANGE_FIELD);
+    IndexableField cb = Iterables.getFirst(doc.get(CHANGE_FIELD), null);
     if (cb != null) {
+      BytesRef proto = cb.binaryValue();
       cd = changeDataFactory.create(db.get(),
-          ChangeProtoField.CODEC.decode(cb.bytes, cb.offset, cb.length));
+          ChangeProtoField.CODEC.decode(proto.bytes, proto.offset, proto.length));
     } else {
-      Change.Id id =
-          new Change.Id(doc.getField(idFieldName).numericValue().intValue());
-      IndexableField project = doc.getField(PROJECT.getName());
+      IndexableField f = Iterables.getFirst(doc.get(idFieldName), null);
+      Change.Id id = new Change.Id(f.numericValue().intValue());
+      IndexableField project = Iterables.getFirst(doc.get(PROJECT.getName()), null);
       if (project == null) {
         // Old schema without project field: we can safely assume NoteDb is
         // disabled.
@@ -429,7 +480,7 @@
     return cd;
   }
 
-  private void decodePatchSets(Document doc, ChangeData cd) {
+  private void decodePatchSets(Multimap<String, IndexableField> doc, ChangeData cd) {
     List<PatchSet> patchSets =
         decodeProtos(doc, PATCH_SET_FIELD, PatchSetProtoField.CODEC);
     if (!patchSets.isEmpty()) {
@@ -439,14 +490,14 @@
     }
   }
 
-  private void decodeApprovals(Document doc, ChangeData cd) {
+  private void decodeApprovals(Multimap<String, IndexableField> doc, ChangeData cd) {
     cd.setCurrentApprovals(
         decodeProtos(doc, APPROVAL_FIELD, PatchSetApprovalProtoField.CODEC));
   }
 
-  private void decodeChangedLines(Document doc, ChangeData cd) {
-    IndexableField added = doc.getField(ADDED_FIELD);
-    IndexableField deleted = doc.getField(DELETED_FIELD);
+  private void decodeChangedLines(Multimap<String, IndexableField> doc, ChangeData cd) {
+    IndexableField added = Iterables.getFirst(doc.get(ADDED_FIELD), null);
+    IndexableField deleted = Iterables.getFirst(doc.get(DELETED_FIELD), null);
     if (added != null && deleted != null) {
       cd.setChangedLines(
           added.numericValue().intValue(),
@@ -460,23 +511,26 @@
     }
   }
 
-  private void decodeMergeable(Document doc, ChangeData cd) {
-    String mergeable = doc.get(MERGEABLE_FIELD);
-    if ("1".equals(mergeable)) {
-      cd.setMergeable(true);
-    } else if ("0".equals(mergeable)) {
-      cd.setMergeable(false);
+  private void decodeMergeable(Multimap<String, IndexableField> doc, ChangeData cd) {
+    IndexableField f = Iterables.getFirst(doc.get(MERGEABLE_FIELD), null);
+    if (f != null) {
+      String mergeable = f.stringValue();
+      if ("1".equals(mergeable)) {
+        cd.setMergeable(true);
+      } else if ("0".equals(mergeable)) {
+        cd.setMergeable(false);
+      }
     }
   }
 
-  private void decodeReviewedBy(Document doc, ChangeData cd) {
-    IndexableField[] reviewedBy = doc.getFields(REVIEWEDBY_FIELD);
-    if (reviewedBy.length > 0) {
+  private void decodeReviewedBy(Multimap<String, IndexableField> doc, ChangeData cd) {
+    Collection<IndexableField> reviewedBy = doc.get(REVIEWEDBY_FIELD);
+    if (reviewedBy.size() > 0) {
       Set<Account.Id> accounts =
-          Sets.newHashSetWithExpectedSize(reviewedBy.length);
+          Sets.newHashSetWithExpectedSize(reviewedBy.size());
       for (IndexableField r : reviewedBy) {
         int id = r.numericValue().intValue();
-        if (reviewedBy.length == 1 && id == ChangeField.NOT_REVIEWED) {
+        if (reviewedBy.size() == 1 && id == ChangeField.NOT_REVIEWED) {
           break;
         }
         accounts.add(new Account.Id(id));
@@ -485,9 +539,9 @@
     }
   }
 
-  private void decodeHashtags(Document doc, ChangeData cd) {
-    IndexableField[] hashtag = doc.getFields(HASHTAG_FIELD);
-    Set<String> hashtags = Sets.newHashSetWithExpectedSize(hashtag.length);
+  private void decodeHashtags(Multimap<String, IndexableField> doc, ChangeData cd) {
+    Collection<IndexableField> hashtag = doc.get(HASHTAG_FIELD);
+    Set<String> hashtags = Sets.newHashSetWithExpectedSize(hashtag.size());
     for (IndexableField r : hashtag) {
       hashtags.add(r.binaryValue().utf8ToString());
     }
@@ -495,18 +549,18 @@
   }
 
   @Deprecated
-  private void decodeStarredBy(Document doc, ChangeData cd) {
-    IndexableField[] starredBy = doc.getFields(STARREDBY_FIELD);
+  private void decodeStarredBy(Multimap<String, IndexableField> doc, ChangeData cd) {
+    Collection<IndexableField> starredBy = doc.get(STARREDBY_FIELD);
     Set<Account.Id> accounts =
-        Sets.newHashSetWithExpectedSize(starredBy.length);
+        Sets.newHashSetWithExpectedSize(starredBy.size());
     for (IndexableField r : starredBy) {
       accounts.add(new Account.Id(r.numericValue().intValue()));
     }
     cd.setStarredBy(accounts);
   }
 
-  private void decodeStar(Document doc, ChangeData cd) {
-    IndexableField[] star = doc.getFields(STAR_FIELD);
+  private void decodeStar(Multimap<String, IndexableField> doc, ChangeData cd) {
+    Collection<IndexableField> star = doc.get(STAR_FIELD);
     Multimap<Account.Id, String> stars = ArrayListMultimap.create();
     for (IndexableField r : star) {
       StarredChangesUtil.StarField starField =
@@ -518,10 +572,10 @@
     cd.setStars(stars);
   }
 
-  private void decodeReviewers(Document doc, ChangeData cd) {
+  private void decodeReviewers(Multimap<String, IndexableField> doc, ChangeData cd) {
     cd.setReviewers(
         ChangeField.parseReviewerFieldValues(
-            FluentIterable.of(doc.getFields(REVIEWER_FIELD))
+            FluentIterable.from(doc.get(REVIEWER_FIELD))
                 .transform(
                     new Function<IndexableField, String>() {
                       @Override
@@ -531,14 +585,16 @@
                     })));
   }
 
-  private static <T> List<T> decodeProtos(Document doc, String fieldName,
-      ProtobufCodec<T> codec) {
-    BytesRef[] bytesRefs = doc.getBinaryValues(fieldName);
-    if (bytesRefs.length == 0) {
+  private static <T> List<T> decodeProtos(Multimap<String, IndexableField> doc,
+      String fieldName, ProtobufCodec<T> codec) {
+    Collection<IndexableField> fields = doc.get(fieldName);
+    if (fields.isEmpty()) {
       return Collections.emptyList();
     }
-    List<T> result = new ArrayList<>(bytesRefs.length);
-    for (BytesRef r : bytesRefs) {
+
+    List<T> result = new ArrayList<>(fields.size());
+    for (IndexableField f : fields) {
+      BytesRef r = f.binaryValue();
       result.add(codec.decode(r.bytes, r.offset, r.length));
     }
     return result;
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index 0e8a8b4..f5d5146 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.lucene;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -39,6 +40,7 @@
 
 import java.util.Collection;
 import java.util.Map;
+import java.util.Set;
 
 public class LuceneIndexModule extends LifecycleModule {
   private static final String SINGLE_VERSIONS =
@@ -115,15 +117,20 @@
 
   @Singleton
   static class SingleVersionListener implements LifecycleListener {
+    private final Set<String> disabled;
     private final Collection<IndexDefinition<?, ?, ?>> defs;
     private final Map<String, Integer> singleVersions;
 
     @Inject
     SingleVersionListener(
+        @GerritServerConfig Config cfg,
         Collection<IndexDefinition<?, ?, ?>> defs,
         @Named(SINGLE_VERSIONS) Map<String, Integer> singleVersions) {
       this.defs = defs;
       this.singleVersions = singleVersions;
+
+      disabled = ImmutableSet.copyOf(
+          cfg.getStringList("index", null, "testDisable"));
     }
 
     @Override
@@ -135,6 +142,9 @@
 
     private <K, V, I extends Index<K, V>> void start(
         IndexDefinition<K, V, I> def) {
+      if (disabled.contains(def.getName())) {
+        return;
+      }
       Schema<V> schema;
       Integer v = singleVersions.get(def.getName());
       if (v == null) {
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index 94408c6..b46f1f6 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -163,11 +163,11 @@
     markNotReady(cfg, def.getName(), versions.values(), write);
 
     int latest = write.get(0).version;
-    if (onlineUpgrade && latest != search.version) {
-      OnlineReindexer<K, V, I> reindexer = new OnlineReindexer<>(def, latest);
-      synchronized (this) {
-        if (!reindexers.containsKey(def.getName())) {
-          reindexers.put(def.getName(), reindexer);
+    OnlineReindexer<K, V, I> reindexer = new OnlineReindexer<>(def, latest);
+    synchronized (this) {
+      if (!reindexers.containsKey(def.getName())) {
+        reindexers.put(def.getName(), reindexer);
+        if (onlineUpgrade && latest != search.version) {
           reindexer.start();
         }
       }
@@ -177,14 +177,15 @@
   /**
    * Start the online reindexer if the current index is not already the latest.
    *
+   * @param  force start re-index
    * @return true if started, otherwise false.
    * @throws ReindexerAlreadyRunningException
    */
-  public synchronized boolean startReindexer(String name)
+  public synchronized boolean startReindexer(String name, boolean force)
       throws ReindexerAlreadyRunningException {
     OnlineReindexer<?, ?, ?> reindexer = reindexers.get(name);
     validateReindexerNotRunning(reindexer);
-    if (!isCurrentIndexVersionLatest(name, reindexer)) {
+    if (force || !isCurrentIndexVersionLatest(name, reindexer)) {
       reindexer.start();
       return true;
     }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
index ccf41e0..a993b49 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -50,7 +50,7 @@
 public class QueryBuilder<V> {
   static Term intTerm(String name, int value) {
     BytesRefBuilder builder = new BytesRefBuilder();
-    NumericUtils.intToPrefixCodedBytes(value, 0, builder);
+    NumericUtils.intToPrefixCoded(value, 0, builder);
     return new Term(name, builder.get());
   }
 
@@ -241,7 +241,12 @@
       throw new QueryParseException(
           "Full-text search over empty string not supported");
     }
-    return queryBuilder.createPhraseQuery(p.getField().getName(), value);
+    Query query = queryBuilder.createPhraseQuery(p.getField().getName(), value);
+    if (query == null) {
+      throw new QueryParseException(
+          "Cannot create full-text query with value: " + value);
+    }
+    return query;
   }
 
   public int toIndexTimeInMinutes(Date ts) {
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index 3a40252..791f9fd 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -24,12 +24,12 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.httpd.template.SiteHeaderFooter;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
diff --git a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
index 5fd582b..63bb842 100644
--- a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
+++ b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
@@ -57,9 +57,11 @@
 
     _socket_ = sslFactory(verify).createSocket(_socket_, hostname, port, true);
 
-    SSLParameters sslParams = new SSLParameters();
-    sslParams.setEndpointIdentificationAlgorithm("HTTPS");
-    ((SSLSocket)_socket_).setSSLParameters(sslParams);
+    if (verify) {
+      SSLParameters sslParams = new SSLParameters();
+      sslParams.setEndpointIdentificationAlgorithm("HTTPS");
+      ((SSLSocket)_socket_).setSSLParameters(sslParams);
+    }
 
     // XXX: Can't call _connectAction_() because SMTP server doesn't
     // give banner information again after STARTTLS, thus SMTP._connectAction_()
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index e62079f..4be941c 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -16,6 +16,7 @@
   '//lib/guice:guice-assistedinject',
   '//lib/guice:guice-servlet',
   '//lib/jgit/org.eclipse.jgit:jgit',
+  '//lib/joda:joda-time',
   '//lib/log:api',
   '//lib/log:log4j',
 ]
diff --git a/gerrit-pgm/BUILD b/gerrit-pgm/BUILD
index 59b371a..ec86c9a 100644
--- a/gerrit-pgm/BUILD
+++ b/gerrit-pgm/BUILD
@@ -19,6 +19,7 @@
   '//lib/guice:guice-assistedinject',
   '//lib/guice:guice-servlet',
   '//lib/jgit/org.eclipse.jgit:jgit',
+  '//lib/joda:joda-time',
   '//lib/log:api',
   '//lib/log:log4j',
 ]
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 4715877..d98f999 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -19,10 +19,8 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
-import com.google.gerrit.common.ChangeHookApiListener;
-import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.common.EventBroker;
-import com.google.gerrit.common.StreamEventsApiListener;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.GerritOptions;
@@ -49,7 +47,6 @@
 import com.google.gerrit.pgm.util.LogFileCompressor;
 import com.google.gerrit.pgm.util.RuntimeShutdown;
 import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.change.ChangeCleanupRunner;
@@ -61,6 +58,7 @@
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.RestCacheAdminModule;
+import com.google.gerrit.server.events.StreamEventsApiListener;
 import com.google.gerrit.server.git.GarbageCollectionModule;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
@@ -343,9 +341,7 @@
     modules.add(createIndexModule());
 
     modules.add(new WorkQueue.Module());
-    modules.add(new ChangeHookApiListener.Module());
     modules.add(new StreamEventsApiListener.Module());
-    modules.add(new ChangeHookRunner.Module());
     modules.add(new EventBroker.Module());
     modules.add(test
         ? new H2AccountPatchReviewStore.InMemoryModule()
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 9d27170..f5212ab 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -18,9 +18,9 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
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 2de71cc..136ec5a 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
@@ -18,6 +18,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.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
@@ -27,7 +28,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
index 6b30f80..a6471c7 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -16,11 +16,11 @@
 
 import static com.google.gerrit.pgm.init.api.InitUtil.dnOf;
 
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.Section;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
index 77c466e..018211b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -55,17 +55,22 @@
 
   @Override
   public void run() throws IOException {
-    ui.header("Index");
-
-    IndexType type = index.select("Type", "type", IndexType.LUCENE);
-    for (SchemaDefinitions<?> def : IndexModule.ALL_SCHEMA_DEFS) {
-      AbstractLuceneIndex.setReady(
-          site, def.getName(), def.getLatest().getVersion(), true);
+    IndexType type = IndexType.LUCENE;
+    if (IndexType.values().length > 1) {
+      ui.header("Index");
+      type = index.select("Type", "type", type);
     }
+
     if ((site.isNew || isEmptySite()) && type == IndexType.LUCENE) {
-      // Do nothing
+      for (SchemaDefinitions<?> def : IndexModule.ALL_SCHEMA_DEFS) {
+        AbstractLuceneIndex.setReady(
+            site, def.getName(), def.getLatest().getVersion(), true);
+      }
     } else {
-      final String message = String.format(
+      if (IndexType.values().length <= 1) {
+        ui.header("Index");
+      }
+      String message = String.format(
         "\nThe index must be %sbuilt before starting Gerrit:\n"
         + "  java -jar gerrit.war reindex -d site_path\n",
         site.isNew ? "" : "re");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index f16e2ec..5f470a5 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2016 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.
@@ -108,6 +108,7 @@
     extractMailExample("DeleteReviewer.vm");
     extractMailExample("DeleteVote.vm");
     extractMailExample("Footer.vm");
+    extractMailExample("footer.soy");
     extractMailExample("Merged.vm");
     extractMailExample("NewChange.vm");
     extractMailExample("RegisterNewEmail.vm");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
index 1c72600..6739ce0 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
@@ -19,48 +19,29 @@
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.VersionedMetaDataOnInit;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AuthorizedKeys;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.git.VersionedMetaData;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.RepositoryCache.FileKey;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.util.FS;
 
-import java.io.File;
 import java.io.IOException;
-import java.nio.file.Path;
 import java.util.List;
 
-public class VersionedAuthorizedKeysOnInit extends VersionedMetaData {
+public class VersionedAuthorizedKeysOnInit extends VersionedMetaDataOnInit {
   public interface Factory {
     VersionedAuthorizedKeysOnInit create(Account.Id accountId);
   }
 
   private final Account.Id accountId;
-  private final String ref;
-  private final String project;
-  private final SitePaths site;
-  private final InitFlags flags;
-
   private List<Optional<AccountSshKey>> keys;
-  private ObjectId revision;
 
   @Inject
   public VersionedAuthorizedKeysOnInit(
@@ -68,41 +49,19 @@
       SitePaths site,
       InitFlags flags,
       @Assisted Account.Id accountId) {
-
-    this.project = allUsers.get();
-    this.site = site;
-    this.flags = flags;
+    super(flags, site, allUsers.get(), RefNames.refsUsers(accountId));
     this.accountId = accountId;
-    this.ref = RefNames.refsUsers(accountId);
   }
 
   @Override
-  protected String getRefName() {
-    return ref;
-  }
-
   public VersionedAuthorizedKeysOnInit load()
       throws IOException, ConfigInvalidException {
-    File path = getPath();
-    if (path != null) {
-      try (Repository repo = new FileRepository(path)) {
-        load(repo);
-      }
-    }
+    super.load();
     return this;
   }
 
-  private File getPath() {
-    Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
-    if (basePath == null) {
-      throw new IllegalStateException("gerrit.basePath must be configured");
-    }
-    return FileKey.resolve(basePath.resolve(project).toFile(), FS.DETECTED);
-  }
-
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
-    revision = getRevision();
     keys = AuthorizedKeys.parse(accountId, readUTF8(AuthorizedKeys.FILE_NAME));
   }
 
@@ -116,50 +75,6 @@
     return key;
   }
 
-  public void save(String message) throws IOException {
-    save(new PersonIdent("Gerrit Initialization", "init@gerrit"), message);
-  }
-
-  private void save(PersonIdent ident, String msg) throws IOException {
-    File path = getPath();
-    if (path == null) {
-      throw new IOException(project + " does not exist.");
-    }
-
-    try (Repository repo = new FileRepository(path);
-        ObjectInserter i = repo.newObjectInserter();
-        ObjectReader r = repo.newObjectReader();
-        RevWalk rw = new RevWalk(reader)) {
-      inserter = i;
-      reader = r;
-
-      RevTree srcTree = revision != null ? rw.parseTree(revision) : null;
-      newTree = readTree(srcTree);
-
-      CommitBuilder commit = new CommitBuilder();
-      commit.setAuthor(ident);
-      commit.setCommitter(ident);
-      commit.setMessage(msg);
-
-      onSave(commit);
-      ObjectId res = newTree.writeTree(inserter);
-      if (res.equals(srcTree)) {
-        return;
-      }
-
-      commit.setTreeId(res);
-      if (revision != null) {
-        commit.addParentId(revision);
-      }
-      ObjectId newRevision = inserter.insert(commit);
-      updateRef(repo, ident, newRevision, "commit: " + msg);
-      revision = newRevision;
-    } finally {
-      inserter = null;
-      reader = null;
-    }
-  }
-
   @Override
   protected boolean onSave(CommitBuilder commit) throws IOException {
     if (Strings.isNullOrEmpty(commit.getMessage())) {
@@ -169,30 +84,4 @@
     saveUTF8(AuthorizedKeys.FILE_NAME, AuthorizedKeys.serialize(keys));
     return true;
   }
-
-  private void updateRef(Repository repo, PersonIdent ident,
-      ObjectId newRevision, String refLogMsg) throws IOException {
-    RefUpdate ru = repo.updateRef(getRefName());
-    ru.setRefLogIdent(ident);
-    ru.setNewObjectId(newRevision);
-    ru.setExpectedOldObjectId(revision);
-    ru.setRefLogMessage(refLogMsg, false);
-    RefUpdate.Result r = ru.update();
-    switch(r) {
-      case FAST_FORWARD:
-      case NEW:
-      case NO_CHANGE:
-        break;
-      case FORCED:
-      case IO_FAILURE:
-      case LOCK_FAILURE:
-      case NOT_ATTEMPTED:
-      case REJECTED:
-      case REJECTED_CURRENT_BRANCH:
-      case RENAMED:
-      default:
-        throw new IOException("Failed to update " + getRefName() + " of "
-            + project + ": " + r.name());
-    }
-  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
index f7d9d4a..a7ebd33 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -18,73 +18,32 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GroupList;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.VersionedMetaData;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryCache;
-import org.eclipse.jgit.lib.RepositoryCache.FileKey;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.util.FS;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
 import java.io.IOException;
-import java.nio.file.Path;
 
-public class AllProjectsConfig extends VersionedMetaData {
+public class AllProjectsConfig extends VersionedMetaDataOnInit {
 
   private static final Logger log = LoggerFactory.getLogger(AllProjectsConfig.class);
 
-  private final String project;
-  private final SitePaths site;
-  private final InitFlags flags;
-
   private Config cfg;
-  private ObjectId revision;
   private GroupList groupList;
 
   @Inject
   AllProjectsConfig(AllProjectsNameOnInitProvider allProjects, SitePaths site,
       InitFlags flags) {
-    this.project = allProjects.get();
-    this.site = site;
-    this.flags = flags;
+    super(flags, site, allProjects.get(), RefNames.REFS_CONFIG);
 
   }
 
-  @Override
-  protected String getRefName() {
-    return RefNames.REFS_CONFIG;
-  }
-
-  private File getPath() {
-    Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
-    if (basePath == null) {
-      throw new IllegalStateException("gerrit.basePath must be configured");
-    }
-    return FileKey.resolve(basePath.resolve(project).toFile(), FS.DETECTED);
-  }
-
-  public AllProjectsConfig load() throws IOException, ConfigInvalidException {
-    File path = getPath();
-    if (path != null) {
-      try (Repository repo = new FileRepository(path)) {
-        load(repo);
-      }
-    }
-    return this;
-  }
-
   public Config getConfig() {
     return cfg;
   }
@@ -94,10 +53,16 @@
   }
 
   @Override
+  public AllProjectsConfig load()
+      throws IOException, ConfigInvalidException {
+    super.load();
+    return this;
+  }
+
+  @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
     groupList = readGroupList();
     cfg = readConfig(ProjectConfig.PROJECT_CONFIG);
-    revision = getRevision();
   }
 
   private GroupList readGroupList() throws IOException {
@@ -105,96 +70,31 @@
         GroupList.createLoggerSink(GroupList.FILE_NAME, log));
   }
 
-  @Override
-  protected boolean onSave(CommitBuilder commit) throws IOException,
-      ConfigInvalidException {
-    throw new UnsupportedOperationException();
-  }
-
-  public void save(String message) throws IOException {
-    save(new PersonIdent("Gerrit Initialization", "init@gerrit"), message);
-  }
-
-  public void save(String pluginName, String message) throws IOException {
+  public void save(String pluginName, String message)
+      throws IOException, ConfigInvalidException {
     save(new PersonIdent(pluginName, pluginName + "@gerrit"),
         "Update from plugin " + pluginName + ": " + message);
   }
 
-  private void save(PersonIdent ident, String msg) throws IOException {
-    File path = getPath();
-    if (path == null) {
-      throw new IOException("All-Projects does not exist.");
-    }
-
-    try (Repository repo = new FileRepository(path)) {
-      inserter = repo.newObjectInserter();
-      reader = repo.newObjectReader();
-      try (RevWalk rw = new RevWalk(reader)) {
-        RevTree srcTree = revision != null ? rw.parseTree(revision) : null;
-        newTree = readTree(srcTree);
-        saveConfig(ProjectConfig.PROJECT_CONFIG, cfg);
-        saveGroupList();
-        ObjectId res = newTree.writeTree(inserter);
-        if (res.equals(srcTree)) {
-          // If there are no changes to the content, don't create the commit.
-          return;
-        }
-
-        CommitBuilder commit = new CommitBuilder();
-        commit.setAuthor(ident);
-        commit.setCommitter(ident);
-        commit.setMessage(msg);
-        commit.setTreeId(res);
-        if (revision != null) {
-          commit.addParentId(revision);
-        }
-        ObjectId newRevision = inserter.insert(commit);
-        updateRef(repo, ident, newRevision, "commit: " + msg);
-        revision = newRevision;
-      } finally {
-        if (inserter != null) {
-          inserter.close();
-          inserter = null;
-        }
-        if (reader != null) {
-          reader.close();
-          reader = null;
-        }
-      }
-    }
+  @Override
+  protected void save(PersonIdent ident, String msg)
+      throws IOException, ConfigInvalidException {
+    super.save(ident, msg);
 
     // we need to invalidate the JGit cache if the group list is invalidated in
     // an unattended init step
     RepositoryCache.clear();
   }
 
-  private void saveGroupList() throws IOException {
-    saveUTF8(GroupList.FILE_NAME, groupList.asText());
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException {
+    saveConfig(ProjectConfig.PROJECT_CONFIG, cfg);
+    saveGroupList();
+    return true;
   }
 
-  private void updateRef(Repository repo, PersonIdent ident,
-      ObjectId newRevision, String refLogMsg) throws IOException {
-    RefUpdate ru = repo.updateRef(getRefName());
-    ru.setRefLogIdent(ident);
-    ru.setNewObjectId(newRevision);
-    ru.setExpectedOldObjectId(revision);
-    ru.setRefLogMessage(refLogMsg, false);
-    RefUpdate.Result r = ru.update();
-    switch (r) {
-      case FAST_FORWARD:
-      case NEW:
-      case NO_CHANGE:
-        break;
-      case FORCED:
-      case IO_FAILURE:
-      case LOCK_FAILURE:
-      case NOT_ATTEMPTED:
-      case REJECTED:
-      case REJECTED_CURRENT_BRANCH:
-      case RENAMED:
-      default:
-        throw new IOException("Failed to update " + getRefName() + " of "
-            + project + ": " + r.name());
-    }
+  private void saveGroupList() throws IOException {
+    saveUTF8(GroupList.FILE_NAME, groupList.asText());
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
new file mode 100644
index 0000000..b953a0b
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
@@ -0,0 +1,148 @@
+// Copyright (C) 2016 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.pgm.init.api;
+
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.VersionedMetaData;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+
+public abstract class VersionedMetaDataOnInit extends VersionedMetaData {
+
+  private final InitFlags flags;
+  private final SitePaths site;
+  private final String project;
+  private final String ref;
+
+  protected VersionedMetaDataOnInit(InitFlags flags, SitePaths site,
+      String project, String ref) {
+    this.flags = flags;
+    this.site = site;
+    this.project = project;
+    this.ref = ref;
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  public VersionedMetaDataOnInit load()
+      throws IOException, ConfigInvalidException {
+    File path = getPath();
+    if (path != null) {
+      try (Repository repo = new FileRepository(path)) {
+        load(repo);
+      }
+    }
+    return this;
+  }
+
+  public void save(String message) throws IOException, ConfigInvalidException {
+    save(new PersonIdent("Gerrit Initialization", "init@gerrit"), message);
+  }
+
+  protected void save(PersonIdent ident, String msg)
+      throws IOException, ConfigInvalidException {
+    File path = getPath();
+    if (path == null) {
+      throw new IOException(project + " does not exist.");
+    }
+
+    try (Repository repo = new FileRepository(path);
+        ObjectInserter i = repo.newObjectInserter();
+        ObjectReader r = repo.newObjectReader();
+        RevWalk rw = new RevWalk(r)) {
+      inserter = i;
+      reader = r;
+
+      RevTree srcTree = revision != null ? rw.parseTree(revision) : null;
+      newTree = readTree(srcTree);
+
+      CommitBuilder commit = new CommitBuilder();
+      commit.setAuthor(ident);
+      commit.setCommitter(ident);
+      commit.setMessage(msg);
+
+      onSave(commit);
+
+      ObjectId res = newTree.writeTree(inserter);
+      if (res.equals(srcTree)) {
+        return;
+      }
+      commit.setTreeId(res);
+
+      if (revision != null) {
+        commit.addParentId(revision);
+      }
+      ObjectId newRevision = inserter.insert(commit);
+      updateRef(repo, ident, newRevision, "commit: " + msg);
+      revision = rw.parseCommit(newRevision);
+    } finally {
+      inserter = null;
+      reader = null;
+    }
+  }
+
+  private void updateRef(Repository repo, PersonIdent ident,
+      ObjectId newRevision, String refLogMsg) throws IOException {
+    RefUpdate ru = repo.updateRef(getRefName());
+    ru.setRefLogIdent(ident);
+    ru.setNewObjectId(newRevision);
+    ru.setExpectedOldObjectId(revision);
+    ru.setRefLogMessage(refLogMsg, false);
+    RefUpdate.Result r = ru.update();
+    switch(r) {
+      case FAST_FORWARD:
+      case NEW:
+      case NO_CHANGE:
+        break;
+      case FORCED:
+      case IO_FAILURE:
+      case LOCK_FAILURE:
+      case NOT_ATTEMPTED:
+      case REJECTED:
+      case REJECTED_CURRENT_BRANCH:
+      case RENAMED:
+      default:
+        throw new IOException("Failed to update " + getRefName() + " of "
+            + project + ": " + r.name());
+    }
+  }
+
+  private File getPath() {
+    Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
+    if (basePath == null) {
+      throw new IllegalStateException("gerrit.basePath must be configured");
+    }
+    return FileKey.resolve(basePath.resolve(project).toFile(), FS.DETECTED);
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
index be573e6..0360cd6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.pgm.util;
 
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.DisabledChangeHooks;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -27,7 +25,6 @@
 public class BatchGitModule extends FactoryModule {
   @Override
   protected void configure() {
-    bind(ChangeHooks.class).to(DisabledChangeHooks.class);
     DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
     DynamicSet.setOf(binder(), CommitValidationListener.class);
     factory(CommitValidators.Factory.class);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index a573625..f076e54 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -17,6 +17,8 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -27,6 +29,9 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountByEmailCacheImpl;
 import com.google.gerrit.server.account.AccountCacheImpl;
+import com.google.gerrit.server.account.AccountVisibility;
+import com.google.gerrit.server.account.AccountVisibilityProvider;
+import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.FakeRealm;
 import com.google.gerrit.server.account.GroupCacheImpl;
@@ -39,6 +44,7 @@
 import com.google.gerrit.server.change.MergeabilityCacheImpl;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.config.AdministrateServerGroups;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.DisableReverseDnsLookup;
@@ -128,6 +134,9 @@
     bind(SearchingChangeCacheImpl.class).toProvider(
         Providers.<SearchingChangeCacheImpl>of(null));
 
+    bind(new TypeLiteral<ImmutableSet<GroupReference>>() {})
+      .annotatedWith(AdministrateServerGroups.class)
+      .toInstance(ImmutableSet.<GroupReference> of());
     bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
       .annotatedWith(GitUploadPackGroups.class)
       .toInstance(Collections.<AccountGroup.UUID> emptySet());
@@ -151,11 +160,15 @@
     install(ChangeKindCacheImpl.module());
     install(MergeabilityCacheImpl.module());
     install(TagCache.module());
+    factory(CapabilityCollection.Factory.class);
     factory(CapabilityControl.Factory.class);
     factory(ChangeData.Factory.class);
     factory(ProjectState.Factory.class);
 
     bind(ChangeJson.Factory.class).toProvider(
         Providers.<ChangeJson.Factory>of(null));
+    bind(AccountVisibility.class)
+        .toProvider(AccountVisibilityProvider.class)
+        .in(SINGLETON);
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
index 1107208..262997b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.pgm.util;
 
 import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -23,6 +24,8 @@
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
 
+import org.joda.time.DateTime;
+import org.joda.time.Duration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -48,17 +51,25 @@
 
   static class Lifecycle implements LifecycleListener {
     private final WorkQueue queue;
-    private final LogFileCompressor compresser;
+    private final LogFileCompressor compressor;
 
     @Inject
-    Lifecycle(final WorkQueue queue, final LogFileCompressor compressor) {
+    Lifecycle(WorkQueue queue,
+        LogFileCompressor compressor) {
       this.queue = queue;
-      this.compresser = compressor;
+      this.compressor = compressor;
     }
 
     @Override
     public void start() {
-      queue.getDefaultQueue().scheduleAtFixedRate(compresser, 1, 24, HOURS);
+      //compress log once and then schedule compression every day at 11:00pm
+      queue.getDefaultQueue().execute(compressor);
+      DateTime now = DateTime.now();
+      long milliSecondsUntil11am =
+          new Duration(now, now.withTimeAtStartOfDay().plusHours(23))
+              .getMillis();
+      queue.getDefaultQueue().scheduleAtFixedRate(compressor,
+          milliSecondsUntil11am, HOURS.toMillis(24), MILLISECONDS);
     }
 
     @Override
@@ -69,7 +80,7 @@
   private final Path logs_dir;
 
   @Inject
-  LogFileCompressor(final SitePaths site) {
+  LogFileCompressor(SitePaths site) {
     logs_dir = resolve(site.logs_dir);
   }
 
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
index 9862d87..0f7f3d8 100755
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
@@ -498,6 +498,7 @@
     $GERRIT_SH stop $*
     sleep 5
     $GERRIT_SH start $*
+    exit $?
   ;;
 
   supervise)
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
index af30f73..89f61cc 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
@@ -128,6 +128,12 @@
     }
 
     @Override
+    public String[] getListForPlugin(String pluginName, String section,
+        String subsection, String name) {
+      throw new UnsupportedOperationException("not used by tests");
+    }
+
+    @Override
     public void setList(String section, String subsection, String name,
         List<String> values) {
       cfg.setStringList(section, subsection, name, values);
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index 777f3a8..70886d5 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -13,10 +13,20 @@
 
 java_binary(
   name = 'plugin-api',
+  merge_manifests = False,
+  manifest_file = ':manifest',
   deps = [':lib'],
   visibility = ['PUBLIC'],
 )
 
+genrule(
+  name = 'manifest',
+  cmd = 'echo "Manifest-Version: 1.0" >$OUT;' +
+    'echo "Implementation-Title: Gerrit Plugin API" >>$OUT;' +
+    'echo "Implementation-Vendor: Gerrit Code Review Project" >>$OUT',
+  out = 'manifest.txt',
+)
+
 java_library(
   name = 'lib',
   exported_deps = PLUGIN_API + [
@@ -29,22 +39,33 @@
     '//gerrit-reviewdb:server',
     '//lib:args4j',
     '//lib:blame-cache',
-    '//lib/dropwizard:dropwizard-core',
+    '//lib:gson',
     '//lib:guava',
     '//lib:gwtorm',
+    '//lib:icu4j',
     '//lib:jsch',
+    '//lib:jsr305',
     '//lib:mime-util',
+    '//lib:protobuf',
     '//lib:servlet-api-3_1',
+    '//lib:soy',
     '//lib:velocity',
     '//lib/commons:lang',
+    '//lib/dropwizard:dropwizard-core',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
+    '//lib/guice:javax-inject',
+    '//lib/guice:multibindings',
     '//lib/guice:guice-servlet',
     '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet',
     '//lib/joda:joda-time',
     '//lib/log:api',
     '//lib/mina:sshd',
+    '//lib/ow2:ow2-asm',
+    '//lib/ow2:ow2-asm-analysis',
+    '//lib/ow2:ow2-asm-commons',
+    '//lib/ow2:ow2-asm-util',
     '//lib/prolog:compiler',
   ],
   visibility = ['PUBLIC'],
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 77bb111..9309921 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index d8e3583..e69da7c 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-archetype</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <name>Gerrit Code Review - Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Plugins</description>
   <url>https://www.gerritcodereview.com/</url>
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index 362b45c..5e9bc33 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwt-archetype</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <name>Gerrit Code Review - Web UI GWT Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Web UI GWT Plugins</description>
   <url>https://www.gerritcodereview.com/</url>
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index 982d268..4b104c6 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwtui</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin GWT UI</name>
   <description>Common Classes for Gerrit GWT UI Plugins</description>
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
index 8d408fb..d627959 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
@@ -37,7 +37,7 @@
   }
 
   public RestApi id(String id) {
-    return idRaw(URL.encodeQueryString(id));
+    return idRaw(URL.encodePathSegment(id));
   }
 
   public RestApi id(int id) {
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index e22478a..d31b455 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-js-archetype</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <name>Gerrit Code Review - Web UI JavaScript Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Web UI JavaScript Plugins</description>
   <url>https://www.gerritcodereview.com/</url>
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 9e36fc1..de2134b 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
@@ -55,10 +55,6 @@
  * </ul>
  */
 public final class Account {
-  public enum FieldName {
-    FULL_NAME, USER_NAME, REGISTER_NEW_EMAIL
-  }
-
   public static final String USER_NAME_PATTERN_FIRST = "[a-zA-Z0-9]";
   public static final String USER_NAME_PATTERN_REST = "[a-zA-Z0-9._-]";
   public static final String USER_NAME_PATTERN_LAST = "[a-zA-Z0-9]";
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
index 41336791..5ae8847 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.reviewdb.client;
 
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
index c7f52b0..a6796e7 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
@@ -22,8 +22,14 @@
 public final class AccountProjectWatch {
 
   public enum NotifyType {
-    NEW_CHANGES, NEW_PATCHSETS, ALL_COMMENTS, SUBMITTED_CHANGES,
-    ABANDONED_CHANGES, ALL
+    // sort by name, except 'ALL' which should stay last
+    ABANDONED_CHANGES,
+    ALL_COMMENTS,
+    NEW_CHANGES,
+    NEW_PATCHSETS,
+    SUBMITTED_CHANGES,
+
+    ALL
   }
 
   public static final String FILTER_ALL = "*";
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
index ea728e8..a8bf07b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -185,7 +185,7 @@
    * Changes on the same branch having patch sets with intersecting groups are
    * considered related, as in the "Related Changes" tab.
    */
-  @Column(id = 6, notNull = false)
+  @Column(id = 6, notNull = false, length = Integer.MAX_VALUE)
   protected String groups;
 
   //DELETED id = 7 (pushCertficate)
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index a1278fa..b2bd818 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -16,6 +16,8 @@
 
 /** Constants and utilities for Gerrit-specific ref names. */
 public class RefNames {
+  public static final String HEAD = "HEAD";
+
   public static final String REFS = "refs/";
 
   public static final String REFS_HEADS = "refs/heads/";
@@ -69,7 +71,8 @@
   public static final String EDIT_PREFIX = "edit-";
 
   public static String fullName(String ref) {
-    return ref.startsWith(REFS) ? ref : REFS_HEADS + ref;
+    return (ref.startsWith(REFS) || ref.equals(HEAD)) ?
+        ref : REFS_HEADS + ref;
   }
 
   public static final String shortName(String ref) {
@@ -175,6 +178,10 @@
     return ref.startsWith(REFS_USERS) && ref.contains(EDIT_PREFIX);
   }
 
+  public static boolean isRefsUsers(String ref) {
+    return ref.startsWith(REFS_USERS);
+  }
+
   static Integer parseShardedRefPart(String name) {
     if (name == null) {
       return null;
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index d723667..bdceb7b 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -9,7 +9,6 @@
 ALTER TABLE patch_set_approvals CLUSTER ON patch_set_approvals_pkey;
 
 ALTER TABLE account_group_members CLUSTER ON account_group_members_pkey;
-ALTER TABLE starred_changes CLUSTER ON starred_changes_pkey;
 CLUSTER;
 
 
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 40d8b53..57cedd5 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
@@ -27,10 +27,11 @@
 
   @Test
   public void fullName() throws Exception {
-    assertThat(RefNames.fullName("refs/meta/config")).isEqualTo("refs/meta/config");
+    assertThat(RefNames.fullName(RefNames.REFS_CONFIG)).isEqualTo(RefNames.REFS_CONFIG);
     assertThat(RefNames.fullName("refs/heads/master")).isEqualTo("refs/heads/master");
     assertThat(RefNames.fullName("master")).isEqualTo("refs/heads/master");
     assertThat(RefNames.fullName("refs/tags/v1.0")).isEqualTo("refs/tags/v1.0");
+    assertThat(RefNames.fullName("HEAD")).isEqualTo("HEAD");
   }
 
   @Test
@@ -81,6 +82,16 @@
   }
 
   @Test
+  public void isRefsUsers() throws Exception {
+    assertThat(RefNames.isRefsUsers("refs/users/23/1011123")).isTrue();
+    assertThat(RefNames.isRefsUsers("refs/users/default")).isTrue();
+    assertThat(RefNames.isRefsUsers("refs/users/23/1011123/edit-67473/42"))
+        .isTrue();
+
+    assertThat(RefNames.isRefsUsers("refs/heads/master")).isFalse();
+  }
+
+  @Test
   public void testParseShardedRefsPart() throws Exception {
     assertThat(parseShardedRefPart("01/1")).isEqualTo(1);
     assertThat(parseShardedRefPart("01/1-drafts")).isEqualTo(1);
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 4fc578c..443d7c4 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -46,6 +46,7 @@
     '//lib:mime-util',
     '//lib:pegdown',
     '//lib:protobuf',
+    '//lib:soy',
     '//lib:tukaani-xz',
     '//lib:velocity',
     '//lib/antlr:java_runtime',
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookApiListener.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookApiListener.java
deleted file mode 100644
index 69a3f61..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookApiListener.java
+++ /dev/null
@@ -1,376 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common;
-
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
-import static org.eclipse.jgit.lib.Constants.R_HEADS;
-
-import com.google.gerrit.common.ChangeHookRunner.HookResult;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.common.ApprovalInfo;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.RevisionInfo;
-import com.google.gerrit.extensions.events.AgreementSignupListener;
-import com.google.gerrit.extensions.events.ChangeAbandonedListener;
-import com.google.gerrit.extensions.events.ChangeMergedListener;
-import com.google.gerrit.extensions.events.ChangeRestoredListener;
-import com.google.gerrit.extensions.events.CommentAddedListener;
-import com.google.gerrit.extensions.events.DraftPublishedListener;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
-import com.google.gerrit.extensions.events.HashtagsEditedListener;
-import com.google.gerrit.extensions.events.NewProjectCreatedListener;
-import com.google.gerrit.extensions.events.ReviewerAddedListener;
-import com.google.gerrit.extensions.events.ReviewerDeletedListener;
-import com.google.gerrit.extensions.events.RevisionCreatedListener;
-import com.google.gerrit.extensions.events.TopicEditedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.lifecycle.LifecycleModule;
-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.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.events.CommitReceivedEvent;
-import com.google.gerrit.server.git.validators.CommitValidationException;
-import com.google.gerrit.server.git.validators.CommitValidationListener;
-import com.google.gerrit.server.git.validators.CommitValidationMessage;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.ObjectId;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
-@Singleton
-public class ChangeHookApiListener implements
-    AgreementSignupListener,
-    ChangeAbandonedListener,
-    ChangeMergedListener,
-    ChangeRestoredListener,
-    CommentAddedListener,
-    DraftPublishedListener,
-    GitReferenceUpdatedListener,
-    HashtagsEditedListener,
-    NewProjectCreatedListener,
-    ReviewerAddedListener,
-    ReviewerDeletedListener,
-    RevisionCreatedListener,
-    TopicEditedListener {
-  /** A logger for this class. */
-  private static final Logger log =
-      LoggerFactory.getLogger(ChangeHookApiListener.class);
-
-  public static class Module extends LifecycleModule {
-    @Override
-    protected void configure() {
-      DynamicSet.bind(binder(), AgreementSignupListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), ChangeAbandonedListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), ChangeMergedListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), ChangeRestoredListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), CommentAddedListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), DraftPublishedListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), HashtagsEditedListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), NewProjectCreatedListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), ReviewerAddedListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), ReviewerDeletedListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), RevisionCreatedListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), TopicEditedListener.class)
-        .to(ChangeHookApiListener.class);
-      DynamicSet.bind(binder(), CommitValidationListener.class)
-        .to(ChangeHookValidator.class);
-    }
-  }
-
-  /** Reject commits that don't pass user-supplied ref-update hook. */
-  public static class ChangeHookValidator implements
-      CommitValidationListener {
-    private final ChangeHooks hooks;
-
-    @Inject
-    public ChangeHookValidator(ChangeHooks hooks) {
-      this.hooks = hooks;
-    }
-
-    @Override
-    public List<CommitValidationMessage> onCommitReceived(
-        CommitReceivedEvent receiveEvent) throws CommitValidationException {
-      IdentifiedUser user = receiveEvent.user;
-      String refname = receiveEvent.refName;
-      ObjectId old = ObjectId.zeroId();
-      if (receiveEvent.commit.getParentCount() > 0) {
-        old = receiveEvent.commit.getParent(0);
-      }
-
-      if (receiveEvent.command.getRefName().startsWith(REFS_CHANGES)) {
-        /*
-        * If the ref-update hook tries to distinguish behavior between pushes to
-        * refs/heads/... and refs/for/..., make sure we send it the correct
-        * refname.
-        * Also, if this is targetting refs/for/, make sure we behave the same as
-        * what a push to refs/for/ would behave; in particular, setting oldrev
-        * to 0000000000000000000000000000000000000000.
-        */
-        refname = refname.replace(R_HEADS, "refs/for/refs/heads/");
-        old = ObjectId.zeroId();
-      }
-      HookResult result = hooks.doRefUpdateHook(receiveEvent.project, refname,
-          user.getAccount(), old, receiveEvent.commit);
-      if (result != null && result.getExitValue() != 0) {
-          throw new CommitValidationException(result.toString().trim());
-      }
-      return Collections.emptyList();
-    }
-  }
-
-  private final Provider<ReviewDb> db;
-  private final AccountCache accounts;
-  private final ChangeHooks hooks;
-  private final PatchSetUtil psUtil;
-  private final ChangeNotes.Factory changeNotesFactory;
-
-  @Inject
-  public ChangeHookApiListener(
-      Provider<ReviewDb> db,
-      AccountCache accounts,
-      ChangeHooks hooks,
-      PatchSetUtil psUtil,
-      ChangeNotes.Factory changeNotesFactory) {
-    this.db = db;
-    this.accounts = accounts;
-    this.hooks = hooks;
-    this.psUtil = psUtil;
-    this.changeNotesFactory = changeNotesFactory;
-  }
-
-  @Override
-  public void onNewProjectCreated(NewProjectCreatedListener.Event ev) {
-    hooks.doProjectCreatedHook(new Project.NameKey(ev.getProjectName()),
-        ev.getHeadName());
-  }
-
-  @Override
-  public void onRevisionCreated(RevisionCreatedListener.Event ev) {
-    try {
-      ChangeNotes notes = getNotes(ev.getChange());
-      hooks.doPatchsetCreatedHook(notes.getChange(),
-        getPatchSet(notes, ev.getRevision()), db.get());
-    } catch (OrmException e) {
-      log.error("PatchsetCreated hook failed to run "
-          + ev.getChange()._number, e);
-    }
-  }
-
-  @Override
-  public void onDraftPublished(DraftPublishedListener.Event ev) {
-    try {
-      ChangeNotes notes = getNotes(ev.getChange());
-      hooks.doDraftPublishedHook(notes.getChange(),
-        getPatchSet(notes, ev.getRevision()), db.get());
-    } catch (OrmException e) {
-      log.error("DraftPublished hook failed to run "
-          + ev.getChange()._number, e);
-    }
-  }
-
-  @Override
-  public void onCommentAdded(CommentAddedListener.Event ev) {
-    Map<String, Short> approvals = convertApprovalsMap(ev.getApprovals());
-    Map<String, Short> oldApprovals = convertApprovalsMap(ev.getOldApprovals());
-    try {
-      ChangeNotes notes = getNotes(ev.getChange());
-      hooks.doCommentAddedHook(notes.getChange(),
-        getAccount(ev.getAuthor()),
-        getPatchSet(notes, ev.getRevision()),
-        ev.getComment(), approvals, oldApprovals, db.get());
-    } catch (OrmException e) {
-      log.error("CommentAdded hook failed to fun" + ev.getChange()._number, e);
-    }
-  }
-
-  @Override
-  public void onChangeMerged(ChangeMergedListener.Event ev) {
-    try {
-      ChangeNotes notes = getNotes(ev.getChange());
-      hooks.doChangeMergedHook(notes.getChange(),
-          getAccount(ev.getMerger()),
-          getPatchSet(notes, ev.getRevision()),
-          db.get(), ev.getNewRevisionId());
-    } catch (OrmException e) {
-      log.error("ChangeMerged hook failed to run " + ev.getChange()._number, e);
-    }
-  }
-
-  @Override
-  public void onChangeAbandoned(ChangeAbandonedListener.Event ev) {
-    try {
-      ChangeNotes notes = getNotes(ev.getChange());
-      hooks.doChangeAbandonedHook(notes.getChange(),
-          getAccount(ev.getAbandoner()),
-          getPatchSet(notes, ev.getRevision()),
-          ev.getReason(), db.get());
-    } catch (OrmException e) {
-      log.error("ChangeAbandoned hook failed to run "
-          + ev.getChange()._number, e);
-    }
-  }
-
-  @Override
-  public void onChangeRestored(ChangeRestoredListener.Event ev) {
-    try {
-      ChangeNotes notes = getNotes(ev.getChange());
-      hooks.doChangeRestoredHook(notes.getChange(),
-          getAccount(ev.getRestorer()),
-          getPatchSet(notes, ev.getRevision()),
-          ev.getReason(), db.get());
-    } catch (OrmException e) {
-      log.error("ChangeRestored hook failed to run "
-          + ev.getChange()._number, e);
-    }
-  }
-
-  @Override
-  public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event ev) {
-    hooks.doRefUpdatedHook(
-        new Branch.NameKey(ev.getProjectName(), ev.getRefName()),
-        ObjectId.fromString(ev.getOldObjectId()),
-        ObjectId.fromString(ev.getNewObjectId()),
-        getAccount(ev.getUpdater()));
-  }
-
-  @Override
-  public void onReviewerAdded(ReviewerAddedListener.Event ev) {
-    try {
-      ChangeNotes notes = getNotes(ev.getChange());
-      hooks.doReviewerAddedHook(notes.getChange(),
-          getAccount(ev.getReviewer()),
-          psUtil.current(db.get(), notes),
-          db.get());
-    } catch (OrmException e) {
-      log.error("ReviewerAdded hook failed to run "
-          + ev.getChange()._number, e);
-    }
-  }
-
-  @Override
-  public void onReviewerDeleted(ReviewerDeletedListener.Event ev) {
-    try {
-      ChangeNotes notes = getNotes(ev.getChange());
-      hooks.doReviewerDeletedHook(notes.getChange(),
-          getAccount(ev.getReviewer()),
-          psUtil.current(db.get(), notes),
-          ev.getComment(),
-          convertApprovalsMap(ev.getNewApprovals()),
-          convertApprovalsMap(ev.getOldApprovals()),
-          db.get());
-    } catch (OrmException e) {
-      log.error("ReviewerDeleted hook failed to run "
-          + ev.getChange()._number, e);
-    }
-  }
-
-  @Override
-  public void onTopicEdited(TopicEditedListener.Event ev) {
-    try {
-      hooks.doTopicChangedHook(getNotes(ev.getChange()).getChange(),
-        getAccount(ev.getEditor()), ev.getOldTopic(), db.get());
-    } catch (OrmException e) {
-      log.error("TopicChanged hook failed to run "
-          + ev.getChange()._number, e);
-    }
-  }
-
-  @Override
-  public void onHashtagsEdited(HashtagsEditedListener.Event ev) {
-    try {
-      hooks.doHashtagsChangedHook(getNotes(ev.getChange()).getChange(),
-          getAccount(ev.getEditor()),
-          new HashSet<>(ev.getAddedHashtags()),
-          new HashSet<>(ev.getRemovedHashtags()),
-          new HashSet<>(ev.getHashtags()),
-          db.get());
-    } catch (OrmException e) {
-      log.error("HashtagsChanged hook failed to run "
-          + ev.getChange()._number, e);
-    }
-  }
-
-  @Override
-  public void onAgreementSignup(AgreementSignupListener.Event ev) {
-    hooks.doClaSignupHook(getAccount(ev.getAccount()), ev.getAgreementName());
-  }
-
-  private ChangeNotes getNotes(ChangeInfo info) throws OrmException {
-    try {
-      return changeNotesFactory.createChecked(new Change.Id(info._number));
-    } catch (NoSuchChangeException e) {
-      throw new OrmException(e);
-    }
-  }
-
-  private PatchSet getPatchSet(ChangeNotes notes, RevisionInfo info)
-      throws OrmException {
-    return psUtil.get(db.get(), notes, PatchSet.Id.fromRef(info.ref));
-  }
-
-  private Account getAccount(AccountInfo info) {
-    if (info != null) {
-      AccountState account = accounts.get(new Account.Id(info._accountId));
-      if (account != null) {
-        return account.getAccount();
-      }
-    }
-    return null;
-  }
-
-  private static Map<String, Short> convertApprovalsMap(
-      Map<String, ApprovalInfo> approvals) {
-    Map<String, Short> result = new HashMap<>();
-    for (Entry<String, ApprovalInfo> e : approvals.entrySet()) {
-      Short value =
-          e.getValue().value == null ? null : e.getValue().value.shortValue();
-      result.put(e.getKey(), value);
-    }
-    return result;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
deleted file mode 100644
index c5ea982..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ /dev/null
@@ -1,886 +0,0 @@
-// Copyright (C) 2010 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.common;
-
-import com.google.common.base.Optional;
-import com.google.common.collect.Sets;
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.lifecycle.LifecycleModule;
-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.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.data.ChangeAttribute;
-import com.google.gerrit.server.data.PatchSetAttribute;
-import com.google.gerrit.server.data.RefUpdateAttribute;
-import com.google.gerrit.server.events.EventFactory;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.StringReader;
-import java.io.StringWriter;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.FutureTask;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/** Spawns local executables when a hook action occurs. */
-@Singleton
-public class ChangeHookRunner implements ChangeHooks, LifecycleListener {
-    /** A logger for this class. */
-    private static final Logger log = LoggerFactory.getLogger(ChangeHookRunner.class);
-
-    public static class Module extends LifecycleModule {
-      @Override
-      protected void configure() {
-        bind(ChangeHookRunner.class);
-        bind(ChangeHooks.class).to(ChangeHookRunner.class);
-        listener().to(ChangeHookRunner.class);
-      }
-    }
-
-    /** Container class used to hold the return code and output of script hook execution */
-    public static class HookResult {
-      private int exitValue = -1;
-      private String output;
-      private String executionError;
-
-      private HookResult(int exitValue, String output) {
-        this.exitValue = exitValue;
-        this.output = output;
-      }
-
-      private HookResult(String output, String executionError) {
-        this.output = output;
-        this.executionError = executionError;
-      }
-
-      public int getExitValue() {
-        return exitValue;
-      }
-
-      public void setExitValue(int exitValue) {
-        this.exitValue = exitValue;
-      }
-
-      public String getOutput() {
-        return output;
-      }
-
-      @Override
-      public String toString() {
-        StringBuilder sb = new StringBuilder();
-
-        if (output != null && output.length() != 0) {
-          sb.append(output);
-
-          if (executionError != null) {
-            sb.append(" - ");
-          }
-        }
-
-        if (executionError != null ) {
-          sb.append(executionError);
-        }
-
-        return sb.toString();
-      }
-    }
-
-    /** Path of the new patchset hook. */
-    private final Optional<Path> patchsetCreatedHook;
-
-    /** Path of the draft published hook. */
-    private final Optional<Path> draftPublishedHook;
-
-    /** Path of the new comments hook. */
-    private final Optional<Path> commentAddedHook;
-
-    /** Path of the change merged hook. */
-    private final Optional<Path> changeMergedHook;
-
-    /** Path of the change abandoned hook. */
-    private final Optional<Path> changeAbandonedHook;
-
-    /** Path of the change restored hook. */
-    private final Optional<Path> changeRestoredHook;
-
-    /** Path of the ref updated hook. */
-    private final Optional<Path> refUpdatedHook;
-
-    /** Path of the reviewer added hook. */
-    private final Optional<Path> reviewerAddedHook;
-
-    /** Path of the reviewer deleted hook. */
-    private final Optional<Path> reviewerDeletedHook;
-
-    /** Path of the topic changed hook. */
-    private final Optional<Path> topicChangedHook;
-
-    /** Path of the cla signed hook. */
-    private final Optional<Path> claSignedHook;
-
-    /** Path of the update hook. */
-    private final Optional<Path> refUpdateHook;
-
-    /** Path of the hashtags changed hook */
-    private final Optional<Path> hashtagsChangedHook;
-
-    /** Path of the project created hook. */
-    private final Optional<Path> projectCreatedHook;
-
-    private final String anonymousCowardName;
-
-    /** Repository Manager. */
-    private final GitRepositoryManager repoManager;
-
-    /** Queue of hooks that need to run. */
-    private final WorkQueue.Executor hookQueue;
-
-    private final ProjectCache projectCache;
-
-    private final AccountCache accountCache;
-
-    private final EventFactory eventFactory;
-
-    private final SitePaths sitePaths;
-
-    /** Thread pool used to monitor sync hooks */
-    private final ExecutorService syncHookThreadPool;
-
-    /** Timeout value for synchronous hooks */
-    private final int syncHookTimeout;
-
-    /**
-     * Create a new ChangeHookRunner.
-     *
-     * @param queue Queue to use when processing hooks.
-     * @param repoManager The repository manager.
-     * @param config Config file to use.
-     * @param sitePath The sitepath of this gerrit install.
-     * @param projectCache the project cache instance for the server.
-     */
-    @Inject
-    public ChangeHookRunner(WorkQueue queue,
-      GitRepositoryManager repoManager,
-      @GerritServerConfig Config config,
-      @AnonymousCowardName String anonymousCowardName,
-      SitePaths sitePath,
-      ProjectCache projectCache,
-      AccountCache accountCache,
-      EventFactory eventFactory) {
-        this.anonymousCowardName = anonymousCowardName;
-        this.repoManager = repoManager;
-        this.hookQueue = queue.createQueue(1, "hook");
-        this.projectCache = projectCache;
-        this.accountCache = accountCache;
-        this.eventFactory = eventFactory;
-        this.sitePaths = sitePath;
-
-        Path hooksPath;
-        String hooksPathConfig = config.getString("hooks", null, "path");
-        if (hooksPathConfig != null) {
-          hooksPath = Paths.get(hooksPathConfig);
-        } else {
-          hooksPath = sitePath.hooks_dir;
-        }
-
-        // When adding a new hook, make sure to check that the setting name
-        // canonicalizes correctly in hook() below.
-        patchsetCreatedHook = hook(config, hooksPath, "patchset-created");
-        draftPublishedHook = hook(config, hooksPath, "draft-published");
-        commentAddedHook = hook(config, hooksPath, "comment-added");
-        changeMergedHook = hook(config, hooksPath, "change-merged");
-        changeAbandonedHook = hook(config, hooksPath, "change-abandoned");
-        changeRestoredHook = hook(config, hooksPath, "change-restored");
-        refUpdatedHook = hook(config, hooksPath, "ref-updated");
-        reviewerAddedHook = hook(config, hooksPath, "reviewer-added");
-        reviewerDeletedHook = hook(config, hooksPath, "reviewer-deleted");
-        topicChangedHook = hook(config, hooksPath, "topic-changed");
-        claSignedHook = hook(config, hooksPath, "cla-signed");
-        refUpdateHook = hook(config, hooksPath, "ref-update");
-        hashtagsChangedHook = hook(config, hooksPath, "hashtags-changed");
-        projectCreatedHook = hook(config, hooksPath, "project-created");
-
-        syncHookTimeout = config.getInt("hooks", "syncHookTimeout", 30);
-        syncHookThreadPool = Executors.newCachedThreadPool(
-            new ThreadFactoryBuilder()
-              .setNameFormat("SyncHook-%d")
-              .build());
-    }
-
-    private static Optional<Path> hook(Config config, Path path, String name) {
-      String setting = name.replace("-", "") + "hook";
-      String value = config.getString("hooks", null, setting);
-      Path p = path.resolve(value != null ? value : name);
-      return Files.exists(p) ? Optional.of(p) : Optional.<Path>absent();
-    }
-
-    /**
-     * Get the Repository for the given project name, or null on error.
-     *
-     * @param name Project to get repo for,
-     * @return Repository or null.
-     */
-    private Repository openRepository(Project.NameKey name) {
-      try {
-        return repoManager.openRepository(name);
-      } catch (IOException err) {
-        log.warn("Cannot open repository " + name.get(), err);
-        return null;
-      }
-    }
-
-    private void addArg(List<String> args, String name, String value) {
-      if (value != null) {
-        args.add(name);
-        args.add(value);
-      }
-    }
-
-    @Override
-    public HookResult doRefUpdateHook(Project project, String refname,
-        Account uploader, ObjectId oldId, ObjectId newId) {
-      if (!refUpdateHook.isPresent()) {
-        return null;
-      }
-
-      List<String> args = new ArrayList<>();
-      addArg(args, "--project", project.getName());
-      addArg(args, "--refname", refname);
-      addArg(args, "--uploader", getDisplayName(uploader));
-      addArg(args, "--oldrev", oldId.getName());
-      addArg(args, "--newrev", newId.getName());
-
-      return runSyncHook(project.getNameKey(), refUpdateHook, args);
-    }
-
-    @Override
-    public void doProjectCreatedHook(Project.NameKey project, String headName) {
-      if (!projectCreatedHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      addArg(args, "--project", project.get());
-      addArg(args, "--head", headName);
-
-      runHook(project, projectCreatedHook, args);
-    }
-
-    @Override
-    public void doPatchsetCreatedHook(Change change,
-        PatchSet patchSet, ReviewDb db) throws OrmException {
-      if (!patchsetCreatedHook.isPresent()) {
-        return;
-      }
-
-      AccountState owner = accountCache.get(change.getOwner());
-      AccountState uploader = accountCache.get(patchSet.getUploader());
-      List<String> args = new ArrayList<>();
-      ChangeAttribute c = eventFactory.asChangeAttribute(change);
-      PatchSetAttribute ps = patchSetAttribute(change, patchSet);
-
-      addArg(args, "--change", c.id);
-      addArg(args, "--is-draft", String.valueOf(patchSet.isDraft()));
-      addArg(args, "--kind", String.valueOf(ps.kind));
-      addArg(args, "--change-url", c.url);
-      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-      addArg(args, "--project", c.project);
-      addArg(args, "--branch", c.branch);
-      addArg(args, "--topic", c.topic);
-      addArg(args, "--uploader", getDisplayName(uploader.getAccount()));
-      addArg(args, "--commit", ps.revision);
-      addArg(args, "--patchset", ps.number);
-
-      runHook(change.getProject(), patchsetCreatedHook, args);
-    }
-
-    @Override
-    public void doDraftPublishedHook(Change change, PatchSet patchSet,
-          ReviewDb db) throws OrmException {
-      if (!draftPublishedHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      ChangeAttribute c = eventFactory.asChangeAttribute(change);
-      PatchSetAttribute ps = patchSetAttribute(change, patchSet);
-      AccountState owner = accountCache.get(change.getOwner());
-      AccountState uploader = accountCache.get(patchSet.getUploader());
-
-      addArg(args, "--change", c.id);
-      addArg(args, "--change-url", c.url);
-      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-      addArg(args, "--project", c.project);
-      addArg(args, "--branch", c.branch);
-      addArg(args, "--topic", c.topic);
-      addArg(args, "--uploader", getDisplayName(uploader.getAccount()));
-      addArg(args, "--commit", ps.revision);
-      addArg(args, "--patchset", ps.number);
-
-      runHook(change.getProject(), draftPublishedHook, args);
-    }
-
-    @Override
-    public void doCommentAddedHook(final Change change, Account account,
-          PatchSet patchSet, String comment, final Map<String, Short> approvals,
-          final Map<String, Short> oldApprovals, ReviewDb db)
-              throws OrmException {
-      if (!commentAddedHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      ChangeAttribute c = eventFactory.asChangeAttribute(change);
-      PatchSetAttribute ps = patchSetAttribute(change, patchSet);
-      AccountState owner = accountCache.get(change.getOwner());
-
-      addArg(args, "--change", c.id);
-      addArg(args, "--is-draft", patchSet.isDraft() ? "true" : "false");
-      addArg(args, "--change-url", c.url);
-      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-      addArg(args, "--project", c.project);
-      addArg(args, "--branch", c.branch);
-      addArg(args, "--topic", c.topic);
-      addArg(args, "--author", getDisplayName(account));
-      addArg(args, "--commit", ps.revision);
-      addArg(args, "--comment", comment == null ? "" : comment);
-      LabelTypes labelTypes = projectCache.get(
-          change.getProject()).getLabelTypes();
-      for (Map.Entry<String, Short> approval : approvals.entrySet()) {
-        LabelType lt = labelTypes.byLabel(approval.getKey());
-        if (lt != null && approval.getValue() != null) {
-          addArg(args, "--" + lt.getName(), Short.toString(approval.getValue()));
-          if (oldApprovals != null && !oldApprovals.isEmpty()) {
-            Short oldValue = oldApprovals.get(approval.getKey());
-            if (oldValue != null) {
-              addArg(args, "--" + lt.getName() + "-oldValue",
-                  Short.toString(oldValue));
-            }
-          }
-        }
-      }
-      runHook(change.getProject(), commentAddedHook, args);
-    }
-
-    @Override
-    public void doChangeMergedHook(Change change, Account account,
-        PatchSet patchSet, ReviewDb db, String mergeResultRev)
-        throws OrmException {
-      if (!changeMergedHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      ChangeAttribute c = eventFactory.asChangeAttribute(change);
-      PatchSetAttribute ps = patchSetAttribute(change, patchSet);
-      AccountState owner = accountCache.get(change.getOwner());
-
-      addArg(args, "--change", c.id);
-      addArg(args, "--change-url", c.url);
-      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-      addArg(args, "--project", c.project);
-      addArg(args, "--branch", c.branch);
-      addArg(args, "--topic", c.topic);
-      addArg(args, "--submitter", getDisplayName(account));
-      addArg(args, "--commit", ps.revision);
-      addArg(args, "--newrev", mergeResultRev);
-
-      runHook(change.getProject(), changeMergedHook, args);
-    }
-
-    @Override
-    public void doChangeAbandonedHook(Change change, Account account,
-          PatchSet patchSet, String reason, ReviewDb db)
-          throws OrmException {
-      if (!changeAbandonedHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      ChangeAttribute c = eventFactory.asChangeAttribute(change);
-      PatchSetAttribute ps = patchSetAttribute(change, patchSet);
-      AccountState owner = accountCache.get(change.getOwner());
-
-      addArg(args, "--change", c.id);
-      addArg(args, "--change-url", c.url);
-      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-      addArg(args, "--project", c.project);
-      addArg(args, "--branch", c.branch);
-      addArg(args, "--topic", c.topic);
-      addArg(args, "--abandoner", getDisplayName(account));
-      addArg(args, "--commit", ps.revision);
-      addArg(args, "--reason", reason == null ? "" : reason);
-
-      runHook(change.getProject(), changeAbandonedHook, args);
-    }
-
-    @Override
-    public void doChangeRestoredHook(Change change, Account account,
-          PatchSet patchSet, String reason, ReviewDb db)
-          throws OrmException {
-      if (!changeRestoredHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      ChangeAttribute c = eventFactory.asChangeAttribute(change);
-      PatchSetAttribute ps = patchSetAttribute(change, patchSet);
-      AccountState owner = accountCache.get(change.getOwner());
-
-      addArg(args, "--change", c.id);
-      addArg(args, "--change-url", c.url);
-      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-      addArg(args, "--project", c.project);
-      addArg(args, "--branch", c.branch);
-      addArg(args, "--topic", c.topic);
-      addArg(args, "--restorer", getDisplayName(account));
-      addArg(args, "--commit", ps.revision);
-      addArg(args, "--reason", reason == null ? "" : reason);
-
-      runHook(change.getProject(), changeRestoredHook, args);
-    }
-
-    @Override
-    public void doRefUpdatedHook(final Branch.NameKey refName,
-        final ObjectId oldId, final ObjectId newId, Account account) {
-      if (!refUpdatedHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      RefUpdateAttribute r =
-          eventFactory.asRefUpdateAttribute(oldId, newId, refName);
-      addArg(args, "--oldrev", r.oldRev);
-      addArg(args, "--newrev", r.newRev);
-      addArg(args, "--refname", r.refName);
-      addArg(args, "--project", r.project);
-      if (account != null) {
-        addArg(args, "--submitter", getDisplayName(account));
-      }
-
-      runHook(refName.getParentKey(), refUpdatedHook, args);
-    }
-
-    @Override
-    public void doReviewerAddedHook(Change change, Account account,
-        PatchSet patchSet, ReviewDb db) throws OrmException {
-      if (!reviewerAddedHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      ChangeAttribute c = eventFactory.asChangeAttribute(change);
-      AccountState owner = accountCache.get(change.getOwner());
-
-      addArg(args, "--change", c.id);
-      addArg(args, "--change-url", c.url);
-      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-      addArg(args, "--project", c.project);
-      addArg(args, "--branch", c.branch);
-      addArg(args, "--reviewer", getDisplayName(account));
-
-      runHook(change.getProject(), reviewerAddedHook, args);
-    }
-
-    @Override
-    public void doReviewerDeletedHook(final Change change, Account account,
-      PatchSet patchSet, String comment, final Map<String, Short> approvals,
-      final Map<String, Short> oldApprovals, ReviewDb db) throws OrmException {
-      if (!reviewerDeletedHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      ChangeAttribute c = eventFactory.asChangeAttribute(change);
-      AccountState owner = accountCache.get(change.getOwner());
-
-      addArg(args, "--change", c.id);
-      addArg(args, "--change-url", c.url);
-      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-      addArg(args, "--project", c.project);
-      addArg(args, "--branch", c.branch);
-      addArg(args, "--reviewer", getDisplayName(account));
-      LabelTypes labelTypes = projectCache.get(
-          change.getProject()).getLabelTypes();
-      // append votes that were removed
-      for (Map.Entry<String, Short> approval : approvals.entrySet()) {
-        LabelType lt = labelTypes.byLabel(approval.getKey());
-        if (lt != null && approval.getValue() != null) {
-          addArg(args, "--" + lt.getName(), Short.toString(approval.getValue()));
-          if (oldApprovals != null && !oldApprovals.isEmpty()) {
-            Short oldValue = oldApprovals.get(approval.getKey());
-            if (oldValue != null) {
-              addArg(args, "--" + lt.getName() + "-oldValue",
-                  Short.toString(oldValue));
-            }
-          }
-        }
-      }
-      runHook(change.getProject(), reviewerDeletedHook, args);
-    }
-
-    @Override
-    public void doTopicChangedHook(Change change, Account account,
-        String oldTopic, ReviewDb db)
-            throws OrmException {
-      if (!topicChangedHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      ChangeAttribute c = eventFactory.asChangeAttribute(change);
-      AccountState owner = accountCache.get(change.getOwner());
-
-      addArg(args, "--change", c.id);
-      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-      addArg(args, "--project", c.project);
-      addArg(args, "--branch", c.branch);
-      addArg(args, "--changer", getDisplayName(account));
-      addArg(args, "--old-topic", oldTopic);
-      addArg(args, "--new-topic", c.topic);
-
-      runHook(change.getProject(), topicChangedHook, args);
-    }
-
-    String[] hashtagArray(Set<String> hashtags) {
-      if (hashtags != null && hashtags.size() > 0) {
-        return Sets.newHashSet(hashtags).toArray(
-            new String[hashtags.size()]);
-      }
-      return null;
-    }
-
-    @Override
-    public void doHashtagsChangedHook(Change change, Account account,
-        Set<String> added, Set<String> removed, Set<String> hashtags, ReviewDb db)
-            throws OrmException {
-      if (!hashtagsChangedHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      ChangeAttribute c = eventFactory.asChangeAttribute(change);
-      AccountState owner = accountCache.get(change.getOwner());
-
-      addArg(args, "--change", c.id);
-      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-      addArg(args, "--project", c.project);
-      addArg(args, "--branch", c.branch);
-      addArg(args, "--editor", getDisplayName(account));
-      if (hashtags != null) {
-        for (String hashtag : hashtags) {
-          addArg(args, "--hashtag", hashtag);
-        }
-      }
-      if (added != null) {
-        for (String hashtag : added) {
-          addArg(args, "--added", hashtag);
-        }
-      }
-      if (removed != null) {
-        for (String hashtag : removed) {
-          addArg(args, "--removed", hashtag);
-        }
-      }
-      runHook(change.getProject(), hashtagsChangedHook, args);
-    }
-
-    @Override
-    public void doClaSignupHook(Account account, String claName) {
-      if (!claSignedHook.isPresent()) {
-        return;
-      }
-
-      if (account != null) {
-        List<String> args = new ArrayList<>();
-        addArg(args, "--submitter", getDisplayName(account));
-        addArg(args, "--user-id", account.getId().toString());
-        addArg(args, "--cla-name", claName);
-
-        runHook(claSignedHook, args);
-      }
-    }
-
-    private PatchSetAttribute patchSetAttribute(Change change,
-        PatchSet patchSet) {
-      try (Repository repo =
-            repoManager.openRepository(change.getProject());
-          RevWalk revWalk = new RevWalk(repo)) {
-        return eventFactory.asPatchSetAttribute(
-            revWalk, change, patchSet);
-      } catch (IOException e) {
-        throw new RuntimeException(e);
-      }
-    }
-
-    /**
-     * Get the display name for the given account.
-     *
-     * @param account Account to get name for.
-     * @return Name for this account.
-     */
-    private String getDisplayName(Account account) {
-      if (account != null) {
-        String result = (account.getFullName() == null)
-            ? anonymousCowardName
-            : account.getFullName();
-        if (account.getPreferredEmail() != null) {
-          result += " (" + account.getPreferredEmail() + ")";
-        }
-        return result;
-      }
-
-      return anonymousCowardName;
-    }
-
-  /**
-   * Run a hook.
-   *
-   * @param project used to open repository to run the hook for.
-   * @param hook the hook to execute.
-   * @param args Arguments to use to run the hook.
-   */
-  private synchronized void runHook(Project.NameKey project, Optional<Path> hook,
-      List<String> args) {
-    if (project != null && hook.isPresent()) {
-      hookQueue.execute(new AsyncHookTask(project, hook.get(), args));
-    }
-  }
-
-  private synchronized void runHook(Optional<Path> hook, List<String> args) {
-    if (hook.isPresent()) {
-      hookQueue.execute(new AsyncHookTask(null, hook.get(), args));
-    }
-  }
-
-  private HookResult runSyncHook(Project.NameKey project,
-      Optional<Path> hook, List<String> args) {
-
-    if (!hook.isPresent()) {
-      return null;
-    }
-
-    SyncHookTask syncHook = new SyncHookTask(project, hook.get(), args);
-    FutureTask<HookResult> task = new FutureTask<>(syncHook);
-
-    syncHookThreadPool.execute(task);
-
-    String message;
-
-    try {
-      return task.get(syncHookTimeout, TimeUnit.SECONDS);
-    } catch (TimeoutException e) {
-      message = "Synchronous hook timed out "  + hook.get().toAbsolutePath();
-      log.error(message);
-    } catch (Exception e) {
-      message = "Error running hook " + hook.get().toAbsolutePath();
-      log.error(message, e);
-    }
-
-    task.cancel(true);
-    syncHook.cancel();
-    return  new HookResult(syncHook.getOutput(), message);
-  }
-
-  @Override
-  public void start() {
-  }
-
-  @Override
-  public void stop() {
-    syncHookThreadPool.shutdown();
-    boolean isTerminated;
-    do {
-      try {
-        isTerminated = syncHookThreadPool.awaitTermination(10, TimeUnit.SECONDS);
-      } catch (InterruptedException ie) {
-        isTerminated = false;
-      }
-    } while (!isTerminated);
-  }
-
-  private class HookTask {
-    private final Project.NameKey project;
-    private final Path hook;
-    private final List<String> args;
-    private StringWriter output;
-    private Process ps;
-
-    protected HookTask(Project.NameKey project, Path hook, List<String> args) {
-      this.project = project;
-      this.hook = hook;
-      this.args = args;
-    }
-
-    public String getOutput() {
-      return output != null ? output.toString() : null;
-    }
-
-    protected HookResult runHook() {
-      Repository repo = null;
-      HookResult result = null;
-      try {
-
-        List<String> argv = new ArrayList<>(1 + args.size());
-        argv.add(hook.toAbsolutePath().toString());
-        argv.addAll(args);
-
-        ProcessBuilder pb = new ProcessBuilder(argv);
-        pb.redirectErrorStream(true);
-
-        if (project != null) {
-          repo = openRepository(project);
-        }
-
-        Map<String, String> env = pb.environment();
-        env.put("GERRIT_SITE", sitePaths.site_path.toAbsolutePath().toString());
-
-        if (repo != null) {
-          pb.directory(repo.getDirectory());
-
-          env.put("GIT_DIR", repo.getDirectory().getAbsolutePath());
-        }
-
-        ps = pb.start();
-        ps.getOutputStream().close();
-        String output = null;
-        try (InputStream is = ps.getInputStream()) {
-          output = readOutput(is);
-        } finally {
-          ps.waitFor();
-          result = new HookResult(ps.exitValue(), output);
-        }
-      } catch (InterruptedException iex) {
-        // InterruptedExeception - timeout or cancel
-      } catch (Throwable err) {
-        log.error("Error running hook " + hook.toAbsolutePath(), err);
-      } finally {
-        if (repo != null) {
-          repo.close();
-        }
-      }
-
-      if (result != null) {
-        int exitValue = result.getExitValue();
-        if (exitValue == 0) {
-          log.debug("hook[" + getName() + "] exitValue:" + exitValue);
-        } else {
-          log.info("hook[" + getName() + "] exitValue:" + exitValue);
-        }
-
-        BufferedReader br =
-            new BufferedReader(new StringReader(result.getOutput()));
-        try {
-          String line;
-          while ((line = br.readLine()) != null) {
-            log.info("hook[" + getName() + "] output: " + line);
-          }
-        } catch (IOException iox) {
-          log.error("Error writing hook output", iox);
-        }
-      }
-
-      return result;
-    }
-
-    private String readOutput(InputStream is) throws IOException {
-      output = new StringWriter();
-      InputStreamReader input = new InputStreamReader(is);
-      char[] buffer = new char[4096];
-      int n;
-      while ((n = input.read(buffer)) != -1) {
-        output.write(buffer, 0, n);
-      }
-
-      return output.toString();
-    }
-
-    protected String getName() {
-      return hook.getFileName().toString();
-    }
-
-    @Override
-    public String toString() {
-      return "hook " + hook.getFileName();
-    }
-
-    public void cancel() {
-      ps.destroy();
-    }
-  }
-
-  /** Callable type used to run synchronous hooks */
-  private final class SyncHookTask extends HookTask
-      implements Callable<HookResult> {
-
-    private SyncHookTask(Project.NameKey project, Path hook, List<String> args) {
-      super(project, hook, args);
-    }
-
-    @Override
-    public HookResult call() throws Exception {
-      return super.runHook();
-    }
-  }
-
-  /** Runnable type used to run asynchronous hooks */
-  private final class AsyncHookTask extends HookTask implements Runnable {
-
-    private AsyncHookTask(Project.NameKey project, Path hook, List<String> args) {
-      super(project, hook, args);
-    }
-
-    @Override
-    public void run() {
-      super.runHook();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
deleted file mode 100644
index b6b4971..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ /dev/null
@@ -1,203 +0,0 @@
-// Copyright (C) 2012 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.common;
-
-import com.google.gerrit.common.ChangeHookRunner.HookResult;
-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.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-
-import org.eclipse.jgit.lib.ObjectId;
-
-import java.util.Map;
-import java.util.Set;
-
-/** Invokes hooks on server actions. */
-public interface ChangeHooks {
-  /**
-   * Fire the Patchset Created Hook.
-   *
-   * @param change The change itself.
-   * @param patchSet The Patchset that was created.
-   * @param db The review database.
-   * @throws OrmException
-   */
-  void doPatchsetCreatedHook(Change change, PatchSet patchSet,
-      ReviewDb db) throws OrmException;
-
-  /**
-   * Fire the Draft Published Hook.
-   *
-   * @param change The change itself.
-   * @param patchSet The Patchset that was published.
-   * @param db The review database.
-   * @throws OrmException
-   */
-  void doDraftPublishedHook(Change change, PatchSet patchSet,
-      ReviewDb db) throws OrmException;
-
-  /**
-   * Fire the Comment Added Hook.
-   *
-   * @param change The change itself.
-   * @param account The gerrit user who added the comment.
-   * @param patchSet The patchset this comment is related to.
-   * @param comment The comment given.
-   * @param approvals Map of label IDs to scores
-   * @param oldApprovals Map of label IDs to old approval scores
-   * @param db The review database.
-   * @throws OrmException
-   */
-  void doCommentAddedHook(Change change, Account account,
-      PatchSet patchSet, String comment,
-      Map<String, Short> approvals, Map<String, Short> oldApprovals,
-      ReviewDb db)
-      throws OrmException;
-
-  /**
-   * Fire the Change Merged Hook.
-   *
-   * @param change The change itself.
-   * @param account The gerrit user who submitted the change.
-   * @param patchSet The patchset that was merged.
-   * @param db The review database.
-   * @param mergeResultRev The SHA-1 of the merge result revision.
-   * @throws OrmException
-   */
-  void doChangeMergedHook(Change change, Account account,
-      PatchSet patchSet, ReviewDb db, String mergeResultRev) throws OrmException;
-
-  /**
-   * Fire the Change Abandoned Hook.
-   *
-   * @param change The change itself.
-   * @param account The gerrit user who abandoned the change.
-   * @param reason Reason for abandoning the change.
-   * @param db The review database.
-   * @throws OrmException
-   */
-  void doChangeAbandonedHook(Change change, Account account,
-      PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
-
-  /**
-   * Fire the Change Restored Hook.
-   *
-   * @param change The change itself.
-   * @param account The gerrit user who restored the change.
-   * @param patchSet The patchset that was restored.
-   * @param reason Reason for restoring the change.
-   * @param db The review database.
-   * @throws OrmException
-   */
-  void doChangeRestoredHook(Change change, Account account,
-      PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
-
-  /**
-   * Fire the Ref Updated Hook.
-   *
-   * @param refName The Branch.NameKey of the ref that was updated.
-   * @param oldId The ref's old id.
-   * @param newId The ref's new id.
-   * @param account The gerrit user who moved the ref.
-   */
-  void doRefUpdatedHook(Branch.NameKey refName, ObjectId oldId,
-      ObjectId newId, Account account);
-
-  /**
-   * Fire the Reviewer Added Hook.
-   *
-   * @param change The change itself.
-   * @param patchSet The patchset that the reviewer was added on.
-   * @param account The gerrit user who was added as reviewer.
-   * @param db The review database.
-   */
-  void doReviewerAddedHook(Change change, Account account,
-      PatchSet patchSet, ReviewDb db) throws OrmException;
-
-  /**
-   * Fire the Reviewer Deleted Hook
-   *
-   * @param change The change itself.
-   * @param account The reviewer that was removed.
-   * @param patchSet The patchset that the reviewer was removed from.
-   * @param comment The comment given.
-   * @param approvals Map of label IDs to scores.
-   * @param oldApprovals Map of label IDs to old approval scores
-   * @param db The review database.
-   * @throws OrmException
-   */
-  void doReviewerDeletedHook(Change change, Account account, PatchSet patchSet,
-      String comment, Map<String, Short> approvals,
-      Map<String, Short> oldApprovals, ReviewDb db) throws OrmException;
-
-  /**
-   * Fire the Topic Changed Hook
-   *
-   * @param change The change itself.
-   * @param account The gerrit user who changed the topic.
-   * @param oldTopic The old topic name.
-   * @param db The review database.
-   */
-  void doTopicChangedHook(Change change, Account account,
-      String oldTopic, ReviewDb db) throws OrmException;
-
-  /**
-   * Fire the contributor license agreement signup hook.
-   *
-   * @param account The gerrit user who signed the contributor license
-   *        agreement.
-   * @param claName The name of the contributor license agreement.
-   */
-  void doClaSignupHook(Account account, String claName);
-
-  /**
-   * Fire the Ref update Hook.
-   *
-   * @param project The target project.
-   * @param refName The Branch.NameKey of the ref provided by client.
-   * @param uploader The gerrit user running the command.
-   * @param oldId The ref's old id.
-   * @param newId The ref's new id.
-   */
-  HookResult doRefUpdateHook(Project project,  String refName,
-       Account uploader, ObjectId oldId, ObjectId newId);
-
-  /**
-   * Fire the hashtags changed Hook.
-   *
-   * @param change The change itself.
-   * @param account The gerrit user changing the hashtags.
-   * @param added List of hashtags that were added to the change.
-   * @param removed List of hashtags that were removed from the change.
-   * @param hashtags List of hashtags on the change after adding or removing.
-   * @param db The review database.
-   * @throws OrmException
-   */
-  void doHashtagsChangedHook(Change change, Account account,
-      Set<String>added, Set<String> removed, Set<String> hashtags,
-      ReviewDb db) throws OrmException;
-
-  /**
-   * Fire the project created hook.
-   *
-   * @param project The project that was created.
-   * @param headName The head name of the created project.
-   */
-  void doProjectCreatedHook(Project.NameKey project, String headName);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
deleted file mode 100644
index 33661ce..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (C) 2012 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.common;
-
-import com.google.gerrit.common.ChangeHookRunner.HookResult;
-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.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-
-import org.eclipse.jgit.lib.ObjectId;
-
-import java.util.Map;
-import java.util.Set;
-
-/** Does not invoke hooks. */
-public final class DisabledChangeHooks implements ChangeHooks {
-  @Override
-  public void doChangeAbandonedHook(Change change, Account account,
-      PatchSet patchSet, String reason, ReviewDb db) {
-  }
-
-  @Override
-  public void doChangeMergedHook(Change change, Account account,
-      PatchSet patchSet, ReviewDb db, String mergeResultRev) {
-  }
-
-  @Override
-  public void doChangeRestoredHook(Change change, Account account,
-      PatchSet patchSet, String reason, ReviewDb db) {
-  }
-
-  @Override
-  public void doClaSignupHook(Account account, String claName) {
-  }
-
-  @Override
-  public void doCommentAddedHook(Change change, Account account,
-      PatchSet patchSet, String comment,
-      Map<String, Short> approvals, Map<String, Short> oldApprovals,
-      ReviewDb db) {
-  }
-
-  @Override
-  public void doPatchsetCreatedHook(Change change, PatchSet patchSet,
-      ReviewDb db) {
-  }
-
-  @Override
-  public void doDraftPublishedHook(Change change, PatchSet patchSet,
-      ReviewDb db) {
-  }
-
-  @Override
-  public void doRefUpdatedHook(Branch.NameKey refName, ObjectId oldId,
-      ObjectId newId, Account account) {
-  }
-
-  @Override
-  public void doReviewerAddedHook(Change change, Account account, PatchSet patchSet,
-      ReviewDb db) {
-  }
-
-  @Override
-  public void doReviewerDeletedHook(Change change, Account account,
-      PatchSet patchSet, String comment, Map<String, Short> approvals,
-      Map<String, Short> oldApprovals, ReviewDb db) {
-  }
-
-  @Override
-  public void doTopicChangedHook(Change change, Account account, String oldTopic,
-      ReviewDb db) {
-  }
-
-  @Override
-  public void doHashtagsChangedHook(Change change, Account account, Set<String> added,
-      Set<String> removed, Set<String> hashtags, ReviewDb db) {
-  }
-
-  @Override
-  public HookResult doRefUpdateHook(Project project, String refName,
-      Account uploader, ObjectId oldId, ObjectId newId) {
-    return null;
-  }
-
-  @Override
-  public void doProjectCreatedHook(Project.NameKey project, String headName) {
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 9034e47..c493ccd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -100,10 +100,9 @@
       PatchListCache plCache = env.getArgs().getPatchListCache();
       Change change = getChange(engine);
       Project.NameKey project = change.getProject();
-      ObjectId a = null;
       ObjectId b = ObjectId.fromString(ps.getRevision().get());
       Whitespace ws = Whitespace.IGNORE_NONE;
-      PatchListKey plKey = new PatchListKey(a, b, ws);
+      PatchListKey plKey = PatchListKey.againstDefaultBase(b, ws);
       PatchList patchList;
       try {
         patchList = plCache.get(plKey, project);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index a3a7734..d69ad3f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -44,9 +44,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.NavigableSet;
-import java.util.Objects;
-import java.util.SortedSet;
 import java.util.TreeMap;
 
 /**
@@ -80,22 +77,54 @@
     this.psUtil = psUtil;
   }
 
+  /**
+   * Apply approval copy settings from prior PatchSets to a new PatchSet.
+   *
+   * @param db review database.
+   * @param ctl change control for user uploading PatchSet
+   * @param ps new PatchSet
+   * @throws OrmException
+   */
   public void copy(ReviewDb db, ChangeControl ctl, PatchSet ps)
       throws OrmException {
-    db.patchSetApprovals().insert(getForPatchSet(db, ctl, ps));
+    copy(db, ctl, ps, Collections.<PatchSetApproval>emptyList());
+  }
+
+  /**
+   * Apply approval copy settings from prior PatchSets to a new PatchSet.
+   *
+   * @param db review database.
+   * @param ctl change control for user uploading PatchSet
+   * @param ps new PatchSet
+   * @param dontCopy PatchSetApprovals indicating which (account, label) pairs
+   *        should not be copied
+   * @throws OrmException
+   */
+  public void copy(ReviewDb db, ChangeControl ctl, PatchSet ps,
+      Iterable<PatchSetApproval> dontCopy) throws OrmException {
+    db.patchSetApprovals().insert(
+        getForPatchSet(db, ctl, ps, dontCopy));
   }
 
   Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
       ChangeControl ctl, PatchSet.Id psId) throws OrmException {
+    return getForPatchSet(db, ctl, psId,
+        Collections.<PatchSetApproval>emptyList());
+  }
+
+  Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
+      ChangeControl ctl, PatchSet.Id psId,
+      Iterable<PatchSetApproval> dontCopy) throws OrmException {
     PatchSet ps = psUtil.get(db, ctl.getNotes(), psId);
     if (ps == null) {
       return Collections.emptyList();
     }
-    return getForPatchSet(db, ctl, ps);
+    return getForPatchSet(db, ctl, ps, dontCopy);
   }
 
   private Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
-      ChangeControl ctl, PatchSet ps) throws OrmException {
+      ChangeControl ctl, PatchSet ps,
+      Iterable<PatchSetApproval> dontCopy) throws OrmException {
     checkNotNull(ps, "ps should not be null");
     ChangeData cd = changeDataFactory.create(db, ctl);
     try {
@@ -104,14 +133,21 @@
       ListMultimap<PatchSet.Id, PatchSetApproval> all = cd.approvals();
       checkNotNull(all, "all should not be null");
 
+      Table<String, Account.Id, PatchSetApproval> wontCopy =
+          HashBasedTable.create();
+      for (PatchSetApproval psa : dontCopy) {
+        wontCopy.put(psa.getLabel(), psa.getAccountId(), psa);
+      }
+
       Table<String, Account.Id, PatchSetApproval> byUser =
           HashBasedTable.create();
       for (PatchSetApproval psa : all.get(ps.getId())) {
-        byUser.put(psa.getLabel(), psa.getAccountId(), psa);
+        if (!wontCopy.contains(psa.getLabel(), psa.getAccountId())) {
+          byUser.put(psa.getLabel(), psa.getAccountId(), psa);
+        }
       }
 
       TreeMap<Integer, PatchSet> patchSets = getPatchSets(cd);
-      NavigableSet<Integer> allPsIds = patchSets.navigableKeySet();
 
       try (Repository repo =
           repoManager.openRepository(project.getProject().getNameKey())) {
@@ -130,11 +166,18 @@
               ObjectId.fromString(ps.getRevision().get()));
 
           for (PatchSetApproval psa : priorApprovals) {
-            if (!byUser.contains(psa.getLabel(), psa.getAccountId())
-                && canCopy(project, psa, ps.getId(), allPsIds, kind)) {
-              byUser.put(psa.getLabel(), psa.getAccountId(),
-                  copy(psa, ps.getId()));
+            if (wontCopy.contains(psa.getLabel(), psa.getAccountId())) {
+              continue;
             }
+            if (byUser.contains(psa.getLabel(), psa.getAccountId())) {
+              continue;
+            }
+            if (!canCopy(project, psa, ps.getId(), kind)) {
+              wontCopy.put(psa.getLabel(), psa.getAccountId(), psa);
+              continue;
+            }
+            byUser.put(psa.getLabel(), psa.getAccountId(),
+                copy(psa, ps.getId()));
           }
         }
         return labelNormalizer.normalize(ctl, byUser.values()).getNormalized();
@@ -155,17 +198,15 @@
   }
 
   private static boolean canCopy(ProjectState project, PatchSetApproval psa,
-      PatchSet.Id psId, NavigableSet<Integer> allPsIds, ChangeKind kind) {
+      PatchSet.Id psId, ChangeKind kind) {
     int n = psa.getKey().getParentKey().get();
     checkArgument(n != psId.get());
     LabelType type = project.getLabelTypes().byLabel(psa.getLabelId());
     if (type == null) {
       return false;
-    } else if (Objects.equals(n, previous(allPsIds, psId.get())) && (
-        type.isCopyMinScore() && type.isMaxNegative(psa)
-        || type.isCopyMaxScore() && type.isMaxPositive(psa))) {
-      // Copy min/max score only from the immediately preceding patch set (which
-      // may not be psId.get() - 1).
+    } else if (
+        (type.isCopyMinScore() && type.isMaxNegative(psa))
+        || (type.isCopyMaxScore() && type.isMaxPositive(psa))) {
       return true;
     }
     switch (kind) {
@@ -192,9 +233,4 @@
     }
     return new PatchSetApproval(psId, src);
   }
-
-  private static <T> T previous(NavigableSet<T> s, T v) {
-    SortedSet<T> head = s.headSet(v);
-    return !head.isEmpty() ? head.last() : null;
-  }
-}
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 847d559..5a5d16c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server;
 
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -40,17 +41,23 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -70,13 +77,18 @@
  */
 @Singleton
 public class ApprovalsUtil {
-  private static Ordering<PatchSetApproval> SORT_APPROVALS = Ordering.natural()
-      .onResultOf(new Function<PatchSetApproval, Timestamp>() {
-        @Override
-        public Timestamp apply(PatchSetApproval a) {
-          return a.getGranted();
-        }
-      });
+  private static final Logger log =
+      LoggerFactory.getLogger(ApprovalsUtil.class);
+
+  private static final Ordering<PatchSetApproval> SORT_APPROVALS =
+      Ordering.natural()
+          .onResultOf(
+              new Function<PatchSetApproval, Timestamp>() {
+                @Override
+                public Timestamp apply(PatchSetApproval a) {
+                  return a.getGranted();
+                }
+              });
 
   public static List<PatchSetApproval> sortApprovals(
       Iterable<PatchSetApproval> approvals) {
@@ -94,13 +106,19 @@
   }
 
   private final NotesMigration migration;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final ApprovalCopier copier;
 
   @VisibleForTesting
   @Inject
   public ApprovalsUtil(NotesMigration migration,
+      IdentifiedUser.GenericFactory userFactory,
+      ChangeControl.GenericFactory changeControlFactory,
       ApprovalCopier copier) {
     this.migration = migration;
+    this.userFactory = userFactory;
+    this.changeControlFactory = changeControlFactory;
     this.copier = copier;
   }
 
@@ -122,7 +140,7 @@
   }
 
   /**
-   * Get all reviewers for a change.
+   * Get all reviewers and CCed accounts for a change.
    *
    * @param allApprovals all approvals to consider; must all belong to the same
    *     change.
@@ -138,25 +156,58 @@
     return notes.load().getReviewers();
   }
 
+  /**
+   * Get updates to reviewer set.
+   * Always returns empty list for ReviewDb.
+   *
+   * @param notes change notes.
+   * @return reviewer updates for the change.
+   * @throws OrmException if reviewer updates for the change could not be read.
+   */
+  public List<ReviewerStatusUpdate> getReviewerUpdates(ChangeNotes notes)
+      throws OrmException {
+    if (!migration.readChanges()) {
+      return ImmutableList.of();
+    }
+    return notes.load().getReviewerUpdates();
+  }
+
   public List<PatchSetApproval> addReviewers(ReviewDb db,
       ChangeUpdate update, LabelTypes labelTypes, Change change, PatchSet ps,
       PatchSetInfo info, Iterable<Account.Id> wantReviewers,
       Collection<Account.Id> existingReviewers) throws OrmException {
     return addReviewers(db, update, labelTypes, change, ps.getId(),
-        ps.isDraft(), info.getAuthor().getAccount(),
-        info.getCommitter().getAccount(), wantReviewers, existingReviewers);
+        info.getAuthor().getAccount(), info.getCommitter().getAccount(),
+        wantReviewers, existingReviewers);
   }
 
   public List<PatchSetApproval> addReviewers(ReviewDb db, ChangeNotes notes,
       ChangeUpdate update, LabelTypes labelTypes, Change change,
       Iterable<Account.Id> wantReviewers) throws OrmException {
     PatchSet.Id psId = change.currentPatchSetId();
-    return addReviewers(db, update, labelTypes, change, psId, false, null, null,
-        wantReviewers, getReviewers(db, notes).all());
+    Collection<Account.Id> existingReviewers;
+    if (migration.readChanges()) {
+      // If using NoteDB, we only want reviewers in the REVIEWER state.
+      existingReviewers = notes.load().getReviewers().byState(REVIEWER);
+    } else {
+      // Prior to NoteDB, we gather all reviewers regardless of state.
+      existingReviewers = getReviewers(db, notes).all();
+    }
+    // Existing reviewers should include pending additions in the REVIEWER
+    // state, taken from ChangeUpdate.
+    existingReviewers = Lists.newArrayList(existingReviewers);
+    for (Map.Entry<Account.Id, ReviewerStateInternal> entry :
+        update.getReviewers().entrySet()) {
+      if (entry.getValue() == REVIEWER) {
+        existingReviewers.add(entry.getKey());
+      }
+    }
+    return addReviewers(db, update, labelTypes, change, psId, null, null,
+        wantReviewers, existingReviewers);
   }
 
   private List<PatchSetApproval> addReviewers(ReviewDb db, ChangeUpdate update,
-      LabelTypes labelTypes, Change change, PatchSet.Id psId, boolean isDraft,
+      LabelTypes labelTypes, Change change, PatchSet.Id psId,
       Account.Id authorId, Account.Id committerId,
       Iterable<Account.Id> wantReviewers,
       Collection<Account.Id> existingReviewers) throws OrmException {
@@ -166,11 +217,11 @@
     }
 
     Set<Account.Id> need = Sets.newLinkedHashSet(wantReviewers);
-    if (authorId != null && !isDraft) {
+    if (authorId != null && canSee(db, update.getNotes(), authorId)) {
       need.add(authorId);
     }
 
-    if (committerId != null && !isDraft) {
+    if (committerId != null && canSee(db, update.getNotes(), authorId)) {
       need.add(committerId);
     }
     need.remove(change.getOwner());
@@ -191,25 +242,76 @@
     return Collections.unmodifiableList(cells);
   }
 
-  public void addApprovals(ReviewDb db, ChangeUpdate update,
+  private boolean canSee(ReviewDb db, ChangeNotes notes, Account.Id accountId) {
+    try {
+      IdentifiedUser user = userFactory.create(accountId);
+      return changeControlFactory.controlFor(notes, user).isVisible(db);
+    } catch (OrmException | NoSuchChangeException e) {
+      log.warn(String.format("Failed to check if account %d can see change %d",
+          accountId.get(), notes.getChangeId().get()), e);
+      return false;
+    }
+  }
+
+  /**
+   * Adds accounts to a change as reviewers in the CC state.
+   *
+   * @param notes change notes.
+   * @param update change update.
+   * @param wantCCs accounts to CC.
+   * @return whether a change was made.
+   * @throws OrmException
+   */
+  public Collection<Account.Id> addCcs(ChangeNotes notes, ChangeUpdate update,
+      Collection<Account.Id> wantCCs) throws OrmException {
+    return addCcs(update, wantCCs, notes.load().getReviewers());
+  }
+
+  private Collection<Account.Id> addCcs(ChangeUpdate update,
+      Collection<Account.Id> wantCCs, ReviewerSet existingReviewers) {
+    Set<Account.Id> need = new LinkedHashSet<>(wantCCs);
+    need.removeAll(existingReviewers.all());
+    need.removeAll(update.getReviewers().keySet());
+    for (Account.Id account : need) {
+      update.putReviewer(account, CC);
+    }
+    return need;
+  }
+
+  /**
+   * Adds approvals to ChangeUpdate and writes to ReviewDb.
+   *
+   * @param db review database.
+   * @param update change update.
+   * @param labelTypes label types for the containing project.
+   * @param ps patch set being approved.
+   * @param changeCtl change control for user adding approvals.
+   * @param approvals approvals to add.
+   * @throws OrmException
+   */
+  public Iterable<PatchSetApproval> addApprovals(ReviewDb db, ChangeUpdate update,
       LabelTypes labelTypes, PatchSet ps, ChangeControl changeCtl,
       Map<String, Short> approvals) throws OrmException {
-    if (!approvals.isEmpty()) {
-      checkApprovals(approvals, changeCtl);
-      List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
-      Date ts = update.getWhen();
-      for (Map.Entry<String, Short> vote : approvals.entrySet()) {
-        LabelType lt = labelTypes.byLabel(vote.getKey());
-        cells.add(new PatchSetApproval(new PatchSetApproval.Key(
-            ps.getId(),
-            ps.getUploader(),
-            lt.getLabelId()),
-            vote.getValue(),
-            ts));
-        update.putApproval(vote.getKey(), vote.getValue());
-      }
-      db.patchSetApprovals().insert(cells);
+    if (approvals.isEmpty()) {
+      return Collections.emptyList();
     }
+    checkApprovals(approvals, changeCtl);
+    List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
+    Date ts = update.getWhen();
+    for (Map.Entry<String, Short> vote : approvals.entrySet()) {
+      LabelType lt = labelTypes.byLabel(vote.getKey());
+      cells.add(new PatchSetApproval(new PatchSetApproval.Key(
+          ps.getId(),
+          ps.getUploader(),
+          lt.getLabelId()),
+          vote.getValue(),
+          ts));
+    }
+    for (PatchSetApproval psa : cells) {
+      update.putApproval(psa.getLabel(), psa.getValue());
+    }
+    db.patchSetApprovals().insert(cells);
+    return cells;
   }
 
   public static void checkLabel(LabelTypes labelTypes, String name, Short value) {
@@ -317,6 +419,6 @@
             .append(LabelVote.create(e.getKey(), e.getValue()).format());
       }
     }
-    return msgs.append('.').toString();
+    return msgs.toString();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
index d990115..603f528 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
@@ -365,9 +365,17 @@
         "cannot set RevId for patch set %s on comment %s", ps.getId(), c);
     if (c.getRevId() == null) {
       try {
-        c.setRevId(Side.fromShort(c.getSide()) == Side.PARENT
-            ? new RevId(ObjectId.toString(cache.getOldId(change, ps)))
-            : ps.getRevision());
+        if (Side.fromShort(c.getSide()) == Side.PARENT) {
+          if (c.getSide() < 0) {
+            c.setRevId(new RevId(ObjectId.toString(
+                cache.getOldId(change, ps, -c.getSide()))));
+          } else {
+            c.setRevId(new RevId(ObjectId.toString(
+                cache.getOldId(change, ps, null))));
+          }
+        } else {
+          c.setRevId(ps.getRevision());
+        }
       } catch (PatchListNotAvailableException e) {
         throw new OrmException(e);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerSet.java
index 515cef7..5e0f77b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerSet.java
@@ -54,11 +54,9 @@
             "multiple change IDs: %s, %s", first.getKey(), psa.getKey());
       }
       Account.Id id = psa.getAccountId();
+      reviewers.put(REVIEWER, id, psa.getGranted());
       if (psa.getValue() != 0) {
-        reviewers.put(REVIEWER, id, psa.getGranted());
         reviewers.remove(CC, id);
-      } else if (!reviewers.contains(REVIEWER, id)) {
-        reviewers.put(CC, id, psa.getGranted());
       }
     }
     return new ReviewerSet(reviewers);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerStatusUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerStatusUpdate.java
new file mode 100644
index 0000000..bbe4013
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerStatusUpdate.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2016 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;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
+
+import java.sql.Timestamp;
+
+/** Change to a reviewer's status. */
+@AutoValue
+public abstract class ReviewerStatusUpdate {
+  public static ReviewerStatusUpdate create(
+      Timestamp ts, Account.Id updatedBy, Account.Id reviewer,
+      ReviewerStateInternal state) {
+    return new AutoValue_ReviewerStatusUpdate(ts, updatedBy, reviewer, state);
+  }
+
+  public abstract Timestamp date();
+  public abstract Account.Id updatedBy();
+  public abstract Account.Id reviewer();
+  public abstract ReviewerStateInternal state();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
index 2f98a0c..e1f786b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Function;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
@@ -34,23 +35,31 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.change.PostReviewers;
-import com.google.gerrit.server.change.ReviewerSuggestionCache;
 import com.google.gerrit.server.change.SuggestReviewers;
+import com.google.gerrit.server.index.account.AccountIndex;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.QueryResult;
+import com.google.gerrit.server.query.account.AccountQueryBuilder;
+import com.google.gerrit.server.query.account.AccountQueryProcessor;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -74,7 +83,9 @@
       });
   private final AccountLoader accountLoader;
   private final AccountCache accountCache;
-  private final ReviewerSuggestionCache reviewerSuggestionCache;
+  private final AccountIndexCollection indexes;
+  private final AccountQueryBuilder queryBuilder;
+  private final AccountQueryProcessor queryProcessor;
   private final AccountControl accountControl;
   private final Provider<ReviewDb> dbProvider;
   private final GroupBackend groupBackend;
@@ -84,15 +95,21 @@
   @Inject
   ReviewersUtil(AccountLoader.Factory accountLoaderFactory,
       AccountCache accountCache,
-      ReviewerSuggestionCache reviewerSuggestionCache,
+      AccountIndexCollection indexes,
+      AccountQueryBuilder queryBuilder,
+      AccountQueryProcessor queryProcessor,
       AccountControl.Factory accountControlFactory,
       Provider<ReviewDb> dbProvider,
       GroupBackend groupBackend,
       GroupMembers.Factory groupMembersFactory,
       Provider<CurrentUser> currentUser) {
-    this.accountLoader = accountLoaderFactory.create(true);
+    Set<FillOptions> fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS);
+    fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
+    this.accountLoader = accountLoaderFactory.create(fillOptions);
     this.accountCache = accountCache;
-    this.reviewerSuggestionCache = reviewerSuggestionCache;
+    this.indexes = indexes;
+    this.queryBuilder = queryBuilder;
+    this.queryProcessor = queryProcessor;
     this.accountControl = accountControlFactory.get();
     this.dbProvider = dbProvider;
     this.groupBackend = groupBackend;
@@ -111,7 +128,6 @@
     String query = suggestReviewers.getQuery();
     boolean suggestAccounts = suggestReviewers.getSuggestAccounts();
     int suggestFrom = suggestReviewers.getSuggestFrom();
-    boolean useFullTextSearch = suggestReviewers.getUseFullTextSearch();
     int limit = suggestReviewers.getLimit();
 
     if (Strings.isNullOrEmpty(query)) {
@@ -122,28 +138,30 @@
       return Collections.emptyList();
     }
 
-    List<AccountInfo> suggestedAccounts;
-    if (useFullTextSearch) {
-      suggestedAccounts = suggestAccountFullTextSearch(suggestReviewers, visibilityControl);
-    } else {
-      suggestedAccounts = suggestAccount(suggestReviewers, visibilityControl);
-    }
+    Collection<AccountInfo> suggestedAccounts =
+        suggestAccounts(suggestReviewers, visibilityControl);
 
     List<SuggestedReviewerInfo> reviewer = new ArrayList<>();
     for (AccountInfo a : suggestedAccounts) {
       SuggestedReviewerInfo info = new SuggestedReviewerInfo();
       info.account = a;
+      info.count = 1;
       reviewer.add(info);
     }
 
     for (GroupReference g : suggestAccountGroup(suggestReviewers, projectControl)) {
-      if (suggestGroupAsReviewer(suggestReviewers, projectControl.getProject(),
-          g, visibilityControl)) {
+      GroupAsReviewer result = suggestGroupAsReviewer(
+          suggestReviewers, projectControl.getProject(), g, visibilityControl);
+      if (result.allowed || result.allowedWithConfirmation) {
         GroupBaseInfo info = new GroupBaseInfo();
         info.id = Url.encode(g.getUUID().get());
         info.name = g.getName();
         SuggestedReviewerInfo suggestedReviewerInfo = new SuggestedReviewerInfo();
         suggestedReviewerInfo.group = info;
+        suggestedReviewerInfo.count = result.size;
+        if (result.allowedWithConfirmation) {
+          suggestedReviewerInfo.confirm = true;
+        }
         reviewer.add(suggestedReviewerInfo);
       }
     }
@@ -155,27 +173,39 @@
     return reviewer.subList(0, limit);
   }
 
-  private List<AccountInfo> suggestAccountFullTextSearch(
-      SuggestReviewers suggestReviewers, VisibilityControl visibilityControl)
-          throws IOException, OrmException {
-    List<AccountInfo> results = reviewerSuggestionCache.search(
-        suggestReviewers.getQuery(), suggestReviewers.getFullTextMaxMatches());
-
-    Iterator<AccountInfo> it = results.iterator();
-    while (it.hasNext()) {
-      Account.Id accountId = new Account.Id(it.next()._accountId);
-      if (!(visibilityControl.isVisibleTo(accountId)
-          && accountControl.canSee(accountId))) {
-        it.remove();
-      }
-    }
-
-    return results;
-  }
-
-  private List<AccountInfo> suggestAccount(SuggestReviewers suggestReviewers,
+  private Collection<AccountInfo> suggestAccounts(SuggestReviewers suggestReviewers,
       VisibilityControl visibilityControl)
       throws OrmException {
+    AccountIndex searchIndex = indexes.getSearchIndex();
+    if (searchIndex != null) {
+      return suggestAccountsFromIndex(suggestReviewers);
+    }
+    return suggestAccountsFromDb(suggestReviewers, visibilityControl);
+  }
+
+  private Collection<AccountInfo> suggestAccountsFromIndex(
+      SuggestReviewers suggestReviewers) throws OrmException {
+    try {
+      Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
+      QueryResult<AccountState> result = queryProcessor
+          .setLimit(suggestReviewers.getLimit())
+          .query(queryBuilder.defaultQuery(suggestReviewers.getQuery()));
+      for (AccountState accountState : result.entities()) {
+        Account.Id id = accountState.getAccount().getId();
+        matches.put(id, accountLoader.get(id));
+      }
+
+      accountLoader.fill();
+
+      return matches.values();
+    } catch (QueryParseException e) {
+      return ImmutableList.of();
+    }
+  }
+
+  private Collection<AccountInfo> suggestAccountsFromDb(
+      SuggestReviewers suggestReviewers, VisibilityControl visibilityControl)
+          throws OrmException {
     String query = suggestReviewers.getQuery();
     int limit = suggestReviewers.getLimit();
 
@@ -246,13 +276,22 @@
             suggestReviewers.getLimit()));
   }
 
-  private boolean suggestGroupAsReviewer(SuggestReviewers suggestReviewers,
+  private static class GroupAsReviewer {
+    boolean allowed;
+    boolean allowedWithConfirmation;
+    int size;
+  }
+
+  private GroupAsReviewer suggestGroupAsReviewer(SuggestReviewers suggestReviewers,
       Project project, GroupReference group,
       VisibilityControl visibilityControl) throws OrmException, IOException {
+    GroupAsReviewer result = new GroupAsReviewer();
     int maxAllowed = suggestReviewers.getMaxAllowed();
+    int maxAllowedWithoutConfirmation =
+        suggestReviewers.getMaxAllowedWithoutConfirmation();
 
     if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
-      return false;
+      return result;
     }
 
     try {
@@ -261,25 +300,33 @@
           .listAccounts(group.getUUID(), project.getNameKey());
 
       if (members.isEmpty()) {
-        return false;
+        return result;
       }
 
-      if (maxAllowed > 0 && members.size() > maxAllowed) {
-        return false;
+      result.size = members.size();
+      if (maxAllowed > 0 && result.size > maxAllowed) {
+        return result;
       }
 
+      boolean needsConfirmation = result.size > maxAllowedWithoutConfirmation;
+
       // require that at least one member in the group can see the change
       for (Account account : members) {
         if (visibilityControl.isVisibleTo(account.getId())) {
-          return true;
+          if (needsConfirmation) {
+            result.allowedWithConfirmation = true;
+          } else {
+            result.allowed = true;
+          }
+          return result;
         }
       }
     } catch (NoSuchGroupException e) {
-      return false;
+      return result;
     } catch (NoSuchProjectException e) {
-      return false;
+      return result;
     }
 
-    return false;
+    return result;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java b/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
index 41a965f..9448ceb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
@@ -14,25 +14,82 @@
 
 package com.google.gerrit.server;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.RepoSequence;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.Config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SuppressWarnings("deprecation")
 @Singleton
 public class Sequences {
   private final Provider<ReviewDb> db;
+  private final NotesMigration migration;
+  private final RepoSequence changeSeq;
 
   @Inject
-  Sequences(Provider<ReviewDb> db) {
+  Sequences(@GerritServerConfig Config cfg,
+      final Provider<ReviewDb> db,
+      NotesMigration migration,
+      GitRepositoryManager repoManager,
+      AllProjectsName allProjects) {
     this.db = db;
+    this.migration = migration;
+
+    final int gap = cfg.getInt("noteDb", "changes", "initialSequenceGap", 0);
+    changeSeq = new RepoSequence(
+        repoManager,
+        allProjects,
+        "changes",
+        new RepoSequence.Seed() {
+          @Override
+          public int get() throws OrmException {
+            return db.get().nextChangeId() + gap;
+          }
+        },
+        cfg.getInt("noteDb", "changes", "sequenceBatchSize", 20));
   }
 
-  @SuppressWarnings("deprecation")
   public int nextChangeId() throws OrmException {
-    // TODO(dborowitz): Use repo sequence when we have ability to turn off
-    // ReviewDb entirely. Until then it's simpler to just keep using ReviewDb.
-    return db.get().nextChangeId();
+    if (!migration.readChangeSequence()) {
+      return db.get().nextChangeId();
+    }
+    return changeSeq.next();
+  }
+
+  public ImmutableList<Integer> nextChangeIds(int count) throws OrmException {
+    if (migration.readChangeSequence()) {
+      return changeSeq.next(count);
+    }
+
+    if (count == 0) {
+      return ImmutableList.of();
+    }
+    checkArgument(count > 0, "count is negative: %s", count);
+    List<Integer> ids = new ArrayList<>(count);
+    ReviewDb db = this.db.get();
+    for (int i = 0; i < count; i++) {
+      ids.add(db.nextChangeId());
+    }
+    return ImmutableList.copyOf(ids);
+  }
+
+  @VisibleForTesting
+  public RepoSequence getChangeIdRepoSequence() {
+    return changeSeq;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
index 30420e0..a0c6118 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
@@ -16,8 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.mail.EmailSender;
@@ -37,11 +36,11 @@
   }
 
   @Override
-  public Set<FieldName> getEditableFields() {
-    Set<Account.FieldName> fields = new  HashSet<>();
-    for (Account.FieldName n : Account.FieldName.values()) {
+  public Set<AccountFieldName> getEditableFields() {
+    Set<AccountFieldName> fields = new  HashSet<>();
+    for (AccountFieldName n : AccountFieldName.values()) {
       if (allowsEdit(n)) {
-        if (n == Account.FieldName.REGISTER_NEW_EMAIL) {
+        if (n == AccountFieldName.REGISTER_NEW_EMAIL) {
           if (emailSender != null && emailSender.isEnabled()) {
             fields.add(n);
           }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index 57ffd0a..0856616 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -21,9 +21,12 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
@@ -84,10 +87,16 @@
 
   static class Loader extends CacheLoader<String, Set<Account.Id>> {
     private final SchemaFactory<ReviewDb> schema;
+    private final AccountIndexCollection accountIndexes;
+    private final Provider<InternalAccountQuery> accountQueryProvider;
 
     @Inject
-    Loader(final SchemaFactory<ReviewDb> schema) {
+    Loader(SchemaFactory<ReviewDb> schema,
+        AccountIndexCollection accountIndexes,
+        Provider<InternalAccountQuery> accountQueryProvider) {
       this.schema = schema;
+      this.accountIndexes = accountIndexes;
+      this.accountQueryProvider = accountQueryProvider;
     }
 
     @Override
@@ -97,9 +106,18 @@
         for (Account a : db.accounts().byPreferredEmail(email)) {
           r.add(a.getId());
         }
-        for (AccountExternalId a : db.accountExternalIds()
-            .byEmailAddress(email)) {
-          r.add(a.getAccountId());
+        if (accountIndexes.getSearchIndex() != null) {
+          for (AccountState accountState : accountQueryProvider.get()
+              .byExternalId(
+                  (new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO,
+                      email)).get())) {
+            r.add(accountState.getAccount().getId());
+          }
+        } else {
+          for (AccountExternalId a : db.accountExternalIds()
+              .byEmailAddress(email)) {
+            r.add(a.getAccountId());
+          }
         }
         return ImmutableSet.copyOf(r);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
index 2e97005..3a4566a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
@@ -29,4 +29,6 @@
   void evict(Account.Id accountId) throws IOException;
 
   void evictByUsername(String username);
+
+  void evictAll() throws IOException;
 }
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 7bf0642..63d2ddb 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
@@ -24,26 +24,34 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
+import com.google.gerrit.server.index.account.AccountIndexer;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 
@@ -76,15 +84,15 @@
 
   private final LoadingCache<Account.Id, AccountState> byId;
   private final LoadingCache<String, Optional<Account.Id>> byName;
-  private final AccountIndexCollection indexes;
+  private final Provider<AccountIndexer> indexer;
 
   @Inject
   AccountCacheImpl(@Named(BYID_NAME) LoadingCache<Account.Id, AccountState> byId,
       @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername,
-      AccountIndexCollection indexes) {
+      Provider<AccountIndexer> indexer) {
     this.byId = byId;
     this.byName = byUsername;
-    this.indexes = indexes;
+    this.indexer = indexer;
   }
 
   @Override
@@ -117,13 +125,15 @@
   public void evict(Account.Id accountId) throws IOException {
     if (accountId != null) {
       byId.invalidate(accountId);
-      index(accountId);
+      indexer.get().index(accountId);
     }
   }
 
-  private void index(Account.Id id) throws IOException {
-    for (Index<?, AccountState> i : indexes.getWriteIndexes()) {
-      i.replace(get(id));
+  @Override
+  public void evictAll() throws IOException {
+    byId.invalidateAll();
+    for (Account.Id accountId : byId.asMap().keySet()) {
+      indexer.get().index(accountId);
     }
   }
 
@@ -139,7 +149,8 @@
     account.setActive(false);
     Collection<AccountExternalId> ids = Collections.emptySet();
     Set<AccountGroup.UUID> anon = ImmutableSet.of();
-    return new AccountState(account, anon, ids);
+    return new AccountState(account, anon, ids,
+        new HashMap<ProjectWatchKey, Set<NotifyType>>());
   }
 
   static class ByIdLoader extends CacheLoader<Account.Id, AccountState> {
@@ -147,17 +158,24 @@
     private final GroupCache groupCache;
     private final GeneralPreferencesLoader loader;
     private final LoadingCache<String, Optional<Account.Id>> byName;
+    private final boolean readFromGit;
+    private final Provider<WatchConfig.Accessor> watchConfig;
 
     @Inject
     ByIdLoader(SchemaFactory<ReviewDb> sf,
         GroupCache groupCache,
         GeneralPreferencesLoader loader,
         @Named(BYUSER_NAME) LoadingCache<String,
-        Optional<Account.Id>> byUsername) {
+            Optional<Account.Id>> byUsername,
+        @GerritServerConfig Config cfg,
+        Provider<WatchConfig.Accessor> watchConfig) {
       this.schema = sf;
       this.groupCache = groupCache;
       this.loader = loader;
       this.byName = byUsername;
+      this.readFromGit =
+          cfg.getBoolean("user", null, "readProjectWatchesFromGit", true);
+      this.watchConfig = watchConfig;
     }
 
     @Override
@@ -173,17 +191,16 @@
     }
 
     private AccountState load(final ReviewDb db, final Account.Id who)
-        throws OrmException {
-      final Account account = db.accounts().get(who);
+        throws OrmException, IOException, ConfigInvalidException {
+      Account account = db.accounts().get(who);
       if (account == null) {
         // Account no longer exists? They are anonymous.
-        //
         return missing(who);
       }
 
-      final Collection<AccountExternalId> externalIds =
-          Collections.unmodifiableCollection(db.accountExternalIds().byAccount(
-              who).toList());
+      Collection<AccountExternalId> externalIds =
+          Collections.unmodifiableCollection(
+              db.accountExternalIds().byAccount(who).toList());
 
       Set<AccountGroup.UUID> internalGroups = new HashSet<>();
       for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
@@ -203,25 +220,45 @@
         account.setGeneralPreferences(GeneralPreferencesInfo.defaults());
       }
 
-      return new AccountState(account, internalGroups, externalIds);
+      Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
+          readFromGit
+              ? watchConfig.get().getProjectWatches(who)
+              : GetWatchedProjects.readProjectWatchesFromDb(db, who);
+
+      return new AccountState(account, internalGroups, externalIds,
+          projectWatches);
     }
   }
 
   static class ByNameLoader extends CacheLoader<String, Optional<Account.Id>> {
     private final SchemaFactory<ReviewDb> schema;
+    private final AccountIndexCollection accountIndexes;
+    private final Provider<InternalAccountQuery> accountQueryProvider;
 
     @Inject
-    ByNameLoader(final SchemaFactory<ReviewDb> sf) {
+    ByNameLoader(SchemaFactory<ReviewDb> sf,
+        AccountIndexCollection accountIndexes,
+        Provider<InternalAccountQuery> accountQueryProvider) {
       this.schema = sf;
+      this.accountIndexes = accountIndexes;
+      this.accountQueryProvider = accountQueryProvider;
     }
 
     @Override
     public Optional<Account.Id> load(String username) throws Exception {
-      try (ReviewDb db = schema.open()) {
-        final AccountExternalId.Key key = new AccountExternalId.Key( //
+        AccountExternalId.Key key = new AccountExternalId.Key( //
             AccountExternalId.SCHEME_USERNAME, //
             username);
-        final AccountExternalId id = db.accountExternalIds().get(key);
+      if (accountIndexes.getSearchIndex() != null) {
+        AccountState accountState =
+            accountQueryProvider.get().oneByExternalId(key.get());
+        return accountState != null
+            ? Optional.of(accountState.getAccount().getId())
+            : Optional.<Account.Id>absent();
+      }
+
+      try (ReviewDb db = schema.open()) {
+        AccountExternalId id = db.accountExternalIds().get(key);
         if (id != null) {
           return Optional.of(id.getAccountId());
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
index b4ca530..63d2fc6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
@@ -32,6 +32,9 @@
     /** Preferred email address to contact the user at. */
     EMAIL,
 
+    /** All secondary email addresses of the user. */
+    SECONDARY_EMAILS,
+
     /** User profile images. */
     AVATARS,
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java
index 68e19e4..89e9419 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java
@@ -23,8 +23,8 @@
 import com.google.gerrit.server.account.AccountDirectory.DirectoryException;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -36,7 +36,7 @@
 import java.util.Set;
 
 public class AccountLoader {
-  private static final Set<FillOptions> DETAILED_OPTIONS =
+  public static final Set<FillOptions> DETAILED_OPTIONS =
       Collections.unmodifiableSet(EnumSet.of(
           FillOptions.ID,
           FillOptions.NAME,
@@ -46,6 +46,7 @@
 
   public interface Factory {
     AccountLoader create(boolean detailed);
+    AccountLoader create(Set<FillOptions> options);
   }
 
   private final InternalAccountDirectory directory;
@@ -53,10 +54,20 @@
   private final Map<Account.Id, AccountInfo> created;
   private final List<AccountInfo> provided;
 
-  @Inject
-  AccountLoader(InternalAccountDirectory directory, @Assisted boolean detailed) {
+  @AssistedInject
+  AccountLoader(InternalAccountDirectory directory,
+      @Assisted boolean detailed) {
+    this(directory,
+        detailed
+            ? DETAILED_OPTIONS
+            : InternalAccountDirectory.ID_ONLY);
+  }
+
+  @AssistedInject
+  AccountLoader(InternalAccountDirectory directory,
+      @Assisted Set<FillOptions> options) {
     this.directory = directory;
-    options = detailed ? DETAILED_OPTIONS : InternalAccountDirectory.ID_ONLY;
+    this.options = options;
     created = new HashMap<>();
     provided = new ArrayList<>();
   }
@@ -83,7 +94,7 @@
       directory.fillAccountInfo(
           Iterables.concat(created.values(), provided), options);
     } catch (DirectoryException e) {
-      Throwables.propagateIfPossible(e.getCause(), OrmException.class);
+      Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
       throw new OrmException(e);
     }
   }
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 ac4d2ba..178cc79 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
@@ -21,17 +21,21 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
@@ -58,6 +62,8 @@
   private final ProjectCache projectCache;
   private final AtomicBoolean awaitsFirstAccountCheck;
   private final AuditService auditService;
+  private final AccountIndexCollection accountIndexes;
+  private final Provider<InternalAccountQuery> accountQueryProvider;
 
   @Inject
   AccountManager(SchemaFactory<ReviewDb> schema,
@@ -67,7 +73,9 @@
       IdentifiedUser.GenericFactory userFactory,
       ChangeUserName.Factory changeUserNameFactory,
       ProjectCache projectCache,
-      AuditService auditService) {
+      AuditService auditService,
+      AccountIndexCollection accountIndexes,
+      Provider<InternalAccountQuery> accountQueryProvider) {
     this.schema = schema;
     this.byIdCache = byIdCache;
     this.byEmailCache = byEmailCache;
@@ -77,6 +85,8 @@
     this.projectCache = projectCache;
     this.awaitsFirstAccountCheck = new AtomicBoolean(true);
     this.auditService = auditService;
+    this.accountIndexes = accountIndexes;
+    this.accountQueryProvider = accountQueryProvider;
   }
 
   /**
@@ -84,6 +94,14 @@
    */
   public Account.Id lookup(String externalId) throws AccountException {
     try {
+      if (accountIndexes.getSearchIndex() != null) {
+        AccountState accountState =
+            accountQueryProvider.get().oneByExternalId(externalId);
+        return accountState != null
+            ? accountState.getAccount().getId()
+            : null;
+      }
+
       try (ReviewDb db = schema.open()) {
         AccountExternalId ext =
             db.accountExternalIds().get(new AccountExternalId.Key(externalId));
@@ -125,23 +143,33 @@
         update(db, who, id);
         return new AuthResult(id.getAccountId(), key, false);
       }
-    } catch (OrmException | NameAlreadyUsedException | InvalidUserNameException e) {
+    } catch (OrmException e) {
       throw new AccountException("Authentication error", e);
     }
   }
 
   private AccountExternalId getAccountExternalId(ReviewDb db,
       AccountExternalId.Key key) throws OrmException {
-    String keyValue = key.get();
-    String keyScheme = keyValue.substring(0, keyValue.indexOf(':') + 1);
+    if (accountIndexes.getSearchIndex() != null) {
+      AccountState accountState =
+          accountQueryProvider.get().oneByExternalId(key.get());
+      if (accountState != null) {
+        for (AccountExternalId extId : accountState.getExternalIds()) {
+          if (extId.getKey().equals(key)) {
+            return extId;
+          }
+        }
+      }
+      return null;
+    }
 
     // We don't have at the moment an account_by_external_id cache
     // but by using the accounts cache we get the list of external_ids
     // without having to query the DB every time
-    if (keyScheme.equals(AccountExternalId.SCHEME_GERRIT)
-        || keyScheme.equals(AccountExternalId.SCHEME_USERNAME)) {
+    if (key.getScheme().equals(AccountExternalId.SCHEME_GERRIT)
+        || key.getScheme().equals(AccountExternalId.SCHEME_USERNAME)) {
       AccountState state = byIdCache.getByUsername(
-          keyValue.substring(keyScheme.length()));
+          key.get().substring(key.getScheme().length()));
       if (state != null) {
         for (AccountExternalId accountExternalId : state.getExternalIds()) {
           if (accountExternalId.getKey().equals(key)) {
@@ -154,8 +182,7 @@
   }
 
   private void update(ReviewDb db, AuthRequest who, AccountExternalId extId)
-      throws OrmException, NameAlreadyUsedException, InvalidUserNameException,
-      IOException {
+      throws OrmException, IOException {
     IdentifiedUser user = userFactory.create(extId.getAccountId());
     Account toUpdate = null;
 
@@ -175,17 +202,18 @@
       db.accountExternalIds().update(Collections.singleton(extId));
     }
 
-    if (!realm.allowsEdit(Account.FieldName.FULL_NAME)
+    if (!realm.allowsEdit(AccountFieldName.FULL_NAME)
         && !Strings.isNullOrEmpty(who.getDisplayName())
         && !eq(user.getAccount().getFullName(), who.getDisplayName())) {
       toUpdate = load(toUpdate, user.getAccountId(), db);
       toUpdate.setFullName(who.getDisplayName());
     }
 
-    if (!realm.allowsEdit(Account.FieldName.USER_NAME)
+    if (!realm.allowsEdit(AccountFieldName.USER_NAME)
         && who.getUserName() != null
         && !eq(user.getUserName(), who.getUserName())) {
-      changeUserNameFactory.create(db, user, who.getUserName()).call();
+      log.warn(String.format("Not changing already set username %s to %s",
+          user.getUserName(), who.getUserName()));
     }
 
     if (toUpdate != null) {
@@ -313,7 +341,7 @@
     } else {
       log.error(errorMessage);
     }
-    if (!realm.allowsEdit(Account.FieldName.USER_NAME)) {
+    if (!realm.allowsEdit(AccountFieldName.USER_NAME)) {
       // 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
@@ -351,12 +379,7 @@
         if (!extId.getAccountId().equals(to)) {
           throw new AccountException("Identity in use by another account");
         }
-        try {
-          update(db, who, extId);
-        } catch (NameAlreadyUsedException | InvalidUserNameException e) {
-          throw new AccountException("Account update failed", e);
-        }
-
+        update(db, who, extId);
       } else {
         extId = createId(to, who);
         extId.setEmailAddress(who.getEmailAddress());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 83e7992..5a18269 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -14,9 +14,13 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -34,15 +38,20 @@
   private final Realm realm;
   private final AccountByEmailCache byEmail;
   private final AccountCache byId;
-  private final Provider<ReviewDb> schema;
+  private final AccountIndexCollection accountIndexes;
+  private final Provider<InternalAccountQuery> accountQueryProvider;
 
   @Inject
-  AccountResolver(final Realm realm, final AccountByEmailCache byEmail,
-      final AccountCache byId, final Provider<ReviewDb> schema) {
+  AccountResolver(Realm realm,
+      AccountByEmailCache byEmail,
+      AccountCache byId,
+      AccountIndexCollection accountIndexes,
+      Provider<InternalAccountQuery> accountQueryProvider) {
     this.realm = realm;
     this.byEmail = byEmail;
     this.byId = byId;
-    this.schema = schema;
+    this.accountIndexes = accountIndexes;
+    this.accountQueryProvider = accountQueryProvider;
   }
 
   /**
@@ -55,8 +64,8 @@
    * @return the single account that matches; null if no account matches or
    *         there are multiple candidates.
    */
-  public Account find(final String nameOrEmail) throws OrmException {
-    Set<Account.Id> r = findAll(nameOrEmail);
+  public Account find(ReviewDb db, String nameOrEmail) throws OrmException {
+    Set<Account.Id> r = findAll(db, nameOrEmail);
     if (r.size() == 1) {
       return byId.get(r.iterator().next()).getAccount();
     }
@@ -78,17 +87,19 @@
   /**
    * Find all accounts matching the name or name/email string.
    *
+   * @param db open database handle.
    * @param nameOrEmail a string of the format
    *        "Full Name &lt;email@example&gt;", just the email address
    *        ("email@example"), a full name ("Full Name"), an account id
    *        ("18419") or an user name ("username").
    * @return the accounts that match, empty collection if none.  Never null.
    */
-  public Set<Account.Id> findAll(String nameOrEmail) throws OrmException {
+  public Set<Account.Id> findAll(ReviewDb db, String nameOrEmail)
+      throws OrmException {
     Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(nameOrEmail);
     if (m.matches()) {
       Account.Id id = Account.Id.parse(m.group(1));
-      if (exists(id)) {
+      if (exists(db, id)) {
         return Collections.singleton(id);
       }
       return Collections.emptySet();
@@ -96,7 +107,7 @@
 
     if (nameOrEmail.matches("^[1-9][0-9]*$")) {
       Account.Id id = Account.Id.parse(nameOrEmail);
-      if (exists(id)) {
+      if (exists(db, id)) {
         return Collections.singleton(id);
       }
       return Collections.emptySet();
@@ -109,40 +120,42 @@
       }
     }
 
-    return findAllByNameOrEmail(nameOrEmail);
+    return findAllByNameOrEmail(db, nameOrEmail);
   }
 
-  private boolean exists(Account.Id id) throws OrmException {
-    return schema.get().accounts().get(id) != null;
+  private boolean exists(ReviewDb db, Account.Id id) throws OrmException {
+    return db.accounts().get(id) != null;
   }
 
   /**
    * Locate exactly one account matching the name or name/email string.
    *
+   * @param db open database handle.
    * @param nameOrEmail a string of the format
    *        "Full Name &lt;email@example&gt;", just the email address
    *        ("email@example"), a full name ("Full Name").
    * @return the single account that matches; null if no account matches or
    *         there are multiple candidates.
    */
-  public Account findByNameOrEmail(final String nameOrEmail)
+  public Account findByNameOrEmail(ReviewDb db, String nameOrEmail)
       throws OrmException {
-    Set<Account.Id> r = findAllByNameOrEmail(nameOrEmail);
+    Set<Account.Id> r = findAllByNameOrEmail(db, nameOrEmail);
     return r.size() == 1 ? byId.get(r.iterator().next()).getAccount() : null;
   }
 
   /**
    * Locate exactly one account matching the name or name/email string.
    *
+   * @param db open database handle.
    * @param nameOrEmail a string of the format
    *        "Full Name &lt;email@example&gt;", just the email address
    *        ("email@example"), a full name ("Full Name").
    * @return the accounts that match, empty collection if none. Never null.
    */
-  public Set<Account.Id> findAllByNameOrEmail(final String nameOrEmail)
+  public Set<Account.Id> findAllByNameOrEmail(ReviewDb db, String nameOrEmail)
       throws OrmException {
-    final int lt = nameOrEmail.indexOf('<');
-    final int gt = nameOrEmail.indexOf('>');
+    int lt = nameOrEmail.indexOf('<');
+    int gt = nameOrEmail.indexOf('>');
     if (lt >= 0 && gt > lt && nameOrEmail.contains("@")) {
       Set<Account.Id> ids = byEmail.get(nameOrEmail.substring(lt + 1, gt));
       if (ids.isEmpty() || ids.size() == 1) {
@@ -165,34 +178,49 @@
       return byEmail.get(nameOrEmail);
     }
 
-    final Account.Id id = realm.lookup(nameOrEmail);
+    Account.Id id = realm.lookup(nameOrEmail);
     if (id != null) {
       return Collections.singleton(id);
     }
 
-    List<Account> m = schema.get().accounts().byFullName(nameOrEmail).toList();
+    if (accountIndexes.getSearchIndex() != null) {
+      List<AccountState> m = accountQueryProvider.get().byFullName(nameOrEmail);
+      if (m.size() == 1) {
+        return Collections.singleton(m.get(0).getAccount().getId());
+      }
+
+      // At this point we have no clue. Just perform a whole bunch of suggestions
+      // and pray we come up with a reasonable result list.
+      return FluentIterable
+          .from(accountQueryProvider.get().byDefault(nameOrEmail))
+          .transform(new Function<AccountState, Account.Id>() {
+            @Override
+            public Account.Id apply(AccountState accountState) {
+              return accountState.getAccount().getId();
+            }
+          }).toSet();
+    }
+
+    List<Account> m = db.accounts().byFullName(nameOrEmail).toList();
     if (m.size() == 1) {
       return Collections.singleton(m.get(0).getId());
     }
 
     // At this point we have no clue. Just perform a whole bunch of suggestions
     // and pray we come up with a reasonable result list.
-    //
     Set<Account.Id> result = new HashSet<>();
     String a = nameOrEmail;
     String b = nameOrEmail + "\u9fa5";
-    for (Account act : schema.get().accounts().suggestByFullName(a, b, 10)) {
+    for (Account act : db.accounts().suggestByFullName(a, b, 10)) {
       result.add(act.getId());
     }
-    for (AccountExternalId extId : schema
-        .get()
-        .accountExternalIds()
+    for (AccountExternalId extId : db.accountExternalIds()
         .suggestByKey(
             new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, a),
             new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, b), 10)) {
       result.add(extId.getAccountId());
     }
-    for (AccountExternalId extId : schema.get().accountExternalIds()
+    for (AccountExternalId extId : db.accountExternalIds()
         .suggestByEmailAddress(a, b, 10)) {
       result.add(extId.getAccountId());
     }
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 37e36cd..05a7179 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
@@ -14,32 +14,49 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
+import com.google.common.base.Function;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.CurrentUser.PropertyKey;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 
 import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 public class AccountState {
+  public static final Function<AccountState, Account.Id> ACCOUNT_ID_FUNCTION =
+      new Function<AccountState, Account.Id>() {
+        @Override
+        public Account.Id apply(AccountState in) {
+          return in.getAccount().getId();
+        }
+      };
+
   private final Account account;
   private final Set<AccountGroup.UUID> internalGroups;
   private final Collection<AccountExternalId> externalIds;
+  private final Map<ProjectWatchKey, Set<NotifyType>> projectWatches;
   private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
 
-  public AccountState(final Account account,
-      final Set<AccountGroup.UUID> actualGroups,
-      final Collection<AccountExternalId> externalIds) {
+  public AccountState(Account account,
+      Set<AccountGroup.UUID> actualGroups,
+      Collection<AccountExternalId> externalIds,
+      Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
     this.account = account;
     this.internalGroups = actualGroups;
     this.externalIds = externalIds;
+    this.projectWatches = projectWatches;
     this.account.setUserName(getUserName(externalIds));
   }
 
@@ -74,6 +91,11 @@
     return externalIds;
   }
 
+  /** The project watches of the account. */
+  public Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches() {
+    return projectWatches;
+  }
+
   /** The set of groups maintained directly within the Gerrit database. */
   public Set<AccountGroup.UUID> getInternalGroups() {
     return internalGroups;
@@ -88,6 +110,16 @@
     return null;
   }
 
+  public static Set<String> getEmails(Collection<AccountExternalId> ids) {
+    Set<String> emails = new HashSet<>();
+    for (AccountExternalId id : ids) {
+      if (id.isScheme(SCHEME_MAILTO)) {
+        emails.add(id.getSchemeRest());
+      }
+    }
+    return emails;
+  }
+
   /**
    * Lookup a previously stored property.
    * <p>
@@ -118,7 +150,7 @@
    */
   public <T> void put(PropertyKey<T> key, @Nullable T value) {
     Cache<PropertyKey<Object>, Object> p = properties(value != null);
-    if (p != null || value != null) {
+    if (p != null) {
       @SuppressWarnings("unchecked")
       PropertyKey<Object> k = (PropertyKey<Object>) key;
       if (value != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index 06cf255..04ebc87 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -36,6 +37,7 @@
 public class AccountsCollection implements
     RestCollection<TopLevelResource, AccountResource>,
     AcceptsCreate<TopLevelResource> {
+  private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> self;
   private final AccountResolver resolver;
   private final AccountControl.Factory accountControlFactory;
@@ -45,13 +47,15 @@
   private final CreateAccount.Factory createAccountFactory;
 
   @Inject
-  AccountsCollection(Provider<CurrentUser> self,
+  AccountsCollection(Provider<ReviewDb> db,
+      Provider<CurrentUser> self,
       AccountResolver resolver,
       AccountControl.Factory accountControlFactory,
       IdentifiedUser.GenericFactory userFactory,
       Provider<QueryAccounts> list,
       DynamicMap<RestView<AccountResource>> views,
       CreateAccount.Factory createAccountFactory) {
+    this.db = db;
     this.self = self;
     this.resolver = resolver;
     this.accountControlFactory = accountControlFactory;
@@ -122,7 +126,7 @@
       }
     }
 
-    Account match = resolver.find(id);
+    Account match = resolver.find(db.get(), id);
     if (match == null) {
       return null;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
index 3017f73..4bf4214 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
@@ -14,32 +14,46 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupReference;
 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.server.config.AdministrateServerGroups;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /** Caches active {@link GlobalCapability} set for a site. */
 public class CapabilityCollection {
-  private final Map<String, List<PermissionRule>> permissions;
+  public interface Factory {
+    CapabilityCollection create(@Nullable AccessSection section);
+  }
 
-  public final List<PermissionRule> administrateServer;
-  public final List<PermissionRule> batchChangesLimit;
-  public final List<PermissionRule> emailReviewers;
-  public final List<PermissionRule> priority;
-  public final List<PermissionRule> queryLimit;
+  private final ImmutableMap<String, ImmutableList<PermissionRule>> permissions;
 
-  public CapabilityCollection(AccessSection section) {
+  public final ImmutableList<PermissionRule> administrateServer;
+  public final ImmutableList<PermissionRule> batchChangesLimit;
+  public final ImmutableList<PermissionRule> emailReviewers;
+  public final ImmutableList<PermissionRule> priority;
+  public final ImmutableList<PermissionRule> queryLimit;
+
+  @Inject
+  CapabilityCollection(
+      @AdministrateServerGroups ImmutableSet<GroupReference> admins,
+      @Assisted @Nullable AccessSection section) {
     if (section == null) {
       section = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
     }
@@ -61,18 +75,19 @@
       }
     }
     configureDefaults(tmp, section);
+    if (!tmp.containsKey(GlobalCapability.ADMINISTRATE_SERVER) && !admins.isEmpty()) {
+      tmp.put(GlobalCapability.ADMINISTRATE_SERVER, ImmutableList.<PermissionRule>of());
+    }
 
-    Map<String, List<PermissionRule>> res = new HashMap<>();
+    ImmutableMap.Builder<String, ImmutableList<PermissionRule>> m = ImmutableMap.builder();
     for (Map.Entry<String, List<PermissionRule>> e : tmp.entrySet()) {
       List<PermissionRule> rules = e.getValue();
-      if (rules.size() == 1) {
-        res.put(e.getKey(), Collections.singletonList(rules.get(0)));
-      } else {
-        res.put(e.getKey(), Collections.unmodifiableList(
-            Arrays.asList(rules.toArray(new PermissionRule[rules.size()]))));
+      if (GlobalCapability.ADMINISTRATE_SERVER.equals(e.getKey())) {
+        rules = mergeAdmin(admins, rules);
       }
+      m.put(e.getKey(), ImmutableList.copyOf(rules));
     }
-    permissions = Collections.unmodifiableMap(res);
+    permissions = m.build();
 
     administrateServer = getPermission(GlobalCapability.ADMINISTRATE_SERVER);
     batchChangesLimit = getPermission(GlobalCapability.BATCH_CHANGES_LIMIT);
@@ -81,9 +96,27 @@
     queryLimit = getPermission(GlobalCapability.QUERY_LIMIT);
   }
 
-  public List<PermissionRule> getPermission(String permissionName) {
-    List<PermissionRule> r = permissions.get(permissionName);
-    return r != null ? r : Collections.<PermissionRule> emptyList();
+  private static List<PermissionRule> mergeAdmin(Set<GroupReference> admins,
+      List<PermissionRule> rules) {
+    if (admins.isEmpty()) {
+      return rules;
+    }
+
+    List<PermissionRule> r = new ArrayList<>(admins.size() + rules.size());
+    for (GroupReference g : admins) {
+      r.add(new PermissionRule(g));
+    }
+    for (PermissionRule rule : rules) {
+      if (!admins.contains(rule.getGroup())) {
+        r.add(rule);
+      }
+    }
+    return r;
+  }
+
+  public ImmutableList<PermissionRule> getPermission(String permissionName) {
+    ImmutableList<PermissionRule> r = permissions.get(permissionName);
+    return r != null ? r : ImmutableList.<PermissionRule> of();
   }
 
   private static final GroupReference anonymous = SystemGroupBackend
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 337cd7c..e348e73 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -263,20 +263,7 @@
     }
 
     rules = capabilities.getPermission(permissionName);
-
-    if (rules.isEmpty()) {
-      effective.put(permissionName, rules);
-      return rules;
-    }
-
     GroupMembership groups = user.getEffectiveGroups();
-    if (rules.size() == 1) {
-      if (!match(groups, rules.get(0))) {
-        rules = Collections.emptyList();
-      }
-      effective.put(permissionName, rules);
-      return rules;
-    }
 
     List<PermissionRule> mine = new ArrayList<>(rules.size());
     for (PermissionRule rule : rules) {
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 2f66321..93865f6 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
@@ -20,10 +20,10 @@
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -35,7 +35,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.CreateAccount.Input;
 import com.google.gerrit.server.api.accounts.AccountExternalIdCreator;
 import com.google.gerrit.server.group.GroupsCollection;
 import com.google.gerrit.server.index.account.AccountIndexer;
@@ -57,17 +56,8 @@
 import java.util.Set;
 
 @RequiresCapability(GlobalCapability.CREATE_ACCOUNT)
-public class CreateAccount implements RestModifyView<TopLevelResource, Input> {
-  public static class Input {
-    @DefaultInput
-    public String username;
-    public String name;
-    public String email;
-    public String sshKey;
-    public String httpPassword;
-    public List<String> groups;
-  }
-
+public class CreateAccount
+    implements RestModifyView<TopLevelResource, AccountInput> {
   public interface Factory {
     CreateAccount create(String username);
   }
@@ -113,12 +103,12 @@
   }
 
   @Override
-  public Response<AccountInfo> apply(TopLevelResource rsrc, Input input)
+  public Response<AccountInfo> apply(TopLevelResource rsrc, AccountInput input)
       throws BadRequestException, ResourceConflictException,
       UnprocessableEntityException, OrmException, IOException,
       ConfigInvalidException {
     if (input == null) {
-      input = new Input();
+      input = new AccountInput();
     }
     if (input.username != null && !username.equals(input.username)) {
       throw new BadRequestException("username must match URL");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
index 1110acd..578352b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -23,8 +25,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GetEmails.EmailInfo;
@@ -96,7 +96,7 @@
       throw new AuthException("not allowed to use no_confirmation");
     }
 
-    if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
+    if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
       throw new MethodNotAllowedException("realm does not allow adding emails");
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index eb3c9a0..57af333 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -15,8 +15,9 @@
 package com.google.gerrit.server.account;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -39,7 +40,7 @@
   }
 
   @Override
-  public boolean allowsEdit(final Account.FieldName field) {
+  public boolean allowsEdit(final AccountFieldName field) {
     if (authConfig.getAuthType() == AuthType.HTTP) {
       switch (field) {
         case USER_NAME:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
index 76f63b7..1f073ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -67,7 +67,7 @@
   public Response<?> apply(IdentifiedUser user, String email)
       throws ResourceNotFoundException, ResourceConflictException,
       MethodNotAllowedException, OrmException, IOException {
-    if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
+    if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
       throw new MethodNotAllowedException("realm does not allow deleting emails");
     }
     AccountExternalId.Key key = new AccountExternalId.Key(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
index c9d3c79..e2fbc3c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
@@ -14,21 +14,28 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
@@ -36,49 +43,75 @@
 @Singleton
 public class DeleteWatchedProjects
     implements RestModifyView<AccountResource, List<ProjectWatchInfo>> {
-
   private final Provider<ReviewDb> dbProvider;
   private final Provider<IdentifiedUser> self;
+  private final AccountCache accountCache;
+  private final WatchConfig.Accessor watchConfig;
 
   @Inject
   DeleteWatchedProjects(Provider<ReviewDb> dbProvider,
-      Provider<IdentifiedUser> self) {
+      Provider<IdentifiedUser> self,
+      AccountCache accountCache,
+      WatchConfig.Accessor watchConfig) {
     this.dbProvider = dbProvider;
     this.self = self;
+    this.accountCache = accountCache;
+    this.watchConfig = watchConfig;
   }
 
   @Override
-  public Response<?> apply(
-      AccountResource rsrc, List<ProjectWatchInfo> input)
-      throws UnprocessableEntityException, OrmException, AuthException {
-    if (self.get() != rsrc.getUser()) {
+  public Response<?> apply(AccountResource rsrc, List<ProjectWatchInfo> input)
+      throws AuthException, UnprocessableEntityException, OrmException,
+      IOException, ConfigInvalidException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("It is not allowed to edit project watches "
           + "of other users");
     }
+    if (input == null) {
+      return Response.none();
+    }
+
+    Account.Id accountId = rsrc.getUser().getAccountId();
+    deleteFromDb(accountId, input);
+    deleteFromGit(accountId, input);
+    accountCache.evict(accountId);
+    return Response.none();
+  }
+
+  private void deleteFromDb(Account.Id accountId, List<ProjectWatchInfo> input)
+      throws OrmException, IOException {
     ResultSet<AccountProjectWatch> watchedProjects =
-        dbProvider.get().accountProjectWatches()
-            .byAccount(rsrc.getUser().getAccountId());
-    HashMap<AccountProjectWatch.Key, AccountProjectWatch>
-        watchedProjectsMap = new HashMap<>();
+        dbProvider.get().accountProjectWatches().byAccount(accountId);
+    HashMap<AccountProjectWatch.Key, AccountProjectWatch> watchedProjectsMap =
+        new HashMap<>();
     for (AccountProjectWatch watchedProject : watchedProjects) {
       watchedProjectsMap.put(watchedProject.getKey(), watchedProject);
     }
 
-    if (input != null) {
-      List<AccountProjectWatch> watchesToDelete = new LinkedList<>();
-      for (ProjectWatchInfo projectInfo : input) {
-        AccountProjectWatch.Key key = new AccountProjectWatch.Key(
-            rsrc.getUser().getAccountId(),
-            new Project.NameKey(projectInfo.project),
-            projectInfo.filter);
-        if (!watchedProjectsMap.containsKey(key)) {
-          throw new UnprocessableEntityException(projectInfo.project
-              + " is not currently watched by this user.");
-        }
+    List<AccountProjectWatch> watchesToDelete = new LinkedList<>();
+    for (ProjectWatchInfo projectInfo : input) {
+      AccountProjectWatch.Key key = new AccountProjectWatch.Key(accountId,
+          new Project.NameKey(projectInfo.project), projectInfo.filter);
+      if (watchedProjectsMap.containsKey(key)) {
         watchesToDelete.add(watchedProjectsMap.get(key));
       }
-      dbProvider.get().accountProjectWatches().delete(watchesToDelete);
     }
-    return Response.none();
+    if (!watchesToDelete.isEmpty()) {
+      dbProvider.get().accountProjectWatches().delete(watchesToDelete);
+      accountCache.evict(accountId);
+    }
+  }
+
+  private void deleteFromGit(Account.Id accountId, List<ProjectWatchInfo> input)
+      throws IOException, ConfigInvalidException {
+    watchConfig.deleteProjectWatches(accountId, Lists.transform(input,
+        new Function<ProjectWatchInfo, ProjectWatchKey>() {
+          @Override
+          public ProjectWatchKey apply(ProjectWatchInfo info) {
+            return ProjectWatchKey.create(new Project.NameKey(info.project),
+                info.filter);
+          }
+        }));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
index d3b938f..a53f64e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
 
 /** Fake implementation of {@link Realm} that does not communicate. */
 public class FakeRealm extends AbstractRealm {
   @Override
-  public boolean allowsEdit(FieldName field) {
+  public boolean allowsEdit(AccountFieldName field) {
     return false;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
index a7dba1a..8339baf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.account;
 
 import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+import static com.google.gerrit.server.config.ConfigUtil.skipField;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_MATCH;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
@@ -40,6 +41,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -75,31 +77,51 @@
       GeneralPreferencesInfo in) throws IOException,
           ConfigInvalidException, RepositoryNotFoundException {
     try (Repository allUsers = gitMgr.openRepository(allUsersName)) {
-      VersionedAccountPreferences p =
-          VersionedAccountPreferences.forUser(id);
-      p.load(allUsers);
+      // Load all users default prefs
+      VersionedAccountPreferences dp = VersionedAccountPreferences.forDefault();
+      dp.load(allUsers);
+      GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
+      loadSection(dp.getConfig(), UserConfigSections.GENERAL, null, allUserPrefs,
+          GeneralPreferencesInfo.defaults(), in);
 
+      // Load user prefs
+      VersionedAccountPreferences p = VersionedAccountPreferences.forUser(id);
+      p.load(allUsers);
       GeneralPreferencesInfo r =
           loadSection(p.getConfig(), UserConfigSections.GENERAL, null,
           new GeneralPreferencesInfo(),
-          GeneralPreferencesInfo.defaults(), in);
+          updateDefaults(allUserPrefs), in);
 
-      return loadFromAllUsers(r, p, allUsers);
+      return loadMyMenusAndUrlAliases(r, p, dp);
     }
   }
 
-  public GeneralPreferencesInfo loadFromAllUsers(
-      GeneralPreferencesInfo r, VersionedAccountPreferences v,
-      Repository allUsers) {
+  private GeneralPreferencesInfo updateDefaults(GeneralPreferencesInfo input) {
+    GeneralPreferencesInfo result = GeneralPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      log.error(
+          "Cannot get default general preferences from " + allUsersName.get(),
+          e);
+      return GeneralPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
+  public GeneralPreferencesInfo loadMyMenusAndUrlAliases(
+      GeneralPreferencesInfo r, VersionedAccountPreferences v, VersionedAccountPreferences d) {
     r.my = my(v);
     if (r.my.isEmpty() && !v.isDefaults()) {
-      try {
-        VersionedAccountPreferences d = VersionedAccountPreferences.forDefault();
-        d.load(allUsers);
-        r.my = my(d);
-      } catch (ConfigInvalidException | IOException e) {
-        log.warn("cannot read default preferences", e);
-      }
+      r.my = my(d);
     }
     if (r.my.isEmpty()) {
       r.my.add(new MenuItem("Changes", "#/dashboard/self", null));
@@ -113,6 +135,9 @@
     }
 
     r.urlAliases = urlAliases(v);
+    if (r.urlAliases == null && !v.isDefaults()) {
+      r.urlAliases = urlAliases(d);
+    }
     return r;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java
new file mode 100644
index 0000000..2924f97
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.extensions.common.AgreementInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AgreementJson;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@Singleton
+public class GetAgreements implements RestReadView<AccountResource> {
+  private static final Logger log =
+      LoggerFactory.getLogger(GetAgreements.class);
+
+  private final Provider<IdentifiedUser> self;
+  private final ProjectCache projectCache;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final AgreementJson agreementJson;
+  private final boolean agreementsEnabled;
+
+  @Inject
+  GetAgreements(Provider<IdentifiedUser> self,
+      ProjectCache projectCache,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      AgreementJson agreementJson,
+      @GerritServerConfig Config config) {
+    this.self = self;
+    this.projectCache = projectCache;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.agreementJson = agreementJson;
+    this.agreementsEnabled =
+        config.getBoolean("auth", "contributorAgreements", false);
+  }
+
+  @Override
+  public List<AgreementInfo> apply(AccountResource resource)
+      throws RestApiException {
+    if (!agreementsEnabled) {
+      throw new MethodNotAllowedException("contributor agreements disabled");
+    }
+
+    if (self.get() != resource.getUser()) {
+      throw new AuthException("not allowed to get contributor agreements");
+    }
+
+    IdentifiedUser user =
+        identifiedUserFactory.create(self.get().getAccountId());
+
+    List<AgreementInfo> results = new ArrayList<>();
+    Collection<ContributorAgreement> cas =
+        projectCache.getAllProjects().getConfig().getContributorAgreements();
+    for (ContributorAgreement ca : cas) {
+      List<AccountGroup.UUID> groupIds = new ArrayList<>();
+      for (PermissionRule rule : ca.getAccepted()) {
+        if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)) {
+          if (rule.getGroup().getUUID() != null) {
+            groupIds.add(rule.getGroup().getUUID());
+          } else {
+            log.warn("group \"" + rule.getGroup().getName() + "\" does not " +
+                "exist, referenced in CLA \"" + ca.getName() + "\"");
+          }
+        }
+      }
+
+      if (user.getEffectiveGroups().containsAnyOf(groupIds)) {
+        results.add(agreementJson.format(ca));
+      }
+    }
+    return results;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java
index 81c860e..e47ceb3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java
@@ -47,7 +47,7 @@
       directory.fillAccountInfo(Collections.singleton(info),
           EnumSet.allOf(FillOptions.class));
     } catch (DirectoryException e) {
-      Throwables.propagateIfPossible(e.getCause(), OrmException.class);
+      Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
       throw new OrmException(e);
     }
     return info;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
index 14cc74e..99e6bd8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import java.util.ArrayList;
@@ -24,11 +26,20 @@
 
 @Singleton
 public class GetEmails implements RestReadView<AccountResource> {
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+
+  @Inject
+  GetEmails(IdentifiedUser.GenericFactory identifiedUserFactory) {
+    this.identifiedUserFactory = identifiedUserFactory;
+  }
 
   @Override
   public List<EmailInfo> apply(AccountResource rsrc) {
+    IdentifiedUser user =
+        identifiedUserFactory.create(rsrc.getUser().getAccountId());
+
     List<EmailInfo> emails = new ArrayList<>();
-    for (String email : rsrc.getUser().getEmailAddresses()) {
+    for (String email : user.getEmailAddresses()) {
       if (email != null) {
         EmailInfo e = new EmailInfo();
         e.email = email;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
index 842ec9d..7cda472 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
@@ -14,66 +14,120 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Strings;
+import com.google.common.collect.ComparisonChain;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 @Singleton
 public class GetWatchedProjects implements RestReadView<AccountResource> {
 
   private final Provider<ReviewDb> dbProvider;
   private final Provider<IdentifiedUser> self;
+  private final boolean readFromGit;
+  private final WatchConfig.Accessor watchConfig;
 
   @Inject
   public GetWatchedProjects(Provider<ReviewDb> dbProvider,
-      Provider<IdentifiedUser> self) {
+      Provider<IdentifiedUser> self,
+      @GerritServerConfig Config cfg,
+      WatchConfig.Accessor watchConfig) {
     this.dbProvider = dbProvider;
     this.self = self;
+    this.readFromGit =
+        cfg.getBoolean("user", null, "readProjectWatchesFromGit", true);
+    this.watchConfig = watchConfig;
   }
 
   @Override
   public List<ProjectWatchInfo> apply(AccountResource rsrc)
-      throws OrmException, AuthException {
-    if (self.get() != rsrc.getUser()) {
+      throws OrmException, AuthException, IOException, ConfigInvalidException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("It is not allowed to list project watches "
           + "of other users");
     }
+    Account.Id accountId = rsrc.getUser().getAccountId();
+    Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
+        readFromGit
+            ? watchConfig.getProjectWatches(accountId)
+            : readProjectWatchesFromDb(dbProvider.get(), accountId);
+
     List<ProjectWatchInfo> projectWatchInfos = new LinkedList<>();
-    Iterable<AccountProjectWatch> projectWatches =
-        dbProvider.get().accountProjectWatches()
-            .byAccount(rsrc.getUser().getAccountId());
-    for (AccountProjectWatch a : projectWatches) {
+    for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : projectWatches
+        .entrySet()) {
       ProjectWatchInfo pwi = new ProjectWatchInfo();
-      pwi.filter = a.getFilter();
-      pwi.project = a.getProjectNameKey().get();
+      pwi.filter = e.getKey().filter();
+      pwi.project = e.getKey().project().get();
       pwi.notifyAbandonedChanges =
-          toBoolean(
-              a.isNotify(AccountProjectWatch.NotifyType.ABANDONED_CHANGES));
+          toBoolean(e.getValue().contains(NotifyType.ABANDONED_CHANGES));
       pwi.notifyNewChanges =
-          toBoolean(a.isNotify(AccountProjectWatch.NotifyType.NEW_CHANGES));
+          toBoolean(e.getValue().contains(NotifyType.NEW_CHANGES));
       pwi.notifyNewPatchSets =
-          toBoolean(a.isNotify(AccountProjectWatch.NotifyType.NEW_PATCHSETS));
+          toBoolean(e.getValue().contains(NotifyType.NEW_PATCHSETS));
       pwi.notifySubmittedChanges =
-          toBoolean(
-              a.isNotify(AccountProjectWatch.NotifyType.SUBMITTED_CHANGES));
+          toBoolean(e.getValue().contains(NotifyType.SUBMITTED_CHANGES));
       pwi.notifyAllComments =
-          toBoolean(a.isNotify(AccountProjectWatch.NotifyType.ALL_COMMENTS));
+          toBoolean(e.getValue().contains(NotifyType.ALL_COMMENTS));
       projectWatchInfos.add(pwi);
     }
+    Collections.sort(projectWatchInfos, new Comparator<ProjectWatchInfo>() {
+      @Override
+      public int compare(ProjectWatchInfo pwi1, ProjectWatchInfo pwi2) {
+        return ComparisonChain.start()
+            .compare(pwi1.project, pwi2.project)
+            .compare(Strings.nullToEmpty(pwi1.filter),
+                Strings.nullToEmpty(pwi2.filter))
+            .result();
+      }
+    });
     return projectWatchInfos;
   }
 
   private static Boolean toBoolean(boolean value) {
     return value ? true : null;
   }
+
+  public static Map<ProjectWatchKey, Set<NotifyType>> readProjectWatchesFromDb(
+      ReviewDb db, Account.Id who) throws OrmException {
+    Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
+        new HashMap<>();
+    for (AccountProjectWatch apw : db.accountProjectWatches().byAccount(who)) {
+      ProjectWatchKey key =
+          ProjectWatchKey.create(apw.getProjectNameKey(), apw.getFilter());
+      Set<NotifyType> notifyValues = EnumSet.noneOf(NotifyType.class);
+      for (NotifyType notifyType : NotifyType.values()) {
+        if (apw.isNotify(notifyType)) {
+          notifyValues.add(notifyType);
+        }
+      }
+      projectWatches.put(key, notifyValues);
+    }
+    return projectWatches;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index 961d554..78a801e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -15,24 +15,23 @@
 package com.google.gerrit.server.account;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Multimap;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.AvatarInfo;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.avatar.AvatarProvider;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.List;
 import java.util.Set;
 
 @Singleton
@@ -47,17 +46,14 @@
     }
   }
 
-  private final Provider<ReviewDb> db;
   private final AccountCache accountCache;
   private final DynamicItem<AvatarProvider> avatar;
   private final IdentifiedUser.GenericFactory userFactory;
 
   @Inject
-  InternalAccountDirectory(Provider<ReviewDb> db,
-      AccountCache accountCache,
+  InternalAccountDirectory(AccountCache accountCache,
       DynamicItem<AvatarProvider> avatar,
       IdentifiedUser.GenericFactory userFactory) {
-    this.db = db;
     this.accountCache = accountCache;
     this.avatar = avatar;
     this.userFactory = userFactory;
@@ -71,35 +67,16 @@
     if (options.equals(ID_ONLY)) {
       return;
     }
-    Multimap<Account.Id, AccountInfo> missing = ArrayListMultimap.create();
     for (AccountInfo info : in) {
       Account.Id id = new Account.Id(info._accountId);
-      AccountState state = accountCache.getIfPresent(id);
-      if (state != null) {
-        fill(info, state.getAccount(), options);
-      } else {
-        missing.put(id, info);
-      }
-    }
-    if (!missing.isEmpty()) {
-      try {
-        for (Account account : db.get().accounts().get(missing.keySet())) {
-          if (options.contains(FillOptions.USERNAME)) {
-            account.setUserName(AccountState.getUserName(
-                db.get().accountExternalIds().byAccount(account.getId()).toList()));
-          }
-          for (AccountInfo info : missing.get(account.getId())) {
-            fill(info, account, options);
-          }
-        }
-      } catch (OrmException e) {
-        throw new DirectoryException(e);
-      }
+      AccountState state = accountCache.get(id);
+      fill(info, state.getAccount(), state.getExternalIds(), options);
     }
   }
 
   private void fill(AccountInfo info,
       Account account,
+      @Nullable Collection<AccountExternalId> externalIds,
       Set<FillOptions> options) {
     if (options.contains(FillOptions.ID)) {
       info._accountId = account.getId().get();
@@ -116,8 +93,15 @@
     if (options.contains(FillOptions.EMAIL)) {
       info.email = account.getPreferredEmail();
     }
+    if (options.contains(FillOptions.SECONDARY_EMAILS)) {
+      info.secondaryEmails = externalIds != null
+          ? getSecondaryEmails(account, externalIds)
+          : null;
+    }
     if (options.contains(FillOptions.USERNAME)) {
-      info.username = account.getUserName();
+      info.username = externalIds != null
+          ? AccountState.getUserName(externalIds)
+          : null;
     }
     if (options.contains(FillOptions.AVATARS)) {
       AvatarProvider ap = avatar.get();
@@ -141,6 +125,16 @@
     }
   }
 
+  public List<String> getSecondaryEmails(Account account,
+      Collection<AccountExternalId> externalIds) {
+    List<String> emails = new ArrayList<>(AccountState.getEmails(externalIds));
+    if (account.getPreferredEmail() != null) {
+      emails.remove(account.getPreferredEmail());
+    }
+    Collections.sort(emails);
+    return emails;
+  }
+
   private static void addAvatar(
       AvatarProvider provider,
       AccountInfo account,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
index 9604322..5b4a200 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -82,6 +82,9 @@
     put(ACCOUNT_KIND, "preferences.edit").to(SetEditPreferences.class);
     get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
 
+    get(ACCOUNT_KIND, "agreements").to(GetAgreements.class);
+    put(ACCOUNT_KIND, "agreements").to(PutAgreement.class);
+
     child(ACCOUNT_KIND, "starred.changes").to(StarredChanges.class);
     put(STARRED_CHANGE_KIND).to(StarredChanges.Put.class);
     delete(STARRED_CHANGE_KIND).to(StarredChanges.Delete.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java
index 8a87f90..d54ec50 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java
@@ -22,54 +22,72 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
 import java.io.IOException;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 @Singleton
 public class PostWatchedProjects
     implements RestModifyView<AccountResource, List<ProjectWatchInfo>> {
+  private final Provider<ReviewDb> dbProvider;
   private final Provider<IdentifiedUser> self;
   private final GetWatchedProjects getWatchedProjects;
-  private final Provider<ReviewDb> dbProvider;
   private final ProjectsCollection projectsCollection;
+  private final AccountCache accountCache;
+  private final WatchConfig.Accessor watchConfig;
 
   @Inject
-  public PostWatchedProjects(GetWatchedProjects getWatchedProjects,
-      Provider<ReviewDb> dbProvider,
+  public PostWatchedProjects(Provider<ReviewDb> dbProvider,
+      Provider<IdentifiedUser> self,
+      GetWatchedProjects getWatchedProjects,
       ProjectsCollection projectsCollection,
-      Provider<IdentifiedUser> self) {
-    this.getWatchedProjects = getWatchedProjects;
+      AccountCache accountCache,
+      WatchConfig.Accessor watchConfig) {
     this.dbProvider = dbProvider;
-    this.projectsCollection = projectsCollection;
     this.self = self;
+    this.getWatchedProjects = getWatchedProjects;
+    this.projectsCollection = projectsCollection;
+    this.accountCache = accountCache;
+    this.watchConfig = watchConfig;
   }
 
   @Override
   public List<ProjectWatchInfo> apply(AccountResource rsrc,
-      List<ProjectWatchInfo> input)
-      throws OrmException, RestApiException, IOException {
-    if (self.get() != rsrc.getUser()) {
+      List<ProjectWatchInfo> input) throws OrmException, RestApiException,
+          IOException, ConfigInvalidException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("not allowed to edit project watches");
     }
-    List<AccountProjectWatch> accountProjectWatchList =
-        getAccountProjectWatchList(input, rsrc.getUser().getAccountId());
-    dbProvider.get().accountProjectWatches().upsert(accountProjectWatchList);
+    Account.Id accountId = rsrc.getUser().getAccountId();
+    updateInDb(accountId, input);
+    updateInGit(accountId, input);
+    accountCache.evict(accountId);
     return getWatchedProjects.apply(rsrc);
   }
 
-  private List<AccountProjectWatch> getAccountProjectWatchList(
-      List<ProjectWatchInfo> input, Account.Id accountId)
-      throws UnprocessableEntityException, BadRequestException, IOException {
+  private void updateInDb(Account.Id accountId, List<ProjectWatchInfo> input)
+      throws BadRequestException, UnprocessableEntityException, IOException,
+      OrmException {
+    Set<AccountProjectWatch.Key> keys = new HashSet<>();
     List<AccountProjectWatch> watchedProjects = new LinkedList<>();
     for (ProjectWatchInfo a : input) {
       if (a.project == null) {
@@ -78,9 +96,12 @@
 
       Project.NameKey projectKey =
           projectsCollection.parse(a.project).getNameKey();
-
       AccountProjectWatch.Key key =
           new AccountProjectWatch.Key(accountId, projectKey, a.filter);
+      if (!keys.add(key)) {
+        throw new BadRequestException("duplicate entry for project "
+            + format(key.getProjectName().get(), key.getFilter().get()));
+      }
       AccountProjectWatch apw = new AccountProjectWatch(key);
       apw.setNotify(AccountProjectWatch.NotifyType.ABANDONED_CHANGES,
           toBoolean(a.notifyAbandonedChanges));
@@ -94,10 +115,61 @@
           toBoolean(a.notifySubmittedChanges));
       watchedProjects.add(apw);
     }
-    return watchedProjects;
+    dbProvider.get().accountProjectWatches().upsert(watchedProjects);
+  }
+
+  private void updateInGit(Account.Id accountId, List<ProjectWatchInfo> input)
+      throws BadRequestException, UnprocessableEntityException, IOException,
+      ConfigInvalidException {
+    watchConfig.upsertProjectWatches(accountId, asMap(input));
+  }
+
+  private Map<ProjectWatchKey, Set<NotifyType>> asMap(
+      List<ProjectWatchInfo> input) throws BadRequestException,
+          UnprocessableEntityException, IOException {
+    Map<ProjectWatchKey, Set<NotifyType>> m = new HashMap<>();
+    for (ProjectWatchInfo info : input) {
+      if (info.project == null) {
+        throw new BadRequestException("project name must be specified");
+      }
+
+      ProjectWatchKey key = ProjectWatchKey.create(
+          projectsCollection.parse(info.project).getNameKey(), info.filter);
+      if (m.containsKey(key)) {
+        throw new BadRequestException(
+            "duplicate entry for project " + format(info.project, info.filter));
+      }
+
+      Set<NotifyType> notifyValues = EnumSet.noneOf(NotifyType.class);
+      if (toBoolean(info.notifyAbandonedChanges)) {
+        notifyValues.add(NotifyType.ABANDONED_CHANGES);
+      }
+      if (toBoolean(info.notifyAllComments)) {
+        notifyValues.add(NotifyType.ALL_COMMENTS);
+      }
+      if (toBoolean(info.notifyNewChanges)) {
+        notifyValues.add(NotifyType.NEW_CHANGES);
+      }
+      if (toBoolean(info.notifyNewPatchSets)) {
+        notifyValues.add(NotifyType.NEW_PATCHSETS);
+      }
+      if (toBoolean(info.notifySubmittedChanges)) {
+        notifyValues.add(NotifyType.SUBMITTED_CHANGES);
+      }
+
+      m.put(key, notifyValues);
+    }
+    return m;
   }
 
   private boolean toBoolean(Boolean b) {
     return b == null ? false : b;
   }
+
+  private static String format(String project, String filter) {
+    return project
+        + (filter != null && !AccountProjectWatch.FILTER_ALL.equals(filter)
+            ? " and filter " + filter
+            : "");
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
index 17e177f..9197011 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
@@ -14,15 +14,16 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.api.accounts.AccountInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.account.CreateAccount.Input;
 import com.google.inject.Singleton;
 
 @Singleton
-public class PutAccount implements RestModifyView<AccountResource, Input> {
+public class PutAccount
+    implements RestModifyView<AccountResource, AccountInput> {
   @Override
-  public Object apply(AccountResource resource, Input input)
+  public Object apply(AccountResource resource, AccountInput input)
       throws ResourceConflictException {
     throw new ResourceConflictException("account exists");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAgreement.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAgreement.java
new file mode 100644
index 0000000..2fdf666
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAgreement.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.extensions.common.AgreementInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.extensions.events.AgreementSignup;
+import com.google.gerrit.server.group.AddMembers;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+
+@Singleton
+public class PutAgreement
+    implements RestModifyView<AccountResource, AgreementInput> {
+  private final ProjectCache projectCache;
+  private final GroupCache groupCache;
+  private final Provider<IdentifiedUser> self;
+  private final AgreementSignup agreementSignup;
+  private final AddMembers addMembers;
+  private final boolean agreementsEnabled;
+
+  @Inject
+  PutAgreement(ProjectCache projectCache,
+      GroupCache groupCache,
+      Provider<IdentifiedUser> self,
+      AgreementSignup agreementSignup,
+      AddMembers addMembers,
+      @GerritServerConfig Config config) {
+    this.projectCache = projectCache;
+    this.groupCache = groupCache;
+    this.self = self;
+    this.agreementSignup = agreementSignup;
+    this.addMembers = addMembers;
+    this.agreementsEnabled =
+        config.getBoolean("auth", "contributorAgreements", false);
+  }
+
+  @Override
+  public Object apply(AccountResource resource, AgreementInput input)
+      throws IOException, OrmException, RestApiException {
+    if (!agreementsEnabled) {
+      throw new MethodNotAllowedException("contributor agreements disabled");
+    }
+
+    if (self.get() != resource.getUser()) {
+      throw new AuthException("not allowed to enter contributor agreement");
+    }
+
+    String agreementName = Strings.nullToEmpty(input.name);
+    ContributorAgreement ca = projectCache.getAllProjects().getConfig()
+        .getContributorAgreement(agreementName);
+    if (ca == null) {
+      throw new UnprocessableEntityException("contributor agreement not found");
+    }
+
+    if (ca.getAutoVerify() == null) {
+      throw new BadRequestException("cannot enter a non-autoVerify agreement");
+    }
+
+    AccountGroup.UUID uuid = ca.getAutoVerify().getUUID();
+    if (uuid == null) {
+      throw new ResourceConflictException("autoverify group uuid not found");
+    }
+
+    AccountGroup group = groupCache.get(uuid);
+    if (group == null) {
+      throw new ResourceConflictException("autoverify group not found");
+    }
+
+    Account account = self.get().getAccount();
+    addMembers.addMembers(group.getId(), ImmutableList.of(account.getId()));
+    agreementSignup.fire(account, agreementName);
+
+    return agreementName;
+  }
+
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
index 6338b15..74c07e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -14,9 +14,8 @@
 
 package com.google.gerrit.server.account;
 
-import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
-
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -24,13 +23,10 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.PutName.Input;
-import com.google.gerrit.server.auth.ldap.LdapRealm;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -77,18 +73,15 @@
     if (input == null) {
       input = new Input();
     }
-    ReviewDb db = dbProvider.get();
-    Account a = db.accounts().get(user.getAccountId());
-    if (a == null) {
-      throw new ResourceNotFoundException("account not found");
-    }
 
-    if (!realm.allowsEdit(FieldName.FULL_NAME)
-        && !(realm instanceof LdapRealm && db.accountExternalIds().get(
-            new AccountExternalId.Key(SCHEME_GERRIT, a.getUserName())) == null)) {
+    if (!realm.allowsEdit(AccountFieldName.FULL_NAME)) {
       throw new MethodNotAllowedException("realm does not allow editing name");
     }
 
+    Account a = dbProvider.get().accounts().get(user.getAccountId());
+    if (a == null) {
+      throw new ResourceNotFoundException("account not found");
+    }
     a.setFullName(input.name);
     dbProvider.get().accounts().update(Collections.singleton(a));
     byIdCache.evict(a.getId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
index e9dc393..29168ed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.PutUsername.Input;
@@ -64,7 +64,7 @@
       throw new AuthException("not allowed to set username");
     }
 
-    if (!realm.allowsEdit(Account.FieldName.USER_NAME)) {
+    if (!realm.allowsEdit(AccountFieldName.USER_NAME)) {
       throw new MethodNotAllowedException("realm does not allow editing username");
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java
index 7c3ef1e..000637a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.api.accounts.AccountInfoComparator;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.account.AccountIndex;
@@ -46,6 +47,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 public class QueryAccounts implements RestReadView<TopLevelResource> {
   private static final int MAX_SUGGEST_RESULTS = 100;
@@ -152,8 +154,20 @@
       return Collections.emptyList();
     }
 
-    accountLoader = accountLoaderFactory
-        .create(suggest || options.contains(ListAccountsOption.DETAILS));
+    Set<FillOptions> fillOptions = EnumSet.of(FillOptions.ID);
+    if (options.contains(ListAccountsOption.DETAILS)) {
+      fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
+    }
+    if (options.contains(ListAccountsOption.ALL_EMAILS)) {
+      fillOptions.add(FillOptions.EMAIL);
+      fillOptions.add(FillOptions.SECONDARY_EMAILS);
+    }
+    if (suggest) {
+      fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
+      fillOptions.add(FillOptions.EMAIL);
+      fillOptions.add(FillOptions.SECONDARY_EMAILS);
+    }
+    accountLoader = accountLoaderFactory.create(fillOptions);
 
     AccountIndex searchIndex = indexes.getSearchIndex();
     if (searchIndex != null) {
@@ -183,7 +197,7 @@
     try {
       Predicate<AccountState> queryPred;
       if (suggest) {
-        queryPred = queryBuilder.defaultField(query);
+        queryPred = queryBuilder.defaultQuery(query);
         queryProcessor.setLimit(suggestLimit);
       } else {
         queryPred = queryBuilder.parse(query);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index 85fde4e..627f529 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 
@@ -21,10 +22,10 @@
 
 public interface Realm {
   /** Can the end-user modify this field of their own account? */
-  boolean allowsEdit(Account.FieldName field);
+  boolean allowsEdit(AccountFieldName field);
 
   /** Returns the account fields that the end-user can modify. */
-  Set<Account.FieldName> getEditableFields();
+  Set<AccountFieldName> getEditableFields();
 
   AuthRequest authenticate(AuthRequest who) throws AccountException;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index eb32e5a..b70cabd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -140,7 +140,7 @@
     }
   }
 
-  private static void storeUrlAliases(VersionedAccountPreferences prefs,
+  public static void storeUrlAliases(VersionedAccountPreferences prefs,
       Map<String, String> urlAliases) {
     if (urlAliases != null) {
       Config cfg = prefs.getConfig();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index dc96d49..30a3bdf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account;
 
-import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.common.base.Function;
@@ -69,8 +68,7 @@
  * Other comment lines are ignored on read, and are not written back when the
  * file is modified.
  */
-public class VersionedAuthorizedKeys extends VersionedMetaData
-    implements AutoCloseable {
+public class VersionedAuthorizedKeys extends VersionedMetaData {
   @Singleton
   public static class Accessor {
     private final GitRepositoryManager repoManager;
@@ -105,28 +103,25 @@
 
     public AccountSshKey addKey(Account.Id accountId, String pub)
         throws IOException, ConfigInvalidException, InvalidSshKeyException {
-      try (VersionedAuthorizedKeys authorizedKeys = open(accountId)) {
-        AccountSshKey key = authorizedKeys.addKey(pub);
-        commit(authorizedKeys);
-        return key;
-      }
+      VersionedAuthorizedKeys authorizedKeys = read(accountId);
+      AccountSshKey key = authorizedKeys.addKey(pub);
+      commit(authorizedKeys);
+      return key;
     }
 
     public void deleteKey(Account.Id accountId, int seq)
         throws IOException, ConfigInvalidException {
-      try (VersionedAuthorizedKeys authorizedKeys = open(accountId)) {
-        if (authorizedKeys.deleteKey(seq)) {
-          commit(authorizedKeys);
-        }
+      VersionedAuthorizedKeys authorizedKeys = read(accountId);
+      if (authorizedKeys.deleteKey(seq)) {
+        commit(authorizedKeys);
       }
     }
 
     public void markKeyInvalid(Account.Id accountId, int seq)
         throws IOException, ConfigInvalidException {
-      try (VersionedAuthorizedKeys authorizedKeys = open(accountId)) {
-        if (authorizedKeys.markKeyInvalid(seq)) {
-          commit(authorizedKeys);
-        }
+      VersionedAuthorizedKeys authorizedKeys = read(accountId);
+      if (authorizedKeys.markKeyInvalid(seq)) {
+        commit(authorizedKeys);
       }
     }
 
@@ -140,15 +135,6 @@
       }
     }
 
-    private VersionedAuthorizedKeys open(Account.Id accountId)
-        throws IOException, ConfigInvalidException {
-      Repository git = repoManager.openRepository(allUsersName);
-      VersionedAuthorizedKeys authorizedKeys =
-          authorizedKeysFactory.create(accountId);
-      authorizedKeys.load(git);
-      return authorizedKeys;
-    }
-
     private void commit(VersionedAuthorizedKeys authorizedKeys)
         throws IOException {
       try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName,
@@ -172,7 +158,6 @@
   private final SshKeyCreator sshKeyCreator;
   private final Account.Id accountId;
   private final String ref;
-  private Repository git;
   private List<Optional<AccountSshKey>> keys;
 
   @Inject
@@ -190,13 +175,6 @@
   }
 
   @Override
-  public void load(Repository git) throws IOException, ConfigInvalidException {
-    checkState(this.git == null);
-    this.git = git;
-    super.load(git);
-  }
-
-  @Override
   protected void onLoad() throws IOException {
     keys = AuthorizedKeys.parse(accountId, readUTF8(AuthorizedKeys.FILE_NAME));
   }
@@ -314,14 +292,7 @@
     }
   }
 
-  @Override
-  public void close() {
-    if (git != null) {
-      git.close();
-    }
-  }
-
   private void checkLoaded() {
-    checkNotNull(keys, "SSH keys not loaded yet");
+    checkState(keys != null, "SSH keys not loaded yet");
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java
new file mode 100644
index 0000000..c3d28ca
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java
@@ -0,0 +1,358 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Enums;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ValidationError;
+import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * ‘watch.config’ file in the user branch in the All-Users repository that
+ * contains the watch configuration of the user.
+ * <p>
+ * The 'watch.config' file is a git config file that has one 'project' section
+ * for all project watches of a project.
+ * <p>
+ * The project name is used as subsection name and the filters with the notify
+ * types that decide for which events email notifications should be sent are
+ * represented as 'notify' values in the subsection. A 'notify' value is
+ * formatted as {@code <filter> [<comma-separated-list-of-notify-types>]}:
+ *
+ * <pre>
+ *   [project "foo"]
+ *     notify = * [ALL_COMMENTS]
+ *     notify = branch:master [ALL_COMMENTS, NEW_PATCHSETS]
+ *     notify = branch:master owner:self [SUBMITTED_CHANGES]
+ * </pre>
+ * <p>
+ * If two notify values in the same subsection have the same filter they are
+ * merged on the next save, taking the union of the notify types.
+ * <p>
+ * For watch configurations that notify on no event the list of notify types is
+ * empty:
+ *
+ * <pre>
+ *   [project "foo"]
+ *     notify = branch:master []
+ * </pre>
+ * <p>
+ * Unknown notify types are ignored and removed on save.
+ */
+public class WatchConfig extends VersionedMetaData
+    implements ValidationError.Sink {
+  @Singleton
+  public static class Accessor {
+    private final GitRepositoryManager repoManager;
+    private final AllUsersName allUsersName;
+    private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+    private final IdentifiedUser.GenericFactory userFactory;
+
+    @Inject
+    Accessor(
+        GitRepositoryManager repoManager,
+        AllUsersName allUsersName,
+        Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+        IdentifiedUser.GenericFactory userFactory) {
+      this.repoManager = repoManager;
+      this.allUsersName = allUsersName;
+      this.metaDataUpdateFactory = metaDataUpdateFactory;
+      this.userFactory = userFactory;
+    }
+
+    public Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches(
+        Account.Id accountId) throws IOException, ConfigInvalidException {
+      try (Repository git = repoManager.openRepository(allUsersName)) {
+        WatchConfig watchConfig = new WatchConfig(accountId);
+        watchConfig.load(git);
+        return watchConfig.getProjectWatches();
+      }
+    }
+
+    public void upsertProjectWatches(Account.Id accountId,
+        Map<ProjectWatchKey, Set<NotifyType>> newProjectWatches)
+        throws IOException, ConfigInvalidException {
+      WatchConfig watchConfig = read(accountId);
+      Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
+          watchConfig.getProjectWatches();
+      projectWatches.putAll(newProjectWatches);
+      commit(watchConfig);
+    }
+
+    public void deleteProjectWatches(Account.Id accountId,
+        Collection<ProjectWatchKey> projectWatchKeys)
+            throws IOException, ConfigInvalidException {
+      WatchConfig watchConfig = read(accountId);
+      Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
+          watchConfig.getProjectWatches();
+      boolean commit = false;
+      for (ProjectWatchKey key : projectWatchKeys) {
+        if (projectWatches.remove(key) != null) {
+          commit = true;
+        }
+      }
+      if (commit) {
+        commit(watchConfig);
+      }
+    }
+
+    private WatchConfig read(Account.Id accountId)
+        throws IOException, ConfigInvalidException {
+      try (Repository git = repoManager.openRepository(allUsersName)) {
+        WatchConfig watchConfig = new WatchConfig(accountId);
+        watchConfig.load(git);
+        return watchConfig;
+      }
+    }
+
+    private void commit(WatchConfig watchConfig)
+        throws IOException {
+      try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName,
+          userFactory.create(watchConfig.accountId))) {
+        watchConfig.commit(md);
+      }
+    }
+  }
+
+  @AutoValue
+  public abstract static class ProjectWatchKey {
+    public static ProjectWatchKey create(Project.NameKey project,
+        @Nullable String filter) {
+      return new AutoValue_WatchConfig_ProjectWatchKey(project,
+          Strings.emptyToNull(filter));
+    }
+
+    public abstract Project.NameKey project();
+    public abstract @Nullable String filter();
+  }
+
+  public static final String WATCH_CONFIG = "watch.config";
+  public static final String PROJECT = "project";
+  public static final String KEY_NOTIFY = "notify";
+
+  private final Account.Id accountId;
+  private final String ref;
+
+  private Map<ProjectWatchKey, Set<NotifyType>> projectWatches;
+  private List<ValidationError> validationErrors;
+
+  public WatchConfig(Account.Id accountId) {
+    this.accountId = accountId;
+    this.ref = RefNames.refsUsers(accountId);
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    Config cfg = readConfig(WATCH_CONFIG);
+    projectWatches = parse(accountId, cfg, this);
+  }
+
+  @VisibleForTesting
+  public static Map<ProjectWatchKey, Set<NotifyType>> parse(
+      Account.Id accountId, Config cfg,
+      ValidationError.Sink validationErrorSink) {
+    Map<ProjectWatchKey, Set<NotifyType>> projectWatches = new HashMap<>();
+    for (String projectName : cfg.getSubsections(PROJECT)) {
+      String[] notifyValues =
+          cfg.getStringList(PROJECT, projectName, KEY_NOTIFY);
+      for (String nv : notifyValues) {
+        if (Strings.isNullOrEmpty(nv)) {
+          continue;
+        }
+
+        NotifyValue notifyValue =
+            NotifyValue.parse(accountId, projectName, nv, validationErrorSink);
+        if (notifyValue == null) {
+          continue;
+        }
+
+        ProjectWatchKey key = ProjectWatchKey
+            .create(new Project.NameKey(projectName), notifyValue.filter());
+        if (!projectWatches.containsKey(key)) {
+          projectWatches.put(key, EnumSet.noneOf(NotifyType.class));
+        }
+        projectWatches.get(key).addAll(notifyValue.notifyTypes());
+      }
+    }
+    return projectWatches;
+  }
+
+  Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches() {
+    checkLoaded();
+    return projectWatches;
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit)
+      throws IOException, ConfigInvalidException {
+    checkLoaded();
+
+    if (Strings.isNullOrEmpty(commit.getMessage())) {
+      commit.setMessage("Updated watch configuration\n");
+    }
+
+    Config cfg = readConfig(WATCH_CONFIG);
+
+    for (String projectName : cfg.getSubsections(PROJECT)) {
+      cfg.unset(PROJECT, projectName, KEY_NOTIFY);
+    }
+
+    Multimap<String, String> notifyValuesByProject = ArrayListMultimap.create();
+    for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : projectWatches
+        .entrySet()) {
+      NotifyValue notifyValue =
+          NotifyValue.create(e.getKey().filter(), e.getValue());
+      notifyValuesByProject.put(e.getKey().project().get(),
+          notifyValue.toString());
+    }
+
+    for (Map.Entry<String, Collection<String>> e : notifyValuesByProject.asMap()
+        .entrySet()) {
+      cfg.setStringList(PROJECT, e.getKey(), KEY_NOTIFY,
+          new ArrayList<>(e.getValue()));
+    }
+
+    saveConfig(WATCH_CONFIG, cfg);
+    return true;
+  }
+
+  private void checkLoaded() {
+    checkState(projectWatches != null, "project watches not loaded yet");
+  }
+
+  @Override
+  public void error(ValidationError error) {
+    if (validationErrors == null) {
+      validationErrors = new ArrayList<>(4);
+    }
+    validationErrors.add(error);
+  }
+
+  /**
+   * Get the validation errors, if any were discovered during load.
+   *
+   * @return list of errors; empty list if there are no errors.
+   */
+  public List<ValidationError> getValidationErrors() {
+    if (validationErrors != null) {
+      return ImmutableList.copyOf(validationErrors);
+    }
+    return ImmutableList.of();
+  }
+
+  @AutoValue
+  public abstract static class NotifyValue {
+    public static NotifyValue parse(Account.Id accountId, String project,
+        String notifyValue, ValidationError.Sink validationErrorSink) {
+      notifyValue = notifyValue.trim();
+      int i = notifyValue.lastIndexOf('[');
+      if (i < 0 || notifyValue.charAt(notifyValue.length() - 1) != ']') {
+        validationErrorSink.error(new ValidationError(WATCH_CONFIG,
+            String.format(
+                "Invalid project watch of account %d for project %s: %s",
+                accountId.get(), project, notifyValue)));
+        return null;
+      }
+      String filter = notifyValue.substring(0, i).trim();
+      if (filter.isEmpty() || AccountProjectWatch.FILTER_ALL.equals(filter)) {
+        filter = null;
+      }
+
+      Set<NotifyType> notifyTypes = EnumSet.noneOf(NotifyType.class);
+      if (i + 1 < notifyValue.length() - 2) {
+        for (String nt : Splitter.on(',').trimResults().splitToList(
+            notifyValue.substring(i + 1, notifyValue.length() - 1))) {
+          Optional<NotifyType> notifyType =
+              Enums.getIfPresent(NotifyType.class, nt);
+          if (!notifyType.isPresent()) {
+            validationErrorSink.error(new ValidationError(WATCH_CONFIG,
+                String.format(
+                    "Invalid notify type %s in project watch "
+                        + "of account %d for project %s: %s",
+                    nt, accountId.get(), project, notifyValue)));
+            continue;
+          }
+          notifyTypes.add(notifyType.get());
+        }
+      }
+      return create(filter, notifyTypes);
+    }
+
+    public static NotifyValue create(@Nullable String filter,
+        Set<NotifyType> notifyTypes) {
+      return new AutoValue_WatchConfig_NotifyValue(Strings.emptyToNull(filter),
+          Sets.immutableEnumSet(notifyTypes));
+    }
+
+    public abstract @Nullable String filter();
+    public abstract ImmutableSet<NotifyType> notifyTypes();
+
+    @Override
+    public String toString() {
+      List<NotifyType> notifyTypes = new ArrayList<>(notifyTypes());
+      StringBuilder notifyValue = new StringBuilder();
+      notifyValue.append(firstNonNull(filter(), AccountProjectWatch.FILTER_ALL))
+          .append(" [");
+      Joiner.on(", ").appendTo(notifyValue, notifyTypes);
+      notifyValue.append("]");
+      return notifyValue.toString();
+    }
+  }
+}
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 2038276..2af9f1d 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
@@ -25,6 +25,8 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.AgreementInfo;
+import com.google.gerrit.extensions.common.AgreementInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.SshKeyInfo;
@@ -38,6 +40,7 @@
 import com.google.gerrit.server.account.CreateEmail;
 import com.google.gerrit.server.account.DeleteSshKey;
 import com.google.gerrit.server.account.DeleteWatchedProjects;
+import com.google.gerrit.server.account.GetAgreements;
 import com.google.gerrit.server.account.GetAvatar;
 import com.google.gerrit.server.account.GetDiffPreferences;
 import com.google.gerrit.server.account.GetEditPreferences;
@@ -45,6 +48,7 @@
 import com.google.gerrit.server.account.GetSshKeys;
 import com.google.gerrit.server.account.GetWatchedProjects;
 import com.google.gerrit.server.account.PostWatchedProjects;
+import com.google.gerrit.server.account.PutAgreement;
 import com.google.gerrit.server.account.SetDiffPreferences;
 import com.google.gerrit.server.account.SetEditPreferences;
 import com.google.gerrit.server.account.SetPreferences;
@@ -93,6 +97,8 @@
   private final AddSshKey addSshKey;
   private final DeleteSshKey deleteSshKey;
   private final SshKeys sshKeys;
+  private final GetAgreements getAgreements;
+  private final PutAgreement putAgreement;
 
   @Inject
   AccountApiImpl(AccountLoader.Factory ailf,
@@ -118,6 +124,8 @@
       AddSshKey addSshKey,
       DeleteSshKey deleteSshKey,
       SshKeys sshKeys,
+      GetAgreements getAgreements,
+      PutAgreement putAgreement,
       @Assisted AccountResource account) {
     this.account = account;
     this.accountLoaderFactory = ailf;
@@ -143,6 +151,8 @@
     this.deleteSshKey = deleteSshKey;
     this.sshKeys = sshKeys;
     this.gpgApiAdapter = gpgApiAdapter;
+    this.getAgreements = getAgreements;
+    this.putAgreement = putAgreement;
   }
 
   @Override
@@ -221,7 +231,7 @@
   public List<ProjectWatchInfo> getWatchedProjects() throws RestApiException {
     try {
       return getWatchedProjects.apply(account);
-    } catch (OrmException e) {
+    } catch (OrmException | IOException | ConfigInvalidException e) {
       throw new RestApiException("Cannot get watched projects", e);
     }
   }
@@ -231,7 +241,7 @@
       List<ProjectWatchInfo> in) throws RestApiException {
     try {
       return postWatchedProjects.apply(account, in);
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | IOException | ConfigInvalidException e) {
       throw new RestApiException("Cannot update watched projects", e);
     }
   }
@@ -241,7 +251,7 @@
       throws RestApiException {
     try {
       deleteWatchedProjects.apply(account, in);
-    } catch (OrmException e) {
+    } catch (OrmException | IOException | ConfigInvalidException e) {
       throw new RestApiException("Cannot delete watched projects", e);
     }
   }
@@ -374,4 +384,21 @@
       throw new RestApiException("Cannot get PGP key", e);
     }
   }
+
+  @Override
+  public List<AgreementInfo> listAgreements() throws RestApiException {
+    return getAgreements.apply(account);
+  }
+
+  @Override
+  public void signAgreement(String agreementName) throws RestApiException {
+    try {
+      AgreementInput input = new AgreementInput();
+      input.name = agreementName;
+      putAgreement.apply(account, input);
+    } catch (IOException | OrmException e) {
+      throw new RestApiException("Cannot sign agreement", e);
+    }
+  }
+
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
index 39e8b51..6a248f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -14,23 +14,32 @@
 
 package com.google.gerrit.server.api.accounts;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
+
 import com.google.gerrit.extensions.api.accounts.AccountApi;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
 import com.google.gerrit.extensions.api.accounts.Accounts;
 import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountsCollection;
+import com.google.gerrit.server.account.CreateAccount;
 import com.google.gerrit.server.account.QueryAccounts;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
 import java.util.List;
 
 @Singleton
@@ -38,16 +47,19 @@
   private final AccountsCollection accounts;
   private final AccountApiImpl.Factory api;
   private final Provider<CurrentUser> self;
+  private final CreateAccount.Factory createAccount;
   private final Provider<QueryAccounts> queryAccountsProvider;
 
   @Inject
   AccountsImpl(AccountsCollection accounts,
       AccountApiImpl.Factory api,
       Provider<CurrentUser> self,
+      CreateAccount.Factory createAccount,
       Provider<QueryAccounts> queryAccountsProvider) {
     this.accounts = accounts;
     this.api = api;
     this.self = self;
+    this.createAccount = createAccount;
     this.queryAccountsProvider = queryAccountsProvider;
   }
 
@@ -75,6 +87,28 @@
   }
 
   @Override
+  public AccountApi create(String username) throws RestApiException {
+    AccountInput in = new AccountInput();
+    in.username = username;
+    return create(in);
+  }
+
+  @Override
+  public AccountApi create(AccountInput in) throws RestApiException {
+    if (checkNotNull(in, "AccountInput").username == null) {
+      throw new BadRequestException("AccountInput must specify username");
+    }
+    checkRequiresCapability(self, null, CreateAccount.class);
+    try {
+      AccountInfo info = createAccount.create(in.username)
+          .apply(TopLevelResource.INSTANCE, in).value();
+      return id(info._accountId);
+    } catch (OrmException | IOException | ConfigInvalidException e) {
+      throw new RestApiException("Cannot create account " + in.username, e);
+    }
+  }
+
+  @Override
   public SuggestAccountsRequest suggestAccounts() throws RestApiException {
     return new SuggestAccountsRequest() {
       @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java
index 7d65ce9..a83110c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java
@@ -27,6 +27,8 @@
 import java.util.Map;
 
 public interface GpgApiAdapter {
+  boolean isEnabled();
+
   Map<String, GpgKeyInfo> listGpgKeys(AccountResource account)
       throws RestApiException, GpgException;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
index 3d40373..e6ca18df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -75,6 +75,15 @@
   }
 
   @Override
+  public DiffInfo diff(int parent) throws RestApiException {
+    try {
+      return getDiff.setParent(parent).apply(file).value();
+    } catch (OrmException | InvalidChangeOperationException | IOException e) {
+      throw new RestApiException("Cannot retrieve diff", e);
+    }
+  }
+
+  @Override
   public DiffRequest diffRequest() {
     return new DiffRequest() {
       @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 4c7adea..213f90d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -166,7 +166,7 @@
   public void review(ReviewInput in) throws RestApiException {
     try {
       review.apply(revision, in);
-    } catch (OrmException | UpdateException e) {
+    } catch (OrmException | UpdateException | IOException e) {
       throw new RestApiException("Cannot post review", e);
     }
   }
@@ -309,10 +309,25 @@
     }
   }
 
+  @SuppressWarnings("unchecked")
   @Override
-  public FileApi file(String path) {
-    return fileApi.create(files.parse(revision,
-        IdString.fromDecoded(path)));
+  public Map<String, FileInfo> files(int parentNum) throws RestApiException {
+    try {
+      return (Map<String, FileInfo>) listFiles.setParent(parentNum)
+          .apply(revision).value();
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot retrieve files", e);
+    }
+  }
+
+  @Override
+  public FileApi file(String path) throws RestApiException {
+    try {
+      return fileApi.create(files.parse(revision,
+          IdString.fromDecoded(path)));
+    } catch (IOException e) {
+      throw new RestApiException("Cannot retrieve file", e);
+    }
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
index e8b4fc8..f433d2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -17,10 +17,15 @@
 import com.google.gerrit.common.Version;
 import com.google.gerrit.extensions.api.config.Server;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.GetDiffPreferences;
+import com.google.gerrit.server.config.GetPreferences;
+import com.google.gerrit.server.config.GetServerInfo;
 import com.google.gerrit.server.config.SetDiffPreferences;
+import com.google.gerrit.server.config.SetPreferences;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -30,14 +35,23 @@
 
 @Singleton
 public class ServerImpl implements Server {
+  private final GetPreferences getPreferences;
+  private final SetPreferences setPreferences;
   private final GetDiffPreferences getDiffPreferences;
   private final SetDiffPreferences setDiffPreferences;
+  private final GetServerInfo getServerInfo;
 
   @Inject
-  ServerImpl(GetDiffPreferences getDiffPreferences,
-      SetDiffPreferences setDiffPreferences) {
+  ServerImpl(GetPreferences getPreferences,
+      SetPreferences setPreferences,
+      GetDiffPreferences getDiffPreferences,
+      SetDiffPreferences setDiffPreferences,
+      GetServerInfo getServerInfo) {
+    this.getPreferences = getPreferences;
+    this.setPreferences = setPreferences;
     this.getDiffPreferences = getDiffPreferences;
     this.setDiffPreferences = setDiffPreferences;
+    this.getServerInfo = getServerInfo;
   }
 
   @Override
@@ -46,6 +60,35 @@
   }
 
   @Override
+  public ServerInfo getInfo() throws RestApiException {
+    try {
+      return getServerInfo.apply(new ConfigResource());
+    } catch (IOException e) {
+      throw new RestApiException("Cannot get server info", e);
+    }
+  }
+
+  @Override
+  public GeneralPreferencesInfo getDefaultPreferences()
+      throws RestApiException {
+    try {
+      return getPreferences.apply(new ConfigResource());
+    } catch (IOException | ConfigInvalidException e) {
+      throw new RestApiException("Cannot get default general preferences", e);
+    }
+  }
+
+  @Override
+  public GeneralPreferencesInfo setDefaultPreferences(
+      GeneralPreferencesInfo in) throws RestApiException {
+    try {
+      return setPreferences.apply(new ConfigResource(), in);
+    } catch (IOException | ConfigInvalidException e) {
+      throw new RestApiException("Cannot set default general preferences", e);
+    }
+  }
+
+  @Override
   public DiffPreferencesInfo getDefaultDiffPreferences()
       throws RestApiException {
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index 51f85fe..4cb96b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.server.args4j;
 
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountResolver;
@@ -23,6 +24,7 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.kohsuke.args4j.CmdLineException;
@@ -35,29 +37,34 @@
 import java.io.IOException;
 
 public class AccountIdHandler extends OptionHandler<Account.Id> {
+  private final Provider<ReviewDb> db;
   private final AccountResolver accountResolver;
   private final AccountManager accountManager;
   private final AuthType authType;
 
   @Inject
-  public AccountIdHandler(final AccountResolver accountResolver,
-      final AccountManager accountManager,
-      final AuthConfig authConfig,
-      @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
-      @Assisted final Setter<Account.Id> setter) {
+  public AccountIdHandler(
+      Provider<ReviewDb> db,
+      AccountResolver accountResolver,
+      AccountManager accountManager,
+      AuthConfig authConfig,
+      @Assisted CmdLineParser parser,
+      @Assisted OptionDef option,
+      @Assisted Setter<Account.Id> setter) {
     super(parser, option, setter);
+    this.db = db;
     this.accountResolver = accountResolver;
     this.accountManager = accountManager;
     this.authType = authConfig.getAuthType();
   }
 
   @Override
-  public final int parseArguments(final Parameters params)
+  public int parseArguments(Parameters params)
       throws CmdLineException {
-    final String token = params.getParameter(0);
-    final Account.Id accountId;
+    String token = params.getParameter(0);
+    Account.Id accountId;
     try {
-      final Account a = accountResolver.find(token);
+      Account a = accountResolver.find(db.get(), token);
       if (a != null) {
         accountId = a.getId();
       } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
index 3567811..354dc62 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -154,8 +154,8 @@
           }
         });
     } catch (PrivilegedActionException e) {
-      Throwables.propagateIfPossible(e.getException(), NamingException.class);
-      Throwables.propagateIfPossible(e.getException(), RuntimeException.class);
+      Throwables.throwIfInstanceOf(e.getException(), NamingException.class);
+      Throwables.throwIfInstanceOf(e.getException(), RuntimeException.class);
       LdapRealm.log.warn("Internal error", e.getException());
       return null;
     } finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
index 8dc7177..3dddf4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.auth.ldap;
 
-import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.auth.AuthBackend;
 import com.google.gerrit.server.auth.AuthException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index d55bbc3..603efe0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -21,10 +21,11 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AbstractRealm;
 import com.google.gerrit.server.account.AccountException;
@@ -58,7 +59,7 @@
 import javax.security.auth.login.LoginException;
 
 @Singleton
-public class LdapRealm extends AbstractRealm {
+class LdapRealm extends AbstractRealm {
   static final Logger log = LoggerFactory.getLogger(LdapRealm.class);
   static final String LDAP = "com.sun.jndi.ldap.LdapCtxFactory";
   static final String USERNAME = "username";
@@ -67,7 +68,7 @@
   private final AuthConfig authConfig;
   private final EmailExpander emailExpander;
   private final LoadingCache<String, Optional<Account.Id>> usernameCache;
-  private final Set<Account.FieldName> readOnlyAccountFields;
+  private final Set<AccountFieldName> readOnlyAccountFields;
   private final boolean fetchMemberOfEagerly;
   private final Config config;
 
@@ -91,13 +92,13 @@
     this.readOnlyAccountFields = new HashSet<>();
 
     if (optdef(config, "accountFullName", "DEFAULT") != null) {
-      readOnlyAccountFields.add(Account.FieldName.FULL_NAME);
+      readOnlyAccountFields.add(AccountFieldName.FULL_NAME);
     }
     if (optdef(config, "accountSshUserName", "DEFAULT") != null) {
-      readOnlyAccountFields.add(Account.FieldName.USER_NAME);
+      readOnlyAccountFields.add(AccountFieldName.USER_NAME);
     }
     if (!authConfig.isAllowRegisterNewEmail()) {
-      readOnlyAccountFields.add(Account.FieldName.REGISTER_NEW_EMAIL);
+      readOnlyAccountFields.add(AccountFieldName.REGISTER_NEW_EMAIL);
     }
 
     fetchMemberOfEagerly = optional(config, "fetchMemberOfEagerly", true);
@@ -196,7 +197,7 @@
   }
 
   @Override
-  public boolean allowsEdit(final Account.FieldName field) {
+  public boolean allowsEdit(final AccountFieldName field) {
     return !readOnlyAccountFields.contains(field);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
index cf9000d..94a3ac2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
@@ -17,9 +17,9 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.server.account.AbstractRealm;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
@@ -37,7 +37,7 @@
 @Singleton
 public class OAuthRealm extends AbstractRealm {
   private final DynamicMap<OAuthLoginProvider> loginProviders;
-  private final Set<FieldName> editableAccountFields;
+  private final Set<AccountFieldName> editableAccountFields;
 
   @Inject
   OAuthRealm(DynamicMap<OAuthLoginProvider> loginProviders,
@@ -45,15 +45,15 @@
     this.loginProviders = loginProviders;
     this.editableAccountFields = new HashSet<>();
     if (config.getBoolean("oauth", null, "allowEditFullName", false)) {
-      editableAccountFields.add(FieldName.FULL_NAME);
+      editableAccountFields.add(AccountFieldName.FULL_NAME);
     }
     if (config.getBoolean("oauth", null, "allowRegisterNewEmail", false)) {
-      editableAccountFields.add(FieldName.REGISTER_NEW_EMAIL);
+      editableAccountFields.add(AccountFieldName.REGISTER_NEW_EMAIL);
     }
   }
 
   @Override
-  public boolean allowsEdit(FieldName field) {
+  public boolean allowsEdit(AccountFieldName field) {
     return editableAccountFields.contains(field);
   }
 
@@ -105,12 +105,12 @@
     }
     if (!Strings.isNullOrEmpty(userInfo.getEmailAddress())
         && (Strings.isNullOrEmpty(who.getUserName())
-            || !allowsEdit(FieldName.REGISTER_NEW_EMAIL))) {
+            || !allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL))) {
       who.setEmailAddress(userInfo.getEmailAddress());
     }
     if (!Strings.isNullOrEmpty(userInfo.getDisplayName())
         && (Strings.isNullOrEmpty(who.getDisplayName())
-            || !allowsEdit(FieldName.FULL_NAME))) {
+            || !allowsEdit(AccountFieldName.FULL_NAME))) {
       who.setDisplayName(userInfo.getDisplayName());
     }
     return who;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index df68411..adbcf22 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -86,17 +87,22 @@
     if (!control.canAbandon(dbProvider.get())) {
       throw new AuthException("abandon not permitted");
     }
-    Change change = abandon(control, input.message);
+    Change change = abandon(control, input.message, input.notify);
     return json.create(ChangeJson.NO_OPTIONS).format(change);
   }
 
   public Change abandon(ChangeControl control, String msgTxt)
       throws RestApiException, UpdateException {
+    return abandon(control, msgTxt, NotifyHandling.ALL);
+  }
+
+  public Change abandon(ChangeControl control, String msgTxt,
+      NotifyHandling notifyHandling) throws RestApiException, UpdateException {
     CurrentUser user = control.getUser();
     Account account = user.isIdentifiedUser()
         ? user.asIdentifiedUser().getAccount()
         : null;
-    Op op = new Op(msgTxt, account);
+    Op op = new Op(msgTxt, account, notifyHandling);
     try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
         control.getProject().getNameKey(), user, TimeUtil.nowTs())) {
       u.addOp(control.getId(), op).execute();
@@ -111,10 +117,12 @@
     private Change change;
     private PatchSet patchSet;
     private ChangeMessage message;
+    private NotifyHandling notifyHandling;
 
-    private Op(String msgTxt, Account account) {
+    private Op(String msgTxt, Account account, NotifyHandling notifyHandling) {
       this.account = account;
       this.msgTxt = msgTxt;
+      this.notifyHandling = notifyHandling;
     }
 
     @Override
@@ -166,12 +174,14 @@
         if (account != null) {
           cm.setFrom(account.getId());
         }
-        cm.setChangeMessage(message);
+        cm.setChangeMessage(message.getMessage(), ctx.getWhen());
+        cm.setNotify(notifyHandling);
         cm.send();
       } catch (Exception e) {
         log.error("Cannot email update for change " + change.getId(), e);
       }
-      changeAbandoned.fire(change, patchSet, account, msgTxt);
+      changeAbandoned.fire(change, patchSet, account, msgTxt, ctx.getWhen(),
+          notifyHandling);
     }
   }
 
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 5c5e1fe..84781f5 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
@@ -19,10 +19,12 @@
 import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -34,6 +36,7 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.CommentAdded;
@@ -48,6 +51,7 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
@@ -86,6 +90,7 @@
       LoggerFactory.getLogger(ChangeInserter.class);
 
   private final ProjectControl.GenericFactory projectControlFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final PatchSetUtil psUtil;
@@ -115,7 +120,7 @@
   private Map<String, Short> approvals;
   private RequestScopePropagator requestScopePropagator;
   private ReceiveCommand updateRefCommand;
-  private boolean runHooks;
+  private boolean fireRevisionCreated;
   private boolean sendMail;
   private boolean updateRef;
 
@@ -124,9 +129,11 @@
   private ChangeMessage changeMessage;
   private PatchSetInfo patchSetInfo;
   private PatchSet patchSet;
+  private String pushCert;
 
   @Inject
   ChangeInserter(ProjectControl.GenericFactory projectControlFactory,
+      IdentifiedUser.GenericFactory userFactory,
       ChangeControl.GenericFactory changeControlFactory,
       PatchSetInfoFactory patchSetInfoFactory,
       PatchSetUtil psUtil,
@@ -141,6 +148,7 @@
       @Assisted RevCommit commit,
       @Assisted String refName) {
     this.projectControlFactory = projectControlFactory;
+    this.userFactory = userFactory;
     this.changeControlFactory = changeControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.psUtil = psUtil;
@@ -160,7 +168,7 @@
     this.extraCC = Collections.emptySet();
     this.approvals = Collections.emptyMap();
     this.updateRefCommand = null;
-    this.runHooks = true;
+    this.fireRevisionCreated = true;
     this.sendMail = true;
     this.updateRef = true;
   }
@@ -170,7 +178,7 @@
     change = new Change(
         getChangeKey(commit),
         changeId,
-        ctx.getUser().getAccountId(),
+        ctx.getAccountId(),
         new Branch.NameKey(ctx.getProject(), refName),
         ctx.getWhen());
     change.setStatus(MoreObjects.firstNonNull(status, Change.Status.NEW));
@@ -256,8 +264,8 @@
     return this;
   }
 
-  public ChangeInserter setRunHooks(boolean runHooks) {
-    this.runHooks = runHooks;
+  public ChangeInserter setFireRevisionCreated(boolean fireRevisionCreated) {
+    this.fireRevisionCreated = fireRevisionCreated;
     return this;
   }
 
@@ -275,6 +283,10 @@
     updateRefCommand = cmd;
   }
 
+  public void setPushCertificate(String cert) {
+    pushCert = cert;
+  }
+
   public PatchSet getPatchSet() {
     checkState(patchSet != null,
         "getPatchSet() only valid after creating change");
@@ -335,7 +347,7 @@
       newGroups = GroupCollector.getDefaultGroups(commit);
     }
     patchSet = psUtil.insert(ctx.getDb(), ctx.getRevWalk(), update, psId,
-        commit, draft, newGroups, null);
+        commit, draft, newGroups, pushCert);
 
     /* TODO: fixStatus is used here because the tests
      * (byStatusClosed() in AbstractQueryChangesTest)
@@ -348,14 +360,16 @@
     update.fixStatus(change.getStatus());
 
     LabelTypes labelTypes = ctl.getProjectControl().getLabelTypes();
-    approvalsUtil.addReviewers(db, update, labelTypes, change,
-        patchSet, patchSetInfo, reviewers, Collections.<Account.Id> emptySet());
+    approvalsUtil.addReviewers(db, update, labelTypes, change, patchSet,
+        patchSetInfo,
+        filterOnChangeVisibility(db, ctx.getNotes(), reviewers),
+        Collections.<Account.Id> emptySet());
     approvalsUtil.addApprovals(db, update, labelTypes, patchSet,
         ctx.getControl(), approvals);
     if (message != null) {
       changeMessage =
           new ChangeMessage(new ChangeMessage.Key(change.getId(),
-              ChangeUtil.messageUUID(db)), ctx.getUser().getAccountId(),
+              ChangeUtil.messageUUID(db)), ctx.getAccountId(),
               patchSet.getCreatedOn(), patchSet.getId());
       changeMessage.setMessage(message);
       cmUtil.addChangeMessage(db, update, changeMessage);
@@ -363,6 +377,25 @@
     return true;
   }
 
+  private Set<Account.Id> filterOnChangeVisibility(final ReviewDb db,
+      final ChangeNotes notes, Set<Account.Id> accounts) {
+    return Sets.filter(accounts, new Predicate<Account.Id>() {
+      @Override
+      public boolean apply(Account.Id accountId) {
+        try {
+          IdentifiedUser user = userFactory.create(accountId);
+          return changeControlFactory.controlFor(notes, user).isVisible(db);
+        } catch (OrmException | NoSuchChangeException e) {
+          log.warn(
+              String.format("Failed to check if account %d can see change %d",
+                  accountId.get(), notes.getChangeId().get()),
+              e);
+          return false;
+        }
+      }
+    });
+  }
+
   @Override
   public void postUpdate(Context ctx) throws OrmException, NoSuchChangeException {
     if (sendMail) {
@@ -400,8 +433,9 @@
      * For labels that are set in this operation, the value was modified, so
      * show a transition from an oldValue of 0 to the new value.
      */
-    if (runHooks) {
-      revisionCreated.fire(change, patchSet, ctx.getUser().getAccountId());
+    if (fireRevisionCreated) {
+      revisionCreated.fire(change, patchSet, ctx.getAccountId(),
+          ctx.getWhen(), notify);
       if (approvals != null && !approvals.isEmpty()) {
         ChangeControl changeControl = changeControlFactory.controlFor(
             ctx.getDb(), change, ctx.getUser());
@@ -419,7 +453,7 @@
           }
         }
         commentAdded.fire(change, patchSet,
-            ctx.getUser().asIdentifiedUser().getAccount(), null,
+            ctx.getAccount(), null,
             allApprovals, oldApprovals, ctx.getWhen());
       }
     }
@@ -446,7 +480,7 @@
           refControl.getProjectControl().getProject(),
           change.getDest().get(),
           commit,
-          ctx.getUser().asIdentifiedUser());
+          ctx.getIdentifiedUser());
 
       switch (validatePolicy) {
       case RECEIVE_COMMITS:
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 ec145d5..ab92a97 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
@@ -31,6 +31,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
+import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWER_UPDATES;
 import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
 import static com.google.gerrit.server.CommonConverters.toGitPerson;
 
@@ -70,6 +71,7 @@
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.ProblemInfo;
 import com.google.gerrit.extensions.common.PushCertificateInfo;
+import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.config.DownloadCommand;
@@ -89,6 +91,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.AccountLoader;
@@ -264,7 +267,7 @@
     } catch (PatchListNotAvailableException | GpgException | OrmException
         | IOException | RuntimeException e) {
       if (!has(CHECK)) {
-        Throwables.propagateIfPossible(e, OrmException.class);
+        Throwables.throwIfInstanceOf(e, OrmException.class);
         throw new OrmException(e);
       }
       return checkOnly(cd);
@@ -355,7 +358,21 @@
   }
 
   private ChangeInfo checkOnly(ChangeData cd) {
-    ConsistencyChecker.Result result = checkerProvider.get().check(cd, fix);
+    ChangeControl ctl;
+    try {
+      ctl = cd.changeControl().forUser(userProvider.get());
+    } catch (OrmException e) {
+      String msg = "Error loading change";
+      log.warn(msg + " " + cd.getId(), e);
+      ChangeInfo info = new ChangeInfo();
+      info._number = cd.getId().get();
+      ProblemInfo p = new ProblemInfo();
+      p.message = msg;
+      info.problems = Lists.newArrayList(p);
+      return info;
+    }
+
+    ConsistencyChecker.Result result = checkerProvider.get().check(ctl, fix);
     ChangeInfo info;
     Change c = result.change();
     if (c != null) {
@@ -384,9 +401,11 @@
       Optional<PatchSet.Id> limitToPsId) throws PatchListNotAvailableException,
       GpgException, OrmException, IOException {
     ChangeInfo out = new ChangeInfo();
+    CurrentUser user = userProvider.get();
+    ChangeControl ctl = cd.changeControl().forUser(user);
 
     if (has(CHECK)) {
-      out.problems = checkerProvider.get().check(cd.change(), fix).problems();
+      out.problems = checkerProvider.get().check(ctl, fix).problems();
       // If any problems were fixed, the ChangeData needs to be reloaded.
       for (ProblemInfo p : out.problems) {
         if (p.status == ProblemInfo.Status.FIXED) {
@@ -397,8 +416,6 @@
     }
 
     Change in = cd.change();
-    CurrentUser user = userProvider.get();
-    ChangeControl ctl = cd.changeControl().forUser(user);
     out.project = in.getProject().get();
     out.branch = in.getDest().getShortName();
     out.topic = in.getTopic();
@@ -459,6 +476,10 @@
       }
     }
 
+    if (has(REVIEWER_UPDATES)) {
+      out.reviewerUpdates = reviewerUpdates(cd);
+    }
+
     boolean needMessages = has(MESSAGES);
     boolean needRevisions = has(ALL_REVISIONS)
         || has(CURRENT_REVISION)
@@ -493,6 +514,21 @@
     return out;
   }
 
+  private Collection<ReviewerUpdateInfo> reviewerUpdates(ChangeData cd)
+      throws OrmException {
+    List<ReviewerStatusUpdate> reviewerUpdates = cd.reviewerUpdates();
+    List<ReviewerUpdateInfo> result = new ArrayList<>(reviewerUpdates.size());
+    for (ReviewerStatusUpdate c : reviewerUpdates) {
+      ReviewerUpdateInfo change = new ReviewerUpdateInfo();
+      change.updated = c.date();
+      change.state = c.state().asReviewerState();
+      change.updatedBy = accountLoader.get(c.updatedBy());
+      change.reviewer = accountLoader.get(c.reviewer());
+      result.add(change);
+    }
+    return result;
+  }
+
   private List<SubmitRecord> submitRecords(ChangeData cd) throws OrmException {
     // Maintain our own cache rather than using cd.getSubmitRecords(),
     // since the latter may not have used the same values for
@@ -890,17 +926,24 @@
         .toSortedList(AccountInfoComparator.ORDER_NULLS_FIRST);
   }
 
+  @Nullable
+  private Repository openRepoIfNecessary(ChangeControl ctl) throws IOException {
+    if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) {
+      return repoManager.openRepository(ctl.getProject().getNameKey());
+    }
+    return null;
+  }
+
   private Map<String, RevisionInfo> revisions(ChangeControl ctl, ChangeData cd,
       Map<PatchSet.Id, PatchSet> map) throws PatchListNotAvailableException,
       GpgException, OrmException, IOException {
     Map<String, RevisionInfo> res = new LinkedHashMap<>();
-    try (Repository repo =
-        repoManager.openRepository(ctl.getProject().getNameKey())) {
+    try (Repository repo = openRepoIfNecessary(ctl)) {
       for (PatchSet in : map.values()) {
         if ((has(ALL_REVISIONS)
             || in.getId().equals(ctl.getChange().currentPatchSetId()))
             && ctl.isPatchVisible(in, db.get())) {
-          res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in, repo));
+          res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in, repo, false));
         }
       }
       return res;
@@ -939,17 +982,16 @@
       throws PatchListNotAvailableException, GpgException, OrmException,
       IOException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
-    try (Repository repo =
-        repoManager.openRepository(ctl.getProject().getNameKey())) {
+    try (Repository repo = openRepoIfNecessary(ctl)) {
       RevisionInfo rev = toRevisionInfo(
-          ctl, changeDataFactory.create(db.get(), ctl), in, repo);
+          ctl, changeDataFactory.create(db.get(), ctl), in, repo, true);
       accountLoader.fill();
       return rev;
     }
   }
 
   private RevisionInfo toRevisionInfo(ChangeControl ctl, ChangeData cd,
-      PatchSet in, Repository repo)
+      PatchSet in, @Nullable Repository repo, boolean fillCommit)
       throws PatchListNotAvailableException, GpgException, OrmException,
       IOException {
     Change c = ctl.getChange();
@@ -973,7 +1015,7 @@
         RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
         rw.parseBody(commit);
         if (setCommit) {
-          out.commit = toCommit(ctl, rw, commit, has(WEB_LINKS));
+          out.commit = toCommit(ctl, rw, commit, has(WEB_LINKS), fillCommit);
         }
         if (addFooters) {
           out.commitWithFooters = mergeUtilFactory
@@ -996,7 +1038,7 @@
           new RevisionResource(changeResourceFactory.create(ctl), in));
     }
 
-    if (has(PUSH_CERTIFICATES)) {
+    if (gpgApi.isEnabled() && has(PUSH_CERTIFICATES)) {
       if (in.getPushCertificate() != null) {
         out.pushCertificate = gpgApi.checkPushCertificate(
             in.getPushCertificate(),
@@ -1010,9 +1052,12 @@
   }
 
   CommitInfo toCommit(ChangeControl ctl, RevWalk rw, RevCommit commit,
-      boolean addLinks) throws IOException {
+      boolean addLinks, boolean fillCommit) throws IOException {
     Project.NameKey project = ctl.getProject().getNameKey();
     CommitInfo info = new CommitInfo();
+    if (fillCommit) {
+      info.commit = commit.name();
+    }
     info.parents = new ArrayList<>(commit.getParentCount());
     info.author = toGitPerson(commit.getAuthorIdent());
     info.committer = toGitPerson(commit.getCommitterIdent());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
index 2302b70..f0075ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -31,10 +32,11 @@
  * implementation changes, which might invalidate old entries).
  */
 public interface ChangeKindCache {
-  ChangeKind getChangeKind(ProjectState project, Repository repo,
+  ChangeKind getChangeKind(ProjectState project, @Nullable Repository repo,
       ObjectId prior, ObjectId next);
 
   ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch);
 
-  ChangeKind getChangeKind(Repository repo, ChangeData cd, PatchSet patch);
+  ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd,
+      PatchSet patch);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index edc1b12..b23bcf8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -22,9 +22,11 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.Weigher;
 import com.google.common.collect.FluentIterable;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -100,11 +102,13 @@
     }
 
     @Override
-    public ChangeKind getChangeKind(ProjectState project, Repository repo,
-        ObjectId prior, ObjectId next) {
+    public ChangeKind getChangeKind(ProjectState project,
+        @Nullable Repository repo, ObjectId prior, ObjectId next) {
       try {
         Key key = new Key(prior, next, useRecursiveMerge);
-        return new Loader(key, repo).call();
+        return new Loader(
+                key, repoManager, project.getProject().getNameKey(), repo)
+            .call();
       } catch (IOException e) {
         log.warn("Cannot check trivial rebase of new patch set " + next.name()
             + " in " + project.getProject().getName(), e);
@@ -120,7 +124,7 @@
     }
 
     @Override
-    public ChangeKind getChangeKind(Repository repo, ChangeData cd,
+    public ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd,
         PatchSet patch) {
       return getChangeKindInternal(this, repo, cd, patch, projectCache);
     }
@@ -191,11 +195,16 @@
 
   private static class Loader implements Callable<ChangeKind> {
     private final Key key;
-    private final Repository repo;
+    private final GitRepositoryManager repoManager;
+    private final Project.NameKey projectName;
+    private final Repository alreadyOpenRepo;
 
-    private Loader(Key key, Repository repo) {
+    private Loader(Key key, GitRepositoryManager repoManager,
+        Project.NameKey projectName, @Nullable Repository alreadyOpenRepo) {
       this.key = key;
-      this.repo = repo;
+      this.repoManager = repoManager;
+      this.projectName = projectName;
+      this.alreadyOpenRepo = alreadyOpenRepo;
     }
 
     @Override
@@ -204,6 +213,12 @@
         return ChangeKind.NO_CODE_CHANGE;
       }
 
+      Repository repo = alreadyOpenRepo;
+      boolean close = false;
+      if (repo == null) {
+        repo = repoManager.openRepository(projectName);
+        close = true;
+      }
       try (RevWalk walk = new RevWalk(repo)) {
         RevCommit prior = walk.parseCommit(key.prior);
         walk.parseBody(prior);
@@ -246,6 +261,10 @@
           // it was a rework.
         }
         return ChangeKind.REWORK;
+      } finally {
+        if (close) {
+          repo.close();
+        }
       }
     }
 
@@ -321,11 +340,14 @@
   }
 
   @Override
-  public ChangeKind getChangeKind(ProjectState project, Repository repo,
-      ObjectId prior, ObjectId next) {
+  public ChangeKind getChangeKind(ProjectState project,
+      @Nullable Repository repo, ObjectId prior, ObjectId next) {
     try {
       Key key = new Key(prior, next, useRecursiveMerge);
-      return cache.get(key, new Loader(key, repo));
+      return cache.get(
+          key,
+          new Loader(
+                key, repoManager, project.getProject().getNameKey(), repo));
     } catch (ExecutionException e) {
       log.warn("Cannot check trivial rebase of new patch set " + next.name()
           + " in " + project.getProject().getName(), e);
@@ -340,14 +362,14 @@
   }
 
   @Override
-  public ChangeKind getChangeKind(Repository repo, ChangeData cd,
+  public ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd,
       PatchSet patch) {
     return getChangeKindInternal(this, repo, cd, patch, projectCache);
   }
 
   private static ChangeKind getChangeKindInternal(
       ChangeKindCache cache,
-      Repository repo,
+      @Nullable Repository repo,
       ChangeData change,
       PatchSet patch,
       ProjectCache projectCache) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
index 465ce95..f4869be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
@@ -18,12 +18,10 @@
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -32,27 +30,22 @@
 
 public class Check implements RestReadView<ChangeResource>,
     RestModifyView<ChangeResource, FixInput> {
-  private final NotesMigration notesMigration;
   private final ChangeJson.Factory jsonFactory;
 
   @Inject
-  Check(NotesMigration notesMigration,
-      ChangeJson.Factory json) {
-    this.notesMigration = notesMigration;
+  Check(ChangeJson.Factory json) {
     this.jsonFactory = json;
   }
 
   @Override
   public Response<ChangeInfo> apply(ChangeResource rsrc)
       throws RestApiException, OrmException {
-    checkEnabled();
     return Response.withMustRevalidate(newChangeJson().format(rsrc));
   }
 
   @Override
   public Response<ChangeInfo> apply(ChangeResource rsrc, FixInput input)
       throws RestApiException, OrmException {
-    checkEnabled();
     ChangeControl ctl = rsrc.getControl();
     if (!ctl.isOwner()
         && !ctl.getProjectControl().isOwner()
@@ -65,10 +58,4 @@
   private ChangeJson newChangeJson() {
     return jsonFactory.create(EnumSet.of(ListChangesOption.CHECK));
   }
-
-  private void checkEnabled() throws NotImplementedException {
-    if (notesMigration.readChanges()) {
-      throw new NotImplementedException("check not implemented for NoteDb");
-    }
-  }
 }
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 e88e031..db18ba2 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
@@ -274,7 +274,7 @@
       ChangeMessage changeMessage = new ChangeMessage(
           new ChangeMessage.Key(
               ctx.getChange().getId(), ChangeUtil.messageUUID(ctx.getDb())),
-              ctx.getUser().getAccountId(), ctx.getWhen(), psId);
+              ctx.getAccountId(), ctx.getWhen(), psId);
       StringBuilder sb = new StringBuilder("Patch Set ")
           .append(psId.get())
           .append(": Cherry Picked")
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
index 0af5656..d1ce453 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
@@ -126,8 +126,11 @@
     }
     r.id = Url.encode(c.getKey().get());
     r.path = c.getKey().getParentKey().getFileName();
-    if (c.getSide() == 0) {
+    if (c.getSide() <= 0) {
       r.side = Side.PARENT;
+      if (c.getSide() < 0) {
+        r.parent = -c.getSide();
+      }
     }
     if (c.getLine() > 0) {
       r.line = c.getLine();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index a10d208..287c3ed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
 import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
 import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER;
@@ -39,23 +40,23 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
-import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.PatchSetState;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -71,6 +72,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -78,9 +80,10 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
 
 /**
  * Checks changes for various kinds of inconsistency and corruption.
@@ -94,12 +97,10 @@
 
   @AutoValue
   public abstract static class Result {
-    private static Result create(Change.Id id, List<ProblemInfo> problems) {
-      return new AutoValue_ConsistencyChecker_Result(id, null, problems);
-    }
-
-    private static Result create(Change c, List<ProblemInfo> problems) {
-      return new AutoValue_ConsistencyChecker_Result(c.getId(), c, problems);
+    private static Result create(ChangeControl ctl,
+        List<ProblemInfo> problems) {
+      return new AutoValue_ConsistencyChecker_Result(
+          ctl.getId(), ctl.getChange(), problems);
     }
 
     public abstract Change.Id id();
@@ -110,22 +111,20 @@
     public abstract List<ProblemInfo> problems();
   }
 
-  private final Provider<ReviewDb> db;
-  private final GitRepositoryManager repoManager;
-  private final NotesMigration notesMigration;
-  private final Provider<CurrentUser> user;
-  private final Provider<PersonIdent> serverIdent;
-  private final PatchSetInfoFactory patchSetInfoFactory;
-  private final PatchSetInserter.Factory patchSetInserterFactory;
   private final BatchUpdate.Factory updateFactory;
-  private final ChangeIndexer indexer;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final ChangeNotes.Factory notesFactory;
-  private final ChangeUpdate.Factory changeUpdateFactory;
   private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore;
+  private final GitRepositoryManager repoManager;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final PatchSetInserter.Factory patchSetInserterFactory;
+  private final PatchSetUtil psUtil;
+  private final Provider<CurrentUser> user;
+  private final Provider<PersonIdent> serverIdent;
+  private final Provider<ReviewDb> db;
 
   private FixInput fix;
-  private Change change;
+  private ChangeControl ctl;
   private Repository repo;
   private RevWalk rw;
 
@@ -137,67 +136,51 @@
   private List<ProblemInfo> problems;
 
   @Inject
-  ConsistencyChecker(Provider<ReviewDb> db,
-      GitRepositoryManager repoManager,
-      NotesMigration notesMigration,
-      Provider<CurrentUser> user,
+  ConsistencyChecker(
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
-      PatchSetInfoFactory patchSetInfoFactory,
-      PatchSetInserter.Factory patchSetInserterFactory,
       BatchUpdate.Factory updateFactory,
-      ChangeIndexer indexer,
       ChangeControl.GenericFactory changeControlFactory,
       ChangeNotes.Factory notesFactory,
-      ChangeUpdate.Factory changeUpdateFactory,
-      DynamicItem<AccountPatchReviewStore> accountPatchReviewStore) {
+      DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
+      GitRepositoryManager repoManager,
+      PatchSetInfoFactory patchSetInfoFactory,
+      PatchSetInserter.Factory patchSetInserterFactory,
+      PatchSetUtil psUtil,
+      Provider<CurrentUser> user,
+      Provider<ReviewDb> db) {
+    this.accountPatchReviewStore = accountPatchReviewStore;
+    this.changeControlFactory = changeControlFactory;
     this.db = db;
-    this.notesMigration = notesMigration;
-    this.repoManager = repoManager;
-    this.user = user;
-    this.serverIdent = serverIdent;
+    this.notesFactory = notesFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.patchSetInserterFactory = patchSetInserterFactory;
+    this.psUtil = psUtil;
+    this.repoManager = repoManager;
+    this.serverIdent = serverIdent;
     this.updateFactory = updateFactory;
-    this.indexer = indexer;
-    this.changeControlFactory = changeControlFactory;
-    this.notesFactory = notesFactory;
-    this.changeUpdateFactory = changeUpdateFactory;
-    this.accountPatchReviewStore = accountPatchReviewStore;
+    this.user = user;
     reset();
   }
 
   private void reset() {
-    change = null;
+    ctl = null;
     repo = null;
     rw = null;
     problems = new ArrayList<>();
   }
 
-  public Result check(ChangeData cd) {
-    return check(cd, null);
+  private Change change() {
+    return ctl.getChange();
   }
 
-  public Result check(ChangeData cd, @Nullable FixInput f) {
-    reset();
+  public Result check(ChangeControl cc, @Nullable FixInput f) {
+    checkNotNull(cc);
     try {
-      return check(cd.change(), f);
-    } catch (OrmException e) {
-      error("Error looking up change", e);
-      return Result.create(cd.getId(), problems);
-    }
-  }
-
-  public Result check(Change c) {
-    return check(c, null);
-  }
-
-  public Result check(Change c, @Nullable FixInput f) {
-    reset();
-    fix = f;
-    change = c;
-    try {
+      reset();
+      ctl = cc;
+      fix = f;
       checkImpl();
-      return Result.create(c, problems);
+      return result();
     } finally {
       if (rw != null) {
         rw.close();
@@ -209,8 +192,6 @@
   }
 
   private void checkImpl() {
-    checkState(!notesMigration.readChanges(),
-        "ConsistencyChecker for NoteDb not yet implemented");
     checkOwner();
     checkCurrentPatchSetEntity();
 
@@ -226,8 +207,8 @@
 
   private void checkOwner() {
     try {
-      if (db.get().accounts().get(change.getOwner()) == null) {
-        problem("Missing change owner: " + change.getOwner());
+      if (db.get().accounts().get(change().getOwner()) == null) {
+        problem("Missing change owner: " + change().getOwner());
       }
     } catch (OrmException e) {
       error("Failed to look up owner", e);
@@ -236,10 +217,10 @@
 
   private void checkCurrentPatchSetEntity() {
     try {
-      PatchSet.Id psId = change.currentPatchSetId();
-      currPs = db.get().patchSets().get(psId);
+      currPs = psUtil.current(db.get(), ctl.getNotes());
       if (currPs == null) {
-        problem(String.format("Current patch set %d not found", psId.get()));
+        problem(String.format("Current patch set %d not found",
+              change().currentPatchSetId().get()));
       }
     } catch (OrmException e) {
       error("Failed to look up current patch set", e);
@@ -247,7 +228,7 @@
   }
 
   private boolean openRepo() {
-    Project.NameKey project = change.getDest().getParentKey();
+    Project.NameKey project = change().getDest().getParentKey();
     try {
       repo = repoManager.openRepository(project);
       rw = new RevWalk(repo);
@@ -262,13 +243,11 @@
   private boolean checkPatchSets() {
     List<PatchSet> all;
     try {
-      all = Lists.newArrayList(db.get().patchSets().byChange(change.getId()));
+      // Iterate in descending order.
+      all = PS_ID_ORDER.sortedCopy(psUtil.byChange(db.get(), ctl.getNotes()));
     } catch (OrmException e) {
       return error("Failed to look up patch sets", e);
     }
-    // Iterate in descending order so deletePatchSet can assume the latest patch
-    // set exists.
-    Collections.sort(all, PS_ID_ORDER.reverse());
     patchSetsBySha = MultimapBuilder.hashKeys(all.size())
         .treeSetValues(PS_ID_ORDER)
         .build();
@@ -287,6 +266,7 @@
       refs = Collections.emptyMap();
     }
 
+    List<DeletePatchSetFromDbOp> deletePatchSetOps = new ArrayList<>();
     for (PatchSet ps : all) {
       // Check revision format.
       int psNum = ps.getId().get();
@@ -317,17 +297,21 @@
           objId, String.format("patch set %d", psNum));
       if (psCommit == null) {
         if (fix != null && fix.deletePatchSetIfCommitMissing) {
-          deletePatchSet(lastProblem(), change.getProject(), ps.getId());
+          deletePatchSetOps.add(
+              new DeletePatchSetFromDbOp(lastProblem(), ps.getId()));
         }
         continue;
       } else if (refProblem != null && fix != null) {
         fixPatchSetRef(refProblem, ps);
       }
-      if (ps.getId().equals(change.currentPatchSetId())) {
+      if (ps.getId().equals(change().currentPatchSetId())) {
         currPsCommit = psCommit;
       }
     }
 
+    // Delete any bad patch sets found above, in a single update.
+    deletePatchSets(deletePatchSetOps);
+
     // Check for duplicates.
     for (Map.Entry<ObjectId, Collection<PatchSet>> e
         : patchSetsBySha.asMap().entrySet()) {
@@ -342,7 +326,7 @@
   }
 
   private void checkMerged() {
-    String refName = change.getDest().get();
+    String refName = change().getDest().get();
     Ref dest;
     try {
       dest = repo.getRefDatabase().exactRef(refName);
@@ -375,22 +359,27 @@
     }
   }
 
+  private ProblemInfo wrongChangeStatus(PatchSet.Id psId, RevCommit commit) {
+    String refName = change().getDest().get();
+    return problem(String.format(
+        "Patch set %d (%s) is merged into destination ref %s (%s), but change"
+        + " status is %s", psId.get(), commit.name(),
+        refName, tip.name(), change().getStatus()));
+  }
+
   private void checkMergedBitMatchesStatus(PatchSet.Id psId, RevCommit commit,
       boolean merged) {
-    String refName = change.getDest().get();
-    if (merged && change.getStatus() != Change.Status.MERGED) {
-      ProblemInfo p = problem(String.format(
-          "Patch set %d (%s) is merged into destination ref %s (%s), but change"
-          + " status is %s", psId.get(), commit.name(),
-          refName, tip.name(), change.getStatus()));
+    String refName = change().getDest().get();
+    if (merged && change().getStatus() != Change.Status.MERGED) {
+      ProblemInfo p = wrongChangeStatus(psId, commit);
       if (fix != null) {
         fixMerged(p);
       }
-    } else if (!merged && change.getStatus() == Change.Status.MERGED) {
+    } else if (!merged && change().getStatus() == Change.Status.MERGED) {
       problem(String.format("Patch set %d (%s) is not merged into"
             + " destination ref %s (%s), but change status is %s",
             currPs.getId().get(), commit.name(), refName, tip.name(),
-            change.getStatus()));
+            change().getStatus()));
     }
   }
 
@@ -401,20 +390,16 @@
     if (commit == null) {
       return;
     }
-    if (Objects.equals(commit, currPsCommit)) {
-      // Caller gave us latest patch set SHA-1; verified in checkPatchSets.
-      return;
-    }
 
     try {
       if (!rw.isMergedInto(commit, tip)) {
         problem(String.format("Expected merged commit %s is not merged into"
               + " destination ref %s (%s)",
-              commit.name(), change.getDest().get(), tip.name()));
+              commit.name(), change().getDest().get(), tip.name()));
         return;
       }
 
-      List<PatchSet.Id> psIds = new ArrayList<>();
+      List<PatchSet.Id> thisCommitPsIds = new ArrayList<>();
       for (Ref ref : repo.getRefDatabase().getRefs(REFS_CHANGES).values()) {
         if (!ref.getObjectId().equals(commit)) {
           continue;
@@ -425,52 +410,53 @@
         }
         try {
           Change c = notesFactory.createChecked(
-              db.get(), change.getProject(), psId.getParentKey()).getChange();
-          if (!c.getDest().equals(change.getDest())) {
+              db.get(), change().getProject(), psId.getParentKey()).getChange();
+          if (!c.getDest().equals(change().getDest())) {
             continue;
           }
         } catch (OrmException | NoSuchChangeException e) {
           warn(e);
           // Include this patch set; should cause an error below, which is good.
         }
-        psIds.add(psId);
+        thisCommitPsIds.add(psId);
       }
-      switch (psIds.size()) {
+      switch (thisCommitPsIds.size()) {
         case 0:
           // No patch set for this commit; insert one.
           rw.parseBody(commit);
           String changeId = Iterables.getFirst(
               commit.getFooterLines(FooterConstants.CHANGE_ID), null);
           // Missing Change-Id footer is ok, but mismatched is not.
-          if (changeId != null && !changeId.equals(change.getKey().get())) {
+          if (changeId != null && !changeId.equals(change().getKey().get())) {
             problem(String.format("Expected merged commit %s has Change-Id: %s,"
                   + " but expected %s",
-                  commit.name(), changeId, change.getKey().get()));
+                  commit.name(), changeId, change().getKey().get()));
             return;
           }
-          PatchSet.Id psId = insertPatchSet(commit);
-          if (psId != null) {
-            checkMergedBitMatchesStatus(psId, commit, true);
-          }
+          insertMergedPatchSet(commit, null, false);
           break;
 
         case 1:
-          // Existing patch set of this commit; check that it is the current
-          // patch set.
-          // TODO(dborowitz): This could be fixed if it's an older patch set of
-          // the current change.
-          PatchSet.Id id = psIds.get(0);
-          if (!id.equals(change.currentPatchSetId())) {
-            problem(String.format("Expected merged commit %s corresponds to"
-                  + " patch set %s, which is not the current patch set %s",
-                  commit.name(), id, change.currentPatchSetId()));
+          // Existing patch set ref pointing to this commit.
+          PatchSet.Id id = thisCommitPsIds.get(0);
+          if (id.equals(change().currentPatchSetId())) {
+            // If it's the current patch set, we can just fix the status.
+            fixMerged(wrongChangeStatus(id, commit));
+          } else if (id.get() > change().currentPatchSetId().get()) {
+            // If it's newer than the current patch set, reuse this patch set
+            // ID when inserting a new merged patch set.
+            insertMergedPatchSet(commit, id, true);
+          } else {
+            // If it's older than the current patch set, just delete the old
+            // ref, and use a new ID when inserting a new merged patch set.
+            insertMergedPatchSet(commit, id, false);
           }
           break;
 
         default:
           problem(String.format(
                 "Multiple patch sets for expected merged commit %s: %s",
-                commit.name(), intKeyOrdering().sortedCopy(psIds)));
+                commit.name(), intKeyOrdering().sortedCopy(thisCommitPsIds)));
           break;
       }
     } catch (IOException e) {
@@ -479,74 +465,129 @@
     }
   }
 
-  private PatchSet.Id insertPatchSet(RevCommit commit) {
-    ProblemInfo p =
+  private void insertMergedPatchSet(final RevCommit commit,
+      final @Nullable PatchSet.Id psIdToDelete, boolean reuseOldPsId) {
+    ProblemInfo notFound =
         problem("No patch set found for merged commit " + commit.name());
     if (!user.get().isIdentifiedUser()) {
-      p.status = Status.FIX_FAILED;
-      p.outcome =
+      notFound.status = Status.FIX_FAILED;
+      notFound.outcome =
           "Must be called by an identified user to insert new patch set";
-      return null;
+      return;
+    }
+    ProblemInfo insertPatchSetProblem;
+    ProblemInfo deleteOldPatchSetProblem;
+
+    if (psIdToDelete == null) {
+      insertPatchSetProblem = problem(String.format(
+          "Expected merged commit %s has no associated patch set",
+          commit.name()));
+      deleteOldPatchSetProblem = null;
+    } else {
+      String msg = String.format(
+          "Expected merge commit %s corresponds to patch set %s,"
+              + " not the current patch set %s",
+          commit.name(), psIdToDelete.get(),
+          change().currentPatchSetId().get());
+      // Maybe an identical problem, but different fix.
+      deleteOldPatchSetProblem = reuseOldPsId ? null : problem(msg);
+      insertPatchSetProblem = problem(msg);
     }
 
+    List<ProblemInfo> currProblems = new ArrayList<>(3);
+    currProblems.add(notFound);
+    if (deleteOldPatchSetProblem != null) {
+      currProblems.add(insertPatchSetProblem);
+    }
+    currProblems.add(insertPatchSetProblem);
+
     try {
-      ChangeControl ctl = changeControlFactory
-          .controlFor(db.get(), change, user.get());
-      PatchSet.Id psId =
-          ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
+      PatchSet.Id psId = (psIdToDelete != null && reuseOldPsId)
+          ? psIdToDelete
+          : ChangeUtil.nextPatchSetId(repo, change().currentPatchSetId());
       PatchSetInserter inserter =
           patchSetInserterFactory.create(ctl, psId, commit);
-      try (BatchUpdate bu = updateFactory.create(
-            db.get(), change.getProject(), ctl.getUser(), TimeUtil.nowTs());
+      try (BatchUpdate bu = newBatchUpdate();
           ObjectInserter oi = repo.newObjectInserter()) {
         bu.setRepository(repo, rw, oi);
-        bu.addOp(change.getId(), inserter
+
+        if (psIdToDelete != null) {
+          // Delete the given patch set ref. If reuseOldPsId is true,
+          // PatchSetInserter will reinsert the same ref, making it a no-op.
+          bu.addOp(ctl.getId(), new BatchUpdate.Op() {
+            @Override
+            public void updateRepo(RepoContext ctx) throws IOException {
+              ctx.addRefUpdate(new ReceiveCommand(
+                  commit, ObjectId.zeroId(), psIdToDelete.toRefName()));
+            }
+          });
+          if (!reuseOldPsId) {
+            bu.addOp(ctl.getId(), new DeletePatchSetFromDbOp(
+                checkNotNull(deleteOldPatchSetProblem), psIdToDelete));
+          }
+        }
+
+        bu.addOp(ctl.getId(), inserter
             .setValidatePolicy(CommitValidators.Policy.NONE)
-            .setRunHooks(false)
+            .setFireRevisionCreated(false)
             .setSendMail(false)
             .setAllowClosed(true)
             .setMessage(
                 "Patch set for merged commit inserted by consistency checker"));
+        bu.addOp(ctl.getId(), new FixMergedOp(notFound));
         bu.execute();
       }
-      change = inserter.getChange();
-      p.status = Status.FIXED;
-      p.outcome = "Inserted as patch set " + psId.get();
-      return psId;
+      ctl = changeControlFactory.controlFor(
+          db.get(), inserter.getChange(), ctl.getUser());
+      insertPatchSetProblem.status = Status.FIXED;
+      insertPatchSetProblem.outcome = "Inserted as patch set " + psId.get();
     } catch (OrmException | IOException | NoSuchChangeException
         | UpdateException | RestApiException e) {
       warn(e);
-      p.status = Status.FIX_FAILED;
-      p.outcome = "Error inserting new patch set";
-      return null;
+      for (ProblemInfo pi : currProblems) {
+        pi.status = Status.FIX_FAILED;
+        pi.outcome = "Error inserting merged patch set";
+      }
+      return;
+    }
+  }
+
+  private static class FixMergedOp extends BatchUpdate.Op {
+    private final ProblemInfo p;
+
+    private FixMergedOp(ProblemInfo p) {
+      this.p = p;
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext ctx) throws OrmException {
+      ctx.getChange().setStatus(Change.Status.MERGED);
+      ctx.getUpdate(ctx.getChange().currentPatchSetId())
+        .fixStatus(Change.Status.MERGED);
+      p.status = Status.FIXED;
+      p.outcome = "Marked change as merged";
+      return true;
     }
   }
 
   private void fixMerged(ProblemInfo p) {
-    try {
-      change = db.get().changes().atomicUpdate(change.getId(),
-          new AtomicUpdate<Change>() {
-            @Override
-            public Change update(Change c) {
-              c.setStatus(Change.Status.MERGED);
-              return c;
-            }
-          });
-      ChangeUpdate changeUpdate =
-          changeUpdateFactory.create(
-              changeControlFactory.controlFor(db.get(), change, user.get()));
-      changeUpdate.fixStatus(Change.Status.MERGED);
-      changeUpdate.commit();
-      indexer.index(db.get(), change);
-      p.status = Status.FIXED;
-      p.outcome = "Marked change as merged";
-    } catch (OrmException | IOException | NoSuchChangeException e) {
-      log.warn("Error marking " + change.getId() + "as merged", e);
+    try (BatchUpdate bu = newBatchUpdate();
+        ObjectInserter oi = repo.newObjectInserter()) {
+      bu.setRepository(repo, rw, oi);
+      bu.addOp(ctl.getId(), new FixMergedOp(p));
+      bu.execute();
+    } catch (UpdateException | RestApiException e) {
+      log.warn("Error marking " + ctl.getId() + "as merged", e);
       p.status = Status.FIX_FAILED;
       p.outcome = "Error updating status to merged";
     }
   }
 
+  private BatchUpdate newBatchUpdate() {
+    return updateFactory.create(
+        db.get(), change().getProject(), ctl.getUser(), TimeUtil.nowTs());
+  }
+
   private void fixPatchSetRef(ProblemInfo p, PatchSet ps) {
     try {
       RefUpdate ru = repo.updateRef(ps.getId().toRefName());
@@ -582,59 +623,108 @@
     }
   }
 
-  private void deletePatchSet(ProblemInfo p, Project.NameKey project,
-      PatchSet.Id psId) {
-    ReviewDb db = this.db.get();
-    Change.Id cid = psId.getParentKey();
-    try {
-      db.changes().beginTransaction(cid);
-      try {
-        ChangeNotes notes = notesFactory.createChecked(db, project, cid);
-        Change c = notes.getChange();
-        if (psId.equals(c.currentPatchSetId())) {
-          List<PatchSet> all = Lists.newArrayList(db.patchSets().byChange(cid));
-          if (all.size() == 1 && all.get(0).getId().equals(psId)) {
-            p.status = Status.FIX_FAILED;
-            p.outcome = "Cannot delete patch set; no patch sets would remain";
-            return;
-          }
-          // If there were multiple missing patch sets, assumes deletePatchSet
-          // has been called in decreasing order, so the max remaining PatchSet
-          // is the effective current patch set.
-          Collections.sort(all, PS_ID_ORDER.reverse());
-          PatchSet.Id latest = null;
-          for (PatchSet ps : all) {
-            latest = ps.getId();
-            if (!ps.getId().equals(psId)) {
-              break;
-            }
-          }
-          c.setCurrentPatchSet(patchSetInfoFactory.get(db, notes, latest));
-          db.changes().update(Collections.singleton(c));
-        }
-
-        // Delete dangling primary key references. Don't delete ChangeMessages,
-        // which don't use patch sets as a primary key, and may provide useful
-        // historical information.
-        accountPatchReviewStore.get().clearReviewed(psId);
-        db.patchSetApprovals().delete(
-            db.patchSetApprovals().byPatchSet(psId));
-        db.patchComments().delete(
-            db.patchComments().byPatchSet(psId));
-        db.patchSets().deleteKeys(Collections.singleton(psId));
-        db.commit();
-
-        p.status = Status.FIXED;
-        p.outcome = "Deleted patch set";
-      } finally {
-        db.rollback();
+  private void deletePatchSets(List<DeletePatchSetFromDbOp> ops) {
+    try (BatchUpdate bu = newBatchUpdate();
+        ObjectInserter oi = repo.newObjectInserter()) {
+      bu.setRepository(repo, rw, oi);
+      for (DeletePatchSetFromDbOp op : ops) {
+        checkArgument(op.psId.getParentKey().equals(ctl.getId()));
+        bu.addOp(ctl.getId(), op);
       }
-    } catch (PatchSetInfoNotAvailableException | OrmException
-        | NoSuchChangeException e) {
+      bu.addOp(ctl.getId(), new UpdateCurrentPatchSetOp(ops));
+      bu.execute();
+    } catch (NoPatchSetsWouldRemainException e) {
+      for (DeletePatchSetFromDbOp op : ops) {
+        op.p.status = Status.FIX_FAILED;
+        op.p.outcome = e.getMessage();
+      }
+    } catch (UpdateException | RestApiException e) {
       String msg = "Error deleting patch set";
-      log.warn(msg + ' ' + psId, e);
-      p.status = Status.FIX_FAILED;
-      p.outcome = msg;
+      log.warn(msg + " of change " + ops.get(0).psId.getParentKey(), e);
+      for (DeletePatchSetFromDbOp op : ops) {
+        // Overwrite existing statuses that were set before the transaction was
+        // rolled back.
+        op.p.status = Status.FIX_FAILED;
+        op.p.outcome = msg;
+      }
+    }
+  }
+
+  private class DeletePatchSetFromDbOp extends BatchUpdate.Op {
+    private final ProblemInfo p;
+    private final PatchSet.Id psId;
+
+    private DeletePatchSetFromDbOp(ProblemInfo p, PatchSet.Id psId) {
+      this.p = p;
+      this.psId = psId;
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext ctx)
+        throws OrmException, PatchSetInfoNotAvailableException {
+      // Delete dangling key references.
+      ReviewDb db = DeleteDraftChangeOp.unwrap(ctx.getDb());
+      accountPatchReviewStore.get().clearReviewed(psId);
+      db.changeMessages().delete(
+          db.changeMessages().byChange(psId.getParentKey()));
+      db.patchSetApprovals().delete(
+          db.patchSetApprovals().byPatchSet(psId));
+      db.patchComments().delete(
+          db.patchComments().byPatchSet(psId));
+      db.patchSets().deleteKeys(Collections.singleton(psId));
+
+      // NoteDb requires no additional fiddling; setting the state to deleted is
+      // sufficient to filter everything else out.
+      ctx.getUpdate(psId).setPatchSetState(PatchSetState.DELETED);
+
+      p.status = Status.FIXED;
+      p.outcome = "Deleted patch set";
+      return true;
+    }
+  }
+
+  private static class NoPatchSetsWouldRemainException
+      extends RestApiException {
+    private static final long serialVersionUID = 1L;
+
+    private NoPatchSetsWouldRemainException() {
+      super("Cannot delete patch set; no patch sets would remain");
+    }
+  }
+
+  private class UpdateCurrentPatchSetOp extends BatchUpdate.Op {
+    private final Set<PatchSet.Id> toDelete;
+
+    private UpdateCurrentPatchSetOp(List<DeletePatchSetFromDbOp> deleteOps) {
+      toDelete = new HashSet<>();
+      for (DeletePatchSetFromDbOp op : deleteOps) {
+        toDelete.add(op.psId);
+      }
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext ctx)
+        throws OrmException, PatchSetInfoNotAvailableException,
+        NoPatchSetsWouldRemainException {
+      if (!toDelete.contains(ctx.getChange().currentPatchSetId())) {
+        return false;
+      }
+      Set<PatchSet.Id> all = new HashSet<>();
+      // Doesn't make any assumptions about the order in which deletes happen
+      // and whether they are seen by this op; we are already given the full set
+      // of patch sets that will eventually be deleted in this update.
+      for (PatchSet ps : psUtil.byChange(ctx.getDb(), ctx.getNotes())) {
+        if (!toDelete.contains(ps.getId())) {
+          all.add(ps.getId());
+        }
+      }
+      if (all.isEmpty()) {
+        throw new NoPatchSetsWouldRemainException();
+      }
+      PatchSet.Id latest = ReviewDbUtil.intKeyOrdering().max(all);
+      ctx.getChange().setCurrentPatchSet(
+          patchSetInfoFactory.get(ctx.getDb(), ctx.getNotes(), latest));
+      return true;
     }
   }
 
@@ -670,7 +760,7 @@
 
   private ProblemInfo problem(String msg) {
     ProblemInfo p = new ProblemInfo();
-    p.message = msg;
+    p.message = checkNotNull(msg);
     problems.add(p);
     return p;
   }
@@ -687,6 +777,10 @@
   }
 
   private void warn(Throwable t) {
-    log.warn("Error in consistency check of change " + change.getId(), t);
+    log.warn("Error in consistency check of change " + ctl.getId(), t);
+  }
+
+  private Result result() {
+    return Result.create(ctl, problems);
   }
 }
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 6d8a2e0..f5ddfe5 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
@@ -16,14 +16,17 @@
 
 import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.MergeInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -50,10 +53,12 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.gerrit.server.project.RefControl;
@@ -99,6 +104,8 @@
   private final BatchUpdate.Factory updateFactory;
   private final PatchSetUtil psUtil;
   private final boolean allowDrafts;
+  private final MergeUtil.Factory mergeUtilFactory;
+  private final SubmitType submitType;
 
   @Inject
   CreateChange(@AnonymousCowardName String anonymousCowardName,
@@ -114,7 +121,8 @@
       ChangeFinder changeFinder,
       BatchUpdate.Factory updateFactory,
       PatchSetUtil psUtil,
-      @GerritServerConfig Config config) {
+      @GerritServerConfig Config config,
+      MergeUtil.Factory mergeUtilFactory) {
     this.anonymousCowardName = anonymousCowardName;
     this.db = db;
     this.gitManager = gitManager;
@@ -129,6 +137,9 @@
     this.updateFactory = updateFactory;
     this.psUtil = psUtil;
     this.allowDrafts = config.getBoolean("change", "allowDrafts", true);
+    this.submitType = config
+        .getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
+    this.mergeUtilFactory = mergeUtilFactory;
   }
 
   @Override
@@ -173,7 +184,8 @@
 
     Project.NameKey project = rsrc.getNameKey();
     try (Repository git = gitManager.openRepository(project);
-        RevWalk rw = new RevWalk(git)) {
+         ObjectInserter oi = git.newObjectInserter();
+         RevWalk rw = new RevWalk(oi.newReader())) {
       ObjectId parentCommit;
       List<String> groups;
       if (input.baseChange != null) {
@@ -219,42 +231,54 @@
       GeneralPreferencesInfo info =
           account.getAccount().getGeneralPreferencesInfo();
 
-      try (ObjectInserter oi = git.newObjectInserter()) {
-        ObjectId treeId =
-            mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree();
-        ObjectId id = ChangeIdUtil.computeChangeId(treeId,
-            mergeTip, author, author, input.subject);
-        String commitMessage = ChangeIdUtil.insertId(input.subject, id);
-        if (Boolean.TRUE.equals(info.signedOffBy)) {
-          commitMessage += String.format("%s%s",
-              SIGNED_OFF_BY_TAG,
-              account.getAccount().getNameEmail(anonymousCowardName));
-        }
-
-        RevCommit c = newCommit(oi, rw, author, mergeTip, commitMessage);
-
-        Change.Id changeId = new Change.Id(seq.nextChangeId());
-        ChangeInserter ins = changeInserterFactory.create(changeId, c, refName)
-            .setValidatePolicy(CommitValidators.Policy.GERRIT);
-        ins.setMessage(String.format("Uploaded patch set %s.",
-            ins.getPatchSetId().get()));
-        String topic = input.topic;
-        if (topic != null) {
-          topic = Strings.emptyToNull(topic.trim());
-        }
-        ins.setTopic(topic);
-        ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
-        ins.setGroups(groups);
-        try (BatchUpdate bu = updateFactory.create(
-            db.get(), project, me, now)) {
-          bu.setRepository(git, rw, oi);
-          bu.insertChange(ins);
-          bu.execute();
-        }
-        ChangeJson json = jsonFactory.create(ChangeJson.NO_OPTIONS);
-        return Response.created(json.format(ins.getChange()));
+      ObjectId treeId =
+          mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree();
+      ObjectId id = ChangeIdUtil.computeChangeId(treeId,
+          mergeTip, author, author, input.subject);
+      String commitMessage = ChangeIdUtil.insertId(input.subject, id);
+      if (Boolean.TRUE.equals(info.signedOffBy)) {
+        commitMessage += String.format("%s%s",
+            SIGNED_OFF_BY_TAG,
+            account.getAccount().getNameEmail(anonymousCowardName));
       }
 
+      RevCommit c;
+      if (input.merge != null) {
+        // create a merge commit
+        if (!(submitType.equals(SubmitType.MERGE_ALWAYS) ||
+              submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
+          throw new BadRequestException(
+              "Submit type: " + submitType + " is not supported");
+        }
+        c = newMergeCommit(git, oi, rw, rsrc.getControl(), mergeTip, input.merge,
+            author, commitMessage);
+      } else {
+        // create an empty commit
+        c = newCommit(oi, rw, author, mergeTip, commitMessage);
+      }
+
+      Change.Id changeId = new Change.Id(seq.nextChangeId());
+      ChangeInserter ins = changeInserterFactory.create(changeId, c, refName)
+          .setValidatePolicy(CommitValidators.Policy.GERRIT);
+      ins.setMessage(String.format("Uploaded patch set %s.",
+          ins.getPatchSetId().get()));
+      String topic = input.topic;
+      if (topic != null) {
+        topic = Strings.emptyToNull(topic.trim());
+      }
+      ins.setTopic(topic);
+      ins.setDraft(input.status == ChangeStatus.DRAFT);
+      ins.setGroups(groups);
+      try (BatchUpdate bu = updateFactory.create(
+          db.get(), project, me, now)) {
+        bu.setRepository(git, rw, oi);
+        bu.insertChange(ins);
+        bu.execute();
+      }
+      ChangeJson json = jsonFactory.create(ChangeJson.NO_OPTIONS);
+      return Response.created(json.format(ins.getChange()));
+    } catch (IllegalArgumentException e) {
+      throw new BadRequestException(e.getMessage());
     }
   }
 
@@ -274,6 +298,31 @@
     return rw.parseCommit(insert(oi, commit));
   }
 
+  private RevCommit newMergeCommit(Repository repo, ObjectInserter oi,
+      RevWalk rw, ProjectControl projectControl, RevCommit mergeTip,
+      MergeInput merge, PersonIdent authorIdent, String commitMessage)
+      throws RestApiException, IOException {
+    if (Strings.isNullOrEmpty(merge.source)) {
+      throw new BadRequestException("merge.source must be non-empty");
+    }
+
+    RevCommit sourceCommit = MergeUtil.resolveCommit(repo, rw, merge.source);
+    if (!projectControl.canReadCommit(db.get(), repo, sourceCommit)) {
+      throw new BadRequestException(
+          "do not have read permission for: " + merge.source);
+    }
+
+    MergeUtil mergeUtil =
+        mergeUtilFactory.create(projectControl.getProjectState());
+    // default merge strategy from project settings
+    String mergeStrategy = MoreObjects.firstNonNull(
+        Strings.emptyToNull(merge.strategy),
+        mergeUtil.mergeStrategyName());
+
+    return MergeUtil.createMergeCommit(repo, oi, mergeTip, sourceCommit,
+        mergeStrategy, authorIdent, commitMessage, rw);
+  }
+
   private static ObjectId insert(ObjectInserter inserter,
       CommitBuilder commit) throws IOException,
       UnsupportedEncodingException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
index bfbc828..7cb2aac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.server.change;
 
 import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+import static com.google.gerrit.server.change.PutDraftComment.side;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.DraftInput;
-import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -117,9 +117,9 @@
           new PatchLineComment.Key(
               new Patch.Key(ps.getId(), in.path),
               ChangeUtil.messageUUID(ctx.getDb())),
-          line, ctx.getUser().getAccountId(), Url.decode(in.inReplyTo),
+          line, ctx.getAccountId(), Url.decode(in.inReplyTo),
           ctx.getWhen());
-      comment.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
+      comment.setSide(side(in));
       comment.setMessage(in.message.trim());
       comment.setRange(in.range);
       comment.setTag(in.tag);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index a2a59e9..a14cf6b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
@@ -42,9 +43,11 @@
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.BatchUpdateReviewDb;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.mail.DeleteReviewerSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -75,6 +78,7 @@
   private final ReviewerDeleted reviewerDeleted;
   private final Provider<IdentifiedUser> user;
   private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
+  private final NotesMigration migration;
 
   @Inject
   DeleteReviewer(Provider<ReviewDb> dbProvider,
@@ -85,7 +89,8 @@
       IdentifiedUser.GenericFactory userFactory,
       ReviewerDeleted reviewerDeleted,
       Provider<IdentifiedUser> user,
-      DeleteReviewerSender.Factory deleteReviewerSenderFactory) {
+      DeleteReviewerSender.Factory deleteReviewerSenderFactory,
+      NotesMigration migration) {
     this.dbProvider = dbProvider;
     this.approvalsUtil = approvalsUtil;
     this.psUtil = psUtil;
@@ -95,6 +100,7 @@
     this.reviewerDeleted = reviewerDeleted;
     this.user = user;
     this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
+    this.migration = migration;
   }
 
   @Override
@@ -133,7 +139,7 @@
         throw new ResourceNotFoundException();
       }
       currChange = ctx.getChange();
-      currPs = psUtil.current(dbProvider.get(), ctx.getNotes());
+      currPs = psUtil.current(ctx.getDb(), ctx.getNotes());
 
       LabelTypes labelTypes = ctx.getControl().getLabelTypes();
       // removing a reviewer will remove all her votes
@@ -142,48 +148,51 @@
       }
 
       StringBuilder msg = new StringBuilder();
+      msg.append("Removed reviewer " + reviewer.getFullName());
+      StringBuilder removedVotesMsg = new StringBuilder();
+      removedVotesMsg.append(" with the following votes:\n\n");
+      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());
-            if (msg.length() == 0) {
-              msg.append("Removed reviewer ").append(reviewer.getFullName())
-                  .append(" with the following votes:\n\n");
-            }
-            msg.append("* ").append(a.getLabel())
+            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 not permitted");
+          throw new AuthException("delete reviewer not permitted");
         }
       }
+
+      if (votesRemoved) {
+        msg.append(removedVotesMsg);
+      } else {
+        msg.append(".");
+      }
+
       ctx.getDb().patchSetApprovals().delete(del);
       ChangeUpdate update = ctx.getUpdate(currPs.getId());
       update.removeReviewer(reviewerId);
 
-      if (msg.length() > 0) {
-        changeMessage = new ChangeMessage(
-            new ChangeMessage.Key(currChange.getId(),
-                ChangeUtil.messageUUID(ctx.getDb())),
-            ctx.getUser().getAccountId(), ctx.getWhen(), currPs.getId());
-        changeMessage.setMessage(msg.toString());
-        cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage);
-      }
+      changeMessage = new ChangeMessage(
+          new ChangeMessage.Key(currChange.getId(),
+              ChangeUtil.messageUUID(ctx.getDb())),
+          ctx.getAccountId(), ctx.getWhen(), currPs.getId());
+      changeMessage.setMessage(msg.toString());
+      cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage);
 
       return true;
     }
 
     @Override
     public void postUpdate(Context ctx) {
-      if (changeMessage == null) {
-        return;
-      }
-
       emailReviewers(ctx.getProject(), currChange, del, changeMessage);
       reviewerDeleted.fire(currChange, currPs, reviewer,
+          ctx.getAccount(),
           changeMessage.getMessage(),
           newApprovals, oldApprovals,
           ctx.getWhen());
@@ -191,8 +200,26 @@
 
     private Iterable<PatchSetApproval> approvals(ChangeContext ctx,
         final Account.Id accountId) throws OrmException {
+      Change.Id changeId = ctx.getNotes().getChangeId();
+      Iterable<PatchSetApproval> approvals;
+
+      if (migration.readChanges()) {
+        // Because NoteDb and ReviewDb have different semantics for zero-value
+        // approvals, we must fall back to ReviewDb as the source of truth here.
+        ReviewDb db = ctx.getDb();
+
+        if (db instanceof BatchUpdateReviewDb) {
+          db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
+        }
+        db = ReviewDbUtil.unwrapDb(db);
+        approvals = db.patchSetApprovals().byChange(changeId);
+      } else {
+        approvals =
+            approvalsUtil.byChange(ctx.getDb(), ctx.getNotes()).values();
+      }
+
       return Iterables.filter(
-          approvalsUtil.byChange(ctx.getDb(), ctx.getNotes()).values(),
+          approvals,
           new Predicate<PatchSetApproval>() {
             @Override
             public boolean apply(PatchSetApproval input) {
@@ -226,7 +253,8 @@
             deleteReviewerSenderFactory.create(projectName, change.getId());
         cm.setFrom(userId);
         cm.addReviewers(toMail);
-        cm.setChangeMessage(changeMessage);
+        cm.setChangeMessage(changeMessage.getMessage(),
+            changeMessage.getWrittenOn());
         cm.send();
       } catch (Exception err) {
         log.error("Cannot email update for change " + change.getId(), err);
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 3ebde05..f1bdba5 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
@@ -18,7 +18,7 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -36,7 +36,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.extensions.events.CommentAdded;
+import com.google.gerrit.server.extensions.events.VoteDeleted;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.BatchUpdate.Context;
@@ -67,7 +67,7 @@
   private final PatchSetUtil psUtil;
   private final ChangeMessagesUtil cmUtil;
   private final IdentifiedUser.GenericFactory userFactory;
-  private final CommentAdded commentAdded;
+  private final VoteDeleted voteDeleted;
   private final DeleteVoteSender.Factory deleteVoteSenderFactory;
 
   @Inject
@@ -77,7 +77,7 @@
       PatchSetUtil psUtil,
       ChangeMessagesUtil cmUtil,
       IdentifiedUser.GenericFactory userFactory,
-      CommentAdded commentAdded,
+      VoteDeleted voteDeleted,
       DeleteVoteSender.Factory deleteVoteSenderFactory) {
     this.db = db;
     this.batchUpdateFactory = batchUpdateFactory;
@@ -85,7 +85,7 @@
     this.psUtil = psUtil;
     this.cmUtil = cmUtil;
     this.userFactory = userFactory;
-    this.commentAdded = commentAdded;
+    this.voteDeleted = voteDeleted;
     this.deleteVoteSenderFactory = deleteVoteSenderFactory;
   }
 
@@ -173,7 +173,7 @@
             break;
           }
         } else {
-          throw new AuthException("delete not permitted");
+          throw new AuthException("delete vote not permitted");
         }
       }
       if (psa == null) {
@@ -185,7 +185,7 @@
         changeMessage =
             new ChangeMessage(new ChangeMessage.Key(change.getId(),
                 ChangeUtil.messageUUID(ctx.getDb())),
-                ctx.getUser().asIdentifiedUser().getAccountId(),
+                ctx.getAccountId(),
                 ctx.getWhen(),
                 change.currentPatchSetId());
         changeMessage.setMessage(msg.toString());
@@ -201,13 +201,13 @@
         return;
       }
 
-      IdentifiedUser user = ctx.getUser().asIdentifiedUser();
+      IdentifiedUser user = ctx.getIdentifiedUser();
       if (input.notify.compareTo(NotifyHandling.NONE) > 0) {
         try {
           ReplyToChangeSender cm = deleteVoteSenderFactory.create(
               ctx.getProject(), change.getId());
           cm.setFrom(user.getAccountId());
-          cm.setChangeMessage(changeMessage);
+          cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen());
           cm.setNotify(input.notify);
           cm.send();
         } catch (Exception e) {
@@ -215,10 +215,9 @@
         }
       }
 
-      commentAdded.fire(change, ps, user.getAccount(),
-          changeMessage.getMessage(),
-          newApprovals, oldApprovals,
-          ctx.getWhen());
+      voteDeleted.fire(change, ps,
+          newApprovals, oldApprovals, input.notify, changeMessage.getMessage(),
+          user.getAccount(), ctx.getWhen());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
index 3f333af..390f416 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.server.PatchLineCommentsUtil.PLC_ORDER;
 
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -109,7 +109,7 @@
       cm.setFrom(user.getAccountId());
       cm.setPatchSet(patchSet,
           patchSetInfoFactory.get(notes.getProjectName(), patchSet));
-      cm.setChangeMessage(message);
+      cm.setChangeMessage(message.getMessage(), message.getWrittenOn());
       cm.setPatchLineComments(comments);
       cm.setNotify(notify);
       cm.send();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
index 45794fb..e0591f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.gerrit.server.util.GitUtil.getParent;
+
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.common.FileInfo;
@@ -21,6 +23,7 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
@@ -29,17 +32,24 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
 
+import java.io.IOException;
 import java.util.Map;
 import java.util.TreeMap;
 
 @Singleton
 public class FileInfoJson {
   private final PatchListCache patchListCache;
+  private final GitRepositoryManager repoManager;
 
   @Inject
-  FileInfoJson(PatchListCache patchListCache) {
+  FileInfoJson(
+      PatchListCache patchListCache,
+      GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
     this.patchListCache = patchListCache;
   }
 
@@ -54,6 +64,22 @@
         ? null
         : ObjectId.fromString(base.getRevision().get());
     ObjectId b = ObjectId.fromString(revision.get());
+    return toFileInfoMap(change, a, b);
+  }
+
+  Map<String, FileInfo> toFileInfoMap(Change change, RevId revision, int parent)
+      throws RepositoryNotFoundException, IOException,
+          PatchListNotAvailableException {
+    ObjectId b = ObjectId.fromString(revision.get());
+    ObjectId a;
+    try (Repository git = repoManager.openRepository(change.getProject())) {
+      a = getParent(git, b, parent);
+    }
+    return toFileInfoMap(change, a, b);
+  }
+
+  private Map<String, FileInfo> toFileInfoMap(Change change,
+      ObjectId a, ObjectId b) throws PatchListNotAvailableException {
     PatchList list = patchListCache.get(
         new PatchListKey(a, b, Whitespace.IGNORE_NONE), change.getProject());
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index 7f74967..1631d48 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -48,6 +49,7 @@
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
@@ -67,11 +69,15 @@
 public class Files implements ChildCollection<RevisionResource, FileResource> {
   private final DynamicMap<RestView<FileResource>> views;
   private final Provider<ListFiles> list;
+  private final GitRepositoryManager repoManager;
 
   @Inject
-  Files(DynamicMap<RestView<FileResource>> views, Provider<ListFiles> list) {
+  Files(DynamicMap<RestView<FileResource>> views,
+      Provider<ListFiles> list,
+      GitRepositoryManager repoManager) {
     this.views = views;
     this.list = list;
+    this.repoManager = repoManager;
   }
 
   @Override
@@ -85,8 +91,20 @@
   }
 
   @Override
-  public FileResource parse(RevisionResource rev, IdString id) {
-    return new FileResource(rev, id.get());
+  public FileResource parse(RevisionResource rev, IdString id)
+      throws ResourceNotFoundException, IOException {
+    if (Patch.COMMIT_MSG.equals(id.get())) {
+      return new FileResource(rev, id.get());
+    }
+    try (Repository repo = repoManager.openRepository(rev.getProject());
+        RevWalk rw = new RevWalk(repo)) {
+      RevTree tree = rw.parseTree(
+          ObjectId.fromString(rev.getPatchSet().getRevision().get()));
+      if (TreeWalk.forPath(repo, id.get(), tree) != null) {
+        return new FileResource(rev, id.get());
+      }
+    }
+    throw new ResourceNotFoundException(id);
   }
 
   public static final class ListFiles implements RestReadView<RevisionResource> {
@@ -95,6 +113,9 @@
     @Option(name = "--base", metaVar = "revision-id")
     String base;
 
+    @Option(name = "--parent", metaVar = "parent-number")
+    int parentNum;
+
     @Option(name = "--reviewed")
     boolean reviewed;
 
@@ -145,24 +166,33 @@
         return Response.ok(query(resource));
       }
 
-      PatchSet basePatchSet = null;
-      if (base != null) {
-        RevisionResource baseResource = revisions.parse(
-            resource.getChangeResource(), IdString.fromDecoded(base));
-        basePatchSet = baseResource.getPatchSet();
-      }
+      Response<Map<String, FileInfo>> r;
       try {
-        Response<Map<String, FileInfo>> r = Response.ok(fileInfoJson.toFileInfoMap(
-            resource.getChange(),
-            resource.getPatchSet().getRevision(),
-            basePatchSet));
-        if (resource.isCacheable()) {
-          r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+        if (base != null) {
+          RevisionResource baseResource = revisions.parse(
+              resource.getChangeResource(), IdString.fromDecoded(base));
+          r = Response.ok(fileInfoJson.toFileInfoMap(
+              resource.getChange(),
+              resource.getPatchSet().getRevision(),
+              baseResource.getPatchSet()));
+        } else if (parentNum > 0) {
+          r = Response.ok(fileInfoJson.toFileInfoMap(
+              resource.getChange(),
+              resource.getPatchSet().getRevision(),
+              parentNum - 1));
+        } else {
+          r = Response.ok(fileInfoJson.toFileInfoMap(
+              resource.getChange(),
+              resource.getPatchSet()));
         }
-        return r;
       } catch (PatchListNotAvailableException e) {
         throw new ResourceNotFoundException(e.getMessage());
       }
+
+      if (resource.isCacheable()) {
+        r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+      }
+      return r;
     }
 
     private void checkOptions() throws BadRequestException {
@@ -170,6 +200,9 @@
       if (base != null) {
         supplied++;
       }
+      if (parentNum > 0) {
+        supplied++;
+      }
       if (reviewed) {
         supplied++;
       }
@@ -177,7 +210,8 @@
         supplied++;
       }
       if (supplied > 1) {
-        throw new BadRequestException("cannot combine base, reviewed, query");
+        throw new BadRequestException(
+            "cannot combine base, parent, reviewed, query");
       }
     }
 
@@ -306,5 +340,10 @@
       this.base = base;
       return this;
     }
+
+    public ListFiles setParent(int parentNum) {
+      this.parentNum = parentNum;
+      return this;
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
index 72f8c35..8c9a0ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
@@ -53,8 +53,7 @@
       RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
       rw.parseBody(commit);
       CommitInfo info = json.create(ChangeJson.NO_OPTIONS)
-          .toCommit(rsrc.getControl(), rw, commit, addLinks);
-      info.commit = commit.name();
+          .toCommit(rsrc.getControl(), rw, commit, addLinks, true);
       Response<CommitInfo> r = Response.ok(info);
       if (rsrc.isCacheable()) {
         r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
index 509bbd4..48bd2f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
@@ -43,6 +43,7 @@
     delegate.addOption(ListChangesOption.DETAILED_LABELS);
     delegate.addOption(ListChangesOption.DETAILED_ACCOUNTS);
     delegate.addOption(ListChangesOption.MESSAGES);
+    delegate.addOption(ListChangesOption.REVIEWER_UPDATES);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index 3d02b83..5cf5895 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -90,6 +90,9 @@
   @Option(name = "--base", metaVar = "REVISION")
   String base;
 
+  @Option(name = "--parent", metaVar = "parent-number")
+  int parentNum;
+
   @Deprecated
   @Option(name = "--ignore-whitespace")
   IgnoreWhitespace ignoreWhitespace;
@@ -121,30 +124,46 @@
   public Response<DiffInfo> apply(FileResource resource)
       throws ResourceConflictException, ResourceNotFoundException,
       OrmException, AuthException, InvalidChangeOperationException, IOException {
-    PatchSet basePatchSet = null;
-    if (base != null) {
-      RevisionResource baseResource = revisions.parse(
-          resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
-      basePatchSet = baseResource.getPatchSet();
-    }
     DiffPreferencesInfo prefs = new DiffPreferencesInfo();
     if (whitespace != null) {
       prefs.ignoreWhitespace = whitespace;
     } else if (ignoreWhitespace != null) {
       prefs.ignoreWhitespace = ignoreWhitespace.whitespace;
     } else {
-      prefs.ignoreWhitespace = Whitespace.IGNORE_ALL;
+      prefs.ignoreWhitespace = Whitespace.IGNORE_LEADING_AND_TRAILING;
     }
     prefs.context = context;
     prefs.intralineDifference = intraline;
 
-    try {
-      PatchScriptFactory psf = patchScriptFactoryFactory.create(
+    PatchScriptFactory psf;
+    PatchSet basePatchSet = null;
+    if (base != null) {
+      RevisionResource baseResource = revisions.parse(
+          resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
+      basePatchSet = baseResource.getPatchSet();
+      psf = patchScriptFactoryFactory.create(
           resource.getRevision().getControl(),
           resource.getPatchKey().getFileName(),
-          basePatchSet != null ? basePatchSet.getId() : null,
+          basePatchSet.getId(),
           resource.getPatchKey().getParentKey(),
           prefs);
+    } else if (parentNum > 0) {
+      psf = patchScriptFactoryFactory.create(
+          resource.getRevision().getControl(),
+          resource.getPatchKey().getFileName(),
+          parentNum - 1,
+          resource.getPatchKey().getParentKey(),
+          prefs);
+    } else {
+      psf = patchScriptFactoryFactory.create(
+          resource.getRevision().getControl(),
+          resource.getPatchKey().getFileName(),
+          null,
+          resource.getPatchKey().getParentKey(),
+          prefs);
+    }
+
+    try {
       psf.setLoadHistory(false);
       psf.setLoadComments(context != DiffPreferencesInfo.WHOLE_FILE_CONTEXT);
       PatchScript ps = psf.call();
@@ -272,6 +291,11 @@
     return this;
   }
 
+  public GetDiff setParent(int parentNum) {
+    this.parentNum = parentNum;
+    return this;
+  }
+
   public GetDiff setContext(int context) {
     this.context = context;
     return this;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
index 3c4d79d..a13e7be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
@@ -167,7 +167,7 @@
 
   private static String fileName(RevWalk rw, RevCommit commit)
       throws IOException {
-    AbbreviatedObjectId id = rw.getObjectReader().abbreviate(commit, 8);
+    AbbreviatedObjectId id = rw.getObjectReader().abbreviate(commit, 7);
     return id.name() + ".diff";
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
index c90b3bc..533468d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.change.ReviewerJson.ReviewerInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
index 37d400c..404fe75 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.config.ExternalIncludedIn;
-import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -39,7 +41,6 @@
 
 import java.io.IOException;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.Map;
 
 @Singleton
@@ -48,13 +49,13 @@
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager repoManager;
   private final PatchSetUtil psUtil;
-  private final DynamicMap<ExternalIncludedIn> includedIn;
+  private final DynamicSet<ExternalIncludedIn> includedIn;
 
   @Inject
   IncludedIn(Provider<ReviewDb> db,
       GitRepositoryManager repoManager,
       PatchSetUtil psUtil,
-      DynamicMap<ExternalIncludedIn> includedIn) {
+      DynamicSet<ExternalIncludedIn> includedIn) {
     this.db = db;
     this.repoManager = repoManager;
     this.psUtil = psUtil;
@@ -80,13 +81,13 @@
       }
 
       IncludedInResolver.Result d = IncludedInResolver.resolve(r, rw, rev);
-      Map<String, Collection<String>> external = new HashMap<>();
-      for (DynamicMap.Entry<ExternalIncludedIn> i : includedIn) {
-        external.put(i.getExportName(),
-            i.getProvider().get().getIncludedIn(
-                project.get(), rev.name(), d.getTags(), d.getBranches()));
+      Multimap<String, String> external = ArrayListMultimap.create();
+      for (ExternalIncludedIn ext : includedIn) {
+        external.putAll(ext.getIncludedIn(project.get(), rev.name(),
+            d.getTags(), d.getBranches()));
       }
-      return new IncludedInInfo(d, (!external.isEmpty() ? external : null));
+      return new IncludedInInfo(d,
+          (!external.isEmpty() ? external.asMap() : null));
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
index 56857b4..0c3ecd9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -111,6 +111,7 @@
     List<RevCommit> before = new LinkedList<>();
     List<RevCommit> after = new LinkedList<>();
     partition(before, after);
+    rw.reset();
     // It is highly likely that the target is reachable from the "after" set
     // Within the "before" set we are trying to handle cases arising from clock skew
     return !includedIn(after, 1).isEmpty() || !includedIn(before, 1).isEmpty();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
index 9a0c691..ccbd552 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.change.ReviewerJson.ReviewerInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index 2dacef9..62d75aa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -26,6 +26,7 @@
 import com.google.common.cache.Weigher;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableBiMap;
+import com.google.common.util.concurrent.UncheckedExecutionException;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.cache.CacheModule;
@@ -229,7 +230,7 @@
     EntryKey key = new EntryKey(commit, into, submitType, mergeStrategy);
     try {
       return cache.get(key, new Loader(key, dest, repo));
-    } catch (ExecutionException e) {
+    } catch (ExecutionException | UncheckedExecutionException e) {
       log.error(String.format("Error checking mergeability of %s into %s (%s)",
             key.commit.name(), key.into.name(), key.submitType.name()),
           e.getCause());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
index 08ef76e..7796d18 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -110,6 +110,7 @@
       ProjectState projectState = projectCache.get(change.getProject());
       String strategy = mergeUtilFactory.create(projectState)
           .mergeStrategyName();
+      result.strategy = strategy;
       result.mergeable =
           isMergable(git, change, commit, ref, result.submitType, strategy);
 
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 6d160a9..e805ad0 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
@@ -19,6 +19,8 @@
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -45,7 +47,6 @@
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.ssh.NoSshInfo;
-import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -72,7 +73,6 @@
 
   // Injected fields.
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final ReviewDb db;
   private final CommitValidators.Factory commitValidatorsFactory;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
   private final RevisionCreated revisionCreated;
@@ -90,13 +90,12 @@
   private final ChangeControl origCtl;
 
   // Fields exposed as setters.
-  private SshInfo sshInfo;
   private String message;
   private CommitValidators.Policy validatePolicy =
       CommitValidators.Policy.GERRIT;
   private boolean draft;
   private List<String> groups = Collections.emptyList();
-  private boolean runHooks = true;
+  private boolean fireRevisionCreated = true;
   private boolean sendMail = true;
   private boolean allowClosed;
   private boolean copyApprovals = true;
@@ -109,8 +108,7 @@
   private ReviewerSet oldReviewers;
 
   @AssistedInject
-  public PatchSetInserter(ReviewDb db,
-      ApprovalsUtil approvalsUtil,
+  public PatchSetInserter(ApprovalsUtil approvalsUtil,
       ApprovalCopier approvalCopier,
       ChangeMessagesUtil cmUtil,
       PatchSetInfoFactory patchSetInfoFactory,
@@ -121,7 +119,6 @@
       @Assisted ChangeControl ctl,
       @Assisted PatchSet.Id psId,
       @Assisted RevCommit commit) {
-    this.db = db;
     this.approvalsUtil = approvalsUtil;
     this.approvalCopier = approvalCopier;
     this.cmUtil = cmUtil;
@@ -145,11 +142,6 @@
     return this;
   }
 
-  public PatchSetInserter setSshInfo(SshInfo sshInfo) {
-    this.sshInfo = sshInfo;
-    return this;
-  }
-
   public PatchSetInserter setValidatePolicy(CommitValidators.Policy validate) {
     this.validatePolicy = checkNotNull(validate);
     return this;
@@ -166,8 +158,8 @@
     return this;
   }
 
-  public PatchSetInserter setRunHooks(boolean runHooks) {
-    this.runHooks = runHooks;
+  public PatchSetInserter setFireRevisionCreated(boolean fireRevisionCreated) {
+    this.fireRevisionCreated = fireRevisionCreated;
     return this;
   }
 
@@ -198,8 +190,7 @@
 
   @Override
   public void updateRepo(RepoContext ctx)
-      throws ResourceConflictException, IOException {
-    init();
+      throws AuthException, ResourceConflictException, IOException, OrmException {
     validate(ctx);
     ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(),
         commit, getPatchSetId().toRefName(), ReceiveCommand.Type.CREATE));
@@ -208,6 +199,7 @@
   @Override
   public boolean updateChange(ChangeContext ctx)
       throws ResourceConflictException, OrmException, IOException {
+    ReviewDb db = ctx.getDb();
     ChangeControl ctl = ctx.getControl();
 
     change = ctx.getChange();
@@ -222,12 +214,12 @@
 
     List<String> newGroups = groups;
     if (newGroups.isEmpty()) {
-      PatchSet prevPs = psUtil.current(ctx.getDb(), ctx.getNotes());
+      PatchSet prevPs = psUtil.current(db, ctx.getNotes());
       if (prevPs != null) {
         newGroups = prevPs.getGroups();
       }
     }
-    patchSet = psUtil.insert(ctx.getDb(), ctx.getRevWalk(), ctx.getUpdate(psId),
+    patchSet = psUtil.insert(db, ctx.getRevWalk(), ctx.getUpdate(psId),
         psId, commit, draft, newGroups, null);
 
     if (sendMail) {
@@ -237,7 +229,7 @@
     if (message != null) {
       changeMessage = new ChangeMessage(
           new ChangeMessage.Key(ctl.getId(), ChangeUtil.messageUUID(db)),
-          ctx.getUser().getAccountId(), ctx.getWhen(), patchSet.getId());
+          ctx.getAccountId(), ctx.getWhen(), patchSet.getId());
       changeMessage.setMessage(message);
     }
 
@@ -261,9 +253,9 @@
       try {
         ReplacePatchSetSender cm = replacePatchSetFactory.create(
             ctx.getProject(), change.getId());
-        cm.setFrom(ctx.getUser().getAccountId());
+        cm.setFrom(ctx.getAccountId());
         cm.setPatchSet(patchSet, patchSetInfo);
-        cm.setChangeMessage(changeMessage);
+        cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen());
         cm.addReviewers(oldReviewers.byState(REVIEWER));
         cm.addExtraCC(oldReviewers.byState(CC));
         cm.send();
@@ -273,21 +265,24 @@
       }
     }
 
-    if (runHooks) {
-      revisionCreated.fire(change, patchSet, ctx.getUser().getAccountId());
-    }
-  }
-
-  private void init() {
-    if (sshInfo == null) {
-      sshInfo = new NoSshInfo();
+    NotifyHandling notify = sendMail
+        ? NotifyHandling.ALL
+        : NotifyHandling.NONE;
+    if (fireRevisionCreated) {
+      revisionCreated.fire(change, patchSet, ctx.getAccountId(),
+          ctx.getWhen(), notify);
     }
   }
 
   private void validate(RepoContext ctx)
-      throws ResourceConflictException, IOException {
+      throws AuthException, ResourceConflictException, IOException,
+      OrmException {
     CommitValidators cv = commitValidatorsFactory.create(
-        origCtl.getRefControl(), sshInfo, ctx.getRepository());
+        origCtl.getRefControl(), new NoSshInfo(), ctx.getRepository());
+
+    if (!origCtl.canAddPatchSet(ctx.getDb())) {
+      throw new AuthException("cannot add patch set");
+    }
 
     String refName = getPatchSetId().toRefName();
     CommitReceivedEvent event = new CommitReceivedEvent(
@@ -297,7 +292,7 @@
             refName.substring(0, refName.lastIndexOf('/') + 1) + "new"),
         origCtl.getProjectControl().getProject(),
         origCtl.getRefControl().getRefName(),
-        commit, ctx.getUser().asIdentifiedUser());
+        commit, ctx.getIdentifiedUser());
 
     try {
       switch (validatePolicy) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index c4cdab5..aa35da8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -17,13 +17,17 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+import static com.google.gerrit.server.change.PutDraftComment.side;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
 import com.google.common.hash.HashCode;
@@ -34,14 +38,18 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.ReviewResult;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -79,6 +87,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -94,10 +103,6 @@
 public class PostReview implements RestModifyView<RevisionResource, ReviewInput> {
   private static final Logger log = LoggerFactory.getLogger(PostReview.class);
 
-  static class Output {
-    Map<String, Short> labels;
-  }
-
   private final Provider<ReviewDb> db;
   private final BatchUpdate.Factory batchUpdateFactory;
   private final ChangesCollection changes;
@@ -110,6 +115,7 @@
   private final AccountsCollection accounts;
   private final EmailReviewComments.Factory email;
   private final CommentAdded commentAdded;
+  private final PostReviewers postReviewers;
 
   @Inject
   PostReview(Provider<ReviewDb> db,
@@ -123,7 +129,8 @@
       PatchListCache patchListCache,
       AccountsCollection accounts,
       EmailReviewComments.Factory email,
-      CommentAdded commentAdded) {
+      CommentAdded commentAdded,
+      PostReviewers postReviewers) {
     this.db = db;
     this.batchUpdateFactory = batchUpdateFactory;
     this.changes = changes;
@@ -136,16 +143,18 @@
     this.accounts = accounts;
     this.email = email;
     this.commentAdded = commentAdded;
+    this.postReviewers = postReviewers;
   }
 
   @Override
-  public Output apply(RevisionResource revision, ReviewInput input)
-      throws RestApiException, UpdateException, OrmException {
+  public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input)
+      throws RestApiException, UpdateException, OrmException, IOException {
     return apply(revision, input, TimeUtil.nowTs());
   }
 
-  public Output apply(RevisionResource revision, ReviewInput input,
-      Timestamp ts) throws RestApiException, UpdateException, OrmException {
+  public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input,
+      Timestamp ts)
+      throws RestApiException, UpdateException, OrmException, IOException {
     // Respect timestamp, but truncate at change created-on time.
     ts = Ordering.natural().max(ts, revision.getChange().getCreatedOn());
     if (revision.getEdit().isPresent()) {
@@ -165,16 +174,53 @@
       input.notify = NotifyHandling.NONE;
     }
 
+    Map<String, AddReviewerResult> reviewerJsonResults = null;
+    List<PostReviewers.Addition> reviewerResults = Lists.newArrayList();
+    boolean hasError = false;
+    boolean confirm = false;
+    if (input.reviewers != null) {
+      reviewerJsonResults = Maps.newHashMap();
+      for (AddReviewerInput reviewerInput : input.reviewers) {
+        PostReviewers.Addition result = postReviewers.prepareApplication(
+            revision.getChangeResource(), reviewerInput);
+        reviewerJsonResults.put(reviewerInput.reviewer, result.result);
+        if (result.result.error != null) {
+          hasError = true;
+          continue;
+        }
+        if (result.result.confirm != null) {
+          confirm = true;
+          continue;
+        }
+        reviewerResults.add(result);
+      }
+    }
+
+    ReviewResult output = new ReviewResult();
+    output.reviewers = reviewerJsonResults;
+    if (hasError || confirm) {
+      return Response.withStatusCode(SC_BAD_REQUEST, output);
+    }
+    output.labels = input.labels;
+
     try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
           revision.getChange().getProject(), revision.getUser(), ts)) {
+      // Apply reviewer changes first. Revision emails should be sent to the
+      // updated set of reviewers.
+      for (PostReviewers.Addition reviewerResult : reviewerResults) {
+        bu.addOp(revision.getChange().getId(), reviewerResult.op);
+      }
       bu.addOp(
           revision.getChange().getId(),
           new Op(revision.getPatchSet().getId(), input));
       bu.execute();
+
+      for (PostReviewers.Addition reviewerResult : reviewerResults) {
+        reviewerResult.gatherResults();
+      }
     }
-    Output output = new Output();
-    output.labels = input.labels;
-    return output;
+
+    return Response.ok(output);
   }
 
   private RevisionResource onBehalfOf(RevisionResource rev, ReviewInput in)
@@ -359,7 +405,7 @@
     @Override
     public boolean updateChange(ChangeContext ctx)
         throws OrmException, ResourceConflictException {
-      user = ctx.getUser().asIdentifiedUser();
+      user = ctx.getIdentifiedUser();
       notes = ctx.getNotes();
       ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
       boolean dirty = false;
@@ -426,7 +472,7 @@
           }
           e.setStatus(PatchLineComment.Status.PUBLISHED);
           e.setWrittenOn(ctx.getWhen());
-          e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
+          e.setSide(side(c));
           setCommentRevId(e, patchListCache, ctx.getChange(), ps);
           e.setMessage(c.message);
           e.setTag(in.tag);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index e21cf54..4166ca3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.gerrit.extensions.client.ReviewerState.CC;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+
+import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
@@ -21,6 +25,9 @@
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
+import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -34,12 +41,9 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.account.GroupMembers;
-import com.google.gerrit.server.change.ReviewerJson.PostResult;
-import com.google.gerrit.server.change.ReviewerJson.ReviewerInfo;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.ReviewerAdded;
 import com.google.gerrit.server.git.BatchUpdate;
@@ -49,6 +53,7 @@
 import com.google.gerrit.server.group.GroupsCollection;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.mail.AddReviewerSender;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gwtorm.server.OrmException;
@@ -62,15 +67,18 @@
 
 import java.io.IOException;
 import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 @Singleton
-public class PostReviewers implements RestModifyView<ChangeResource, AddReviewerInput> {
-  private static final Logger log = LoggerFactory
-      .getLogger(PostReviewers.class);
+public class PostReviewers
+    implements RestModifyView<ChangeResource, AddReviewerInput> {
+  private static final Logger log =
+      LoggerFactory.getLogger(PostReviewers.class);
 
   public static final int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
   public static final int DEFAULT_MAX_REVIEWERS = 20;
@@ -88,9 +96,9 @@
   private final Provider<IdentifiedUser> user;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final Config cfg;
-  private final AccountCache accountCache;
   private final ReviewerJson json;
   private final ReviewerAdded reviewerAdded;
+  private final NotesMigration migration;
 
   @Inject
   PostReviewers(AccountsCollection accounts,
@@ -106,9 +114,9 @@
       Provider<IdentifiedUser> user,
       IdentifiedUser.GenericFactory identifiedUserFactory,
       @GerritServerConfig Config cfg,
-      AccountCache accountCache,
       ReviewerJson json,
-      ReviewerAdded reviewerAdded) {
+      ReviewerAdded reviewerAdded,
+      NotesMigration migration) {
     this.accounts = accounts;
     this.reviewerFactory = reviewerFactory;
     this.approvalsUtil = approvalsUtil;
@@ -122,52 +130,67 @@
     this.user = user;
     this.identifiedUserFactory = identifiedUserFactory;
     this.cfg = cfg;
-    this.accountCache = accountCache;
     this.json = json;
     this.reviewerAdded = reviewerAdded;
+    this.migration = migration;
   }
 
   @Override
-  public PostResult apply(ChangeResource rsrc, AddReviewerInput input)
-      throws UpdateException, OrmException, RestApiException, IOException {
+  public AddReviewerResult apply(ChangeResource rsrc, AddReviewerInput input)
+      throws IOException, OrmException, RestApiException, UpdateException {
     if (input.reviewer == null) {
       throw new BadRequestException("missing reviewer field");
     }
 
+    Addition addition = prepareApplication(rsrc, input);
+    if (addition.op == null) {
+      return addition.result;
+    }
+    try (BatchUpdate bu = batchUpdateFactory.create(dbProvider.get(),
+        rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+      Change.Id id = rsrc.getChange().getId();
+      bu.addOp(id, addition.op);
+      bu.execute();
+      addition.gatherResults();
+    }
+    return addition.result;
+  }
+
+  public Addition prepareApplication(ChangeResource rsrc, AddReviewerInput input)
+      throws OrmException, RestApiException, IOException {
+    Account.Id accountId;
     try {
-      Account.Id accountId = accounts.parse(input.reviewer).getAccountId();
-      return putAccount(reviewerFactory.create(rsrc, accountId));
+      accountId = accounts.parse(input.reviewer).getAccountId();
     } catch (UnprocessableEntityException e) {
       try {
         return putGroup(rsrc, input);
       } catch (UnprocessableEntityException e2) {
-        throw new UnprocessableEntityException(MessageFormat.format(
-            ChangeMessages.get().reviewerNotFound,
-            input.reviewer));
+        throw new UnprocessableEntityException(MessageFormat
+            .format(ChangeMessages.get().reviewerNotFound, input.reviewer));
       }
     }
+    return putAccount(input.reviewer, reviewerFactory.create(rsrc, accountId),
+        input.state());
   }
 
-  private PostResult putAccount(ReviewerResource rsrc)
-      throws OrmException, UpdateException, RestApiException {
+  private Addition putAccount(String reviewer, ReviewerResource rsrc,
+      ReviewerState state) throws UnprocessableEntityException {
     Account member = rsrc.getReviewerUser().getAccount();
     ChangeControl control = rsrc.getReviewerControl();
-    PostResult result = new PostResult();
     if (isValidReviewer(member, control)) {
-      addReviewers(rsrc.getChangeResource(), result,
-          ImmutableMap.of(member.getId(), control));
+      return new Addition(reviewer, rsrc.getChangeResource(),
+          ImmutableMap.of(member.getId(), control), state);
     }
-    return result;
+    throw new UnprocessableEntityException("Change not visible to " + reviewer);
   }
 
-  private PostResult putGroup(ChangeResource rsrc, AddReviewerInput input)
-      throws UpdateException, RestApiException, OrmException, IOException {
-    GroupDescription.Basic group = groupsCollection.parseInternal(input.reviewer);
-    PostResult result = new PostResult();
+  private Addition putGroup(ChangeResource rsrc, AddReviewerInput input)
+      throws RestApiException, OrmException, IOException {
+    GroupDescription.Basic group =
+        groupsCollection.parseInternal(input.reviewer);
     if (!isLegalReviewerGroup(group.getGroupUUID())) {
-      result.error = MessageFormat.format(
-          ChangeMessages.get().groupIsNotAllowed, group.getName());
-      return result;
+      return fail(input.reviewer, MessageFormat.format(ChangeMessages.get().groupIsNotAllowed,
+          group.getName()));
     }
 
     Map<Account.Id, ChangeControl> reviewers = new HashMap<>();
@@ -187,22 +210,19 @@
     int maxAllowed =
         cfg.getInt("addreviewer", "maxAllowed", DEFAULT_MAX_REVIEWERS);
     if (maxAllowed > 0 && members.size() > maxAllowed) {
-      result.error = MessageFormat.format(
-          ChangeMessages.get().groupHasTooManyMembers, group.getName());
-      return result;
+      return fail(input.reviewer, MessageFormat.format(
+          ChangeMessages.get().groupHasTooManyMembers, group.getName()));
     }
 
     // if maxWithoutCheck is set to 0, we never ask for confirmation
-    int maxWithoutConfirmation =
-        cfg.getInt("addreviewer", "maxWithoutConfirmation",
-            DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
+    int maxWithoutConfirmation = cfg.getInt("addreviewer",
+        "maxWithoutConfirmation", DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
     if (!input.confirmed() && maxWithoutConfirmation > 0
         && members.size() > maxWithoutConfirmation) {
-      result.confirm = true;
-      result.error = MessageFormat.format(
-          ChangeMessages.get().groupManyMembersConfirmation,
-          group.getName(), members.size());
-      return result;
+      return fail(input.reviewer, true,
+          MessageFormat.format(
+              ChangeMessages.get().groupManyMembersConfirmation,
+              group.getName(), members.size()));
     }
 
     for (Account member : members) {
@@ -211,8 +231,7 @@
       }
     }
 
-    addReviewers(rsrc, result, reviewers);
-    return result;
+    return new Addition(input.reviewer, rsrc, reviewers, input.state());
   }
 
   private boolean isValidReviewer(Account member, ChangeControl control) {
@@ -225,78 +244,131 @@
     return false;
   }
 
+  private Addition fail(String reviewer, String error) {
+    return fail(reviewer, false, error);
+  }
 
-  private void addReviewers(
-      ChangeResource rsrc, PostResult result, Map<Account.Id, ChangeControl> reviewers)
-      throws OrmException, RestApiException, UpdateException {
-    try (BatchUpdate bu = batchUpdateFactory.create(
-            dbProvider.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
-      Op op = new Op(rsrc, reviewers);
-      Change.Id id = rsrc.getChange().getId();
-      bu.addOp(id, op);
-      bu.execute();
+  private Addition fail(String reviewer, boolean confirm, String error) {
+    Addition addition = new Addition(reviewer);
+    addition.result.confirm = confirm ? true : null;
+    addition.result.error = error;
+    return addition;
+  }
 
-      result.reviewers = Lists.newArrayListWithCapacity(op.added.size());
-      for (PatchSetApproval psa : op.added) {
-        // New reviewers have value 0, don't bother normalizing.
-        result.reviewers.add(
-          json.format(new ReviewerInfo(
-              psa.getAccountId()), reviewers.get(psa.getAccountId()),
-              ImmutableList.of(psa)));
+  class Addition {
+    final AddReviewerResult result;
+    final Op op;
+
+    private final Map<Account.Id, ChangeControl> reviewers;
+
+    protected Addition(String reviewer) {
+      this(reviewer, null, null, REVIEWER);
+    }
+
+    protected Addition(String reviewer, ChangeResource rsrc,
+        Map<Account.Id, ChangeControl> reviewers, ReviewerState state) {
+      result = new AddReviewerResult(reviewer);
+      if (reviewers == null) {
+        this.reviewers = ImmutableMap.of();
+        op = null;
+        return;
       }
+      this.reviewers = reviewers;
+      op = new Op(rsrc, reviewers, state);
+    }
 
-      // We don't do this inside Op, since the accounts are in a different
-      // table.
-      accountLoaderFactory.create(true).fill(result.reviewers);
+    void gatherResults() throws OrmException {
+      // Generate result details and fill AccountLoader. This occurs outside
+      // the Op because the accounts are in a different table.
+      if (migration.readChanges() && op.state == CC) {
+        result.ccs = Lists.newArrayListWithCapacity(op.addedCCs.size());
+        for (Account.Id accountId : op.addedCCs) {
+          result.ccs.add(
+              json.format(new ReviewerInfo(accountId.get()), reviewers.get(accountId)));
+        }
+        accountLoaderFactory.create(true).fill(result.ccs);
+      } else {
+        result.reviewers = Lists.newArrayListWithCapacity(op.addedReviewers.size());
+        for (PatchSetApproval psa : op.addedReviewers) {
+          // New reviewers have value 0, don't bother normalizing.
+          result.reviewers.add(
+            json.format(new ReviewerInfo(psa.getAccountId().get()),
+                reviewers.get(psa.getAccountId()),
+                ImmutableList.of(psa)));
+        }
+        accountLoaderFactory.create(true).fill(result.reviewers);
+      }
     }
   }
 
-  private class Op extends BatchUpdate.Op {
-    private final ChangeResource rsrc;
-    private final Map<Account.Id, ChangeControl> reviewers;
+  class Op extends BatchUpdate.Op {
+    final Map<Account.Id, ChangeControl> reviewers;
+    final ReviewerState state;
+    List<PatchSetApproval> addedReviewers;
+    Collection<Account.Id> addedCCs;
 
-    private List<PatchSetApproval> added;
+    private final ChangeResource rsrc;
     private PatchSet patchSet;
 
-    Op(ChangeResource rsrc, Map<Account.Id, ChangeControl> reviewers) {
+    Op(ChangeResource rsrc, Map<Account.Id, ChangeControl> reviewers,
+        ReviewerState state) {
       this.rsrc = rsrc;
       this.reviewers = reviewers;
+      this.state = state;
     }
 
     @Override
     public boolean updateChange(ChangeContext ctx)
         throws RestApiException, OrmException, IOException {
-      added =
-          approvalsUtil.addReviewers(
-              ctx.getDb(),
-              ctx.getNotes(),
-              ctx.getUpdate(ctx.getChange().currentPatchSetId()),
-              rsrc.getControl().getLabelTypes(),
-              rsrc.getChange(),
-              reviewers.keySet());
-
-      if (added.isEmpty()) {
-        return false;
+      if (migration.readChanges() && state == CC) {
+        addedCCs = approvalsUtil.addCcs(ctx.getNotes(),
+            ctx.getUpdate(ctx.getChange().currentPatchSetId()),
+            reviewers.keySet());
+        if (addedCCs.isEmpty()) {
+          return false;
+        }
+      } else {
+        addedReviewers = approvalsUtil.addReviewers(ctx.getDb(), ctx.getNotes(),
+            ctx.getUpdate(ctx.getChange().currentPatchSetId()),
+            rsrc.getControl().getLabelTypes(), rsrc.getChange(),
+            reviewers.keySet());
+        if (addedReviewers.isEmpty()) {
+          return false;
+        }
       }
+
       patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
       return true;
     }
 
     @Override
     public void postUpdate(Context ctx) throws Exception {
-      emailReviewers(rsrc.getChange(), added);
-
-      if (!added.isEmpty()) {
-        for (PatchSetApproval psa : added) {
-          Account account = accountCache.get(psa.getAccountId()).getAccount();
-          reviewerAdded.fire(rsrc.getChange(), patchSet, account);
+      if (addedReviewers != null || addedCCs != null) {
+        if (addedReviewers == null) {
+          addedReviewers = new ArrayList<>();
+        }
+        if (addedCCs == null) {
+          addedCCs = new ArrayList<>();
+        }
+        emailReviewers(rsrc.getChange(), addedReviewers, addedCCs);
+        if (!addedReviewers.isEmpty()) {
+          List<Account.Id> reviewers = Lists.transform(addedReviewers,
+              new Function<PatchSetApproval, Account.Id>() {
+                @Override
+                public Account.Id apply(PatchSetApproval psa) {
+                  return psa.getAccountId();
+                }
+              });
+          reviewerAdded.fire(rsrc.getChange(), patchSet, reviewers,
+              ctx.getAccount(), ctx.getWhen());
         }
       }
     }
   }
 
-  private void emailReviewers(Change change, List<PatchSetApproval> added) {
-    if (added.isEmpty()) {
+  private void emailReviewers(Change change, List<PatchSetApproval> added,
+      Collection<Account.Id> copied) {
+    if (added.isEmpty() && copied.isEmpty()) {
       return;
     }
 
@@ -310,18 +382,27 @@
         toMail.add(psa.getAccountId());
       }
     }
-    if (!toMail.isEmpty()) {
-      try {
-        AddReviewerSender cm = addReviewerSenderFactory
-            .create(change.getProject(), change.getId());
-        cm.setFrom(userId);
-        cm.addReviewers(toMail);
-        cm.send();
-      } catch (Exception err) {
-        log.error("Cannot send email to new reviewers of change "
-            + change.getId(), err);
+    List<Account.Id> toCopy = Lists.newArrayListWithCapacity(copied.size());
+    for (Account.Id id : copied) {
+      if (!id.equals(userId)) {
+        toCopy.add(id);
       }
     }
+    if (toMail.isEmpty() && toCopy.isEmpty()) {
+      return;
+    }
+
+    try {
+      AddReviewerSender cm = addReviewerSenderFactory
+          .create(change.getProject(), change.getId());
+      cm.setFrom(userId);
+      cm.addReviewers(toMail);
+      cm.addExtraCC(toCopy);
+      cm.send();
+    } catch (Exception err) {
+      log.error("Cannot send email to new reviewers of change "
+          + change.getId(), err);
+    }
   }
 
   public static boolean isLegalReviewerGroup(AccountGroup.UUID groupUUID) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index 32605bd..9d997f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -214,8 +214,8 @@
 
       List<FooterLine> footerLines = commit.getFooterLines();
       recipients = getRecipientsFromFooters(
-          accountResolver, patchSet.isDraft(), footerLines);
-      recipients.remove(ctx.getUser().getAccountId());
+          ctx.getDb(), accountResolver, patchSet.isDraft(), footerLines);
+      recipients.remove(ctx.getAccountId());
       approvalsUtil.addReviewers(ctx.getDb(), ctx.getUpdate(psId), labelTypes,
           change, patchSet, patchSetInfo, recipients.getReviewers(),
           oldReviewers);
@@ -223,7 +223,8 @@
 
     @Override
     public void postUpdate(Context ctx) throws OrmException {
-      draftPublished.fire(change, patchSet, ctx.getUser().getAccountId());
+      draftPublished.fire(change, patchSet, ctx.getAccountId(),
+          ctx.getWhen());
       if (patchSet.isDraft() && change.getStatus() == Change.Status.DRAFT) {
         // Skip emails if the patch set is still a draft.
         return;
@@ -242,7 +243,7 @@
     private void sendCreateChange(Context ctx) throws EmailException {
       CreateChangeSender cm =
           createChangeSenderFactory.create(ctx.getProject(), change.getId());
-      cm.setFrom(ctx.getUser().getAccountId());
+      cm.setFrom(ctx.getAccountId());
       cm.setPatchSet(patchSet, patchSetInfo);
       cm.addReviewers(recipients.getReviewers());
       cm.addExtraCC(recipients.getCcOnly());
@@ -251,7 +252,7 @@
 
     private void sendReplacePatchSet(Context ctx)
         throws EmailException, OrmException {
-      Account.Id accountId = ctx.getUser().getAccountId();
+      Account.Id accountId = ctx.getAccountId();
       ChangeMessage msg =
           new ChangeMessage(new ChangeMessage.Key(change.getId(),
               ChangeUtil.messageUUID(ctx.getDb())), accountId,
@@ -261,7 +262,7 @@
           replacePatchSetFactory.create(ctx.getProject(), change.getId());
       cm.setFrom(accountId);
       cm.setPatchSet(patchSet, patchSetInfo);
-      cm.setChangeMessage(msg);
+      cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
       cm.addReviewers(recipients.getReviewers());
       cm.addExtraCC(recipients.getCcOnly());
       cm.send();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
index 1d756f4..655e07d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -19,6 +19,7 @@
 import com.google.common.base.Optional;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -141,7 +142,7 @@
                 new Patch.Key(psId, in.path),
                 comment.getKey().get()),
             comment.getLine(),
-            ctx.getUser().getAccountId(),
+            ctx.getAccountId(),
             comment.getParentUuid(), ctx.getWhen());
         comment.setTag(origComment.getTag());
         setCommentRevId(comment, patchListCache, ctx.getChange(), ps);
@@ -163,7 +164,7 @@
   private static PatchLineComment update(PatchLineComment e, DraftInput in,
       Timestamp when) {
     if (in.side != null) {
-      e.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
+      e.setSide(side(in));
     }
     if (in.inReplyTo != null) {
       e.setParentUuid(Url.decode(in.inReplyTo));
@@ -180,4 +181,11 @@
     }
     return e;
   }
+
+  static short side(Comment c) {
+    if (c.side == Side.PARENT) {
+      return (short) (c.parent == null ? 0 : -c.parent.shortValue());
+    }
+    return 1;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index 8e5adea..31ae892 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -119,7 +119,7 @@
           new ChangeMessage.Key(
               change.getId(),
               ChangeUtil.messageUUID(ctx.getDb())),
-          ctx.getUser().getAccountId(), ctx.getWhen(),
+          ctx.getAccountId(), ctx.getWhen(),
           change.currentPatchSetId());
       cmsg.setMessage(summary);
       cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
@@ -130,8 +130,9 @@
     public void postUpdate(Context ctx) {
       if (change != null) {
         topicEdited.fire(change,
-            ctx.getUser().asIdentifiedUser().getAccount(),
-            oldTopicName);
+            ctx.getAccount(),
+            oldTopicName,
+            ctx.getWhen());
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index 84f7307..4b81c31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -107,7 +107,7 @@
             control, rsrc.getPatchSet(),
             findBaseRev(rw, rsrc, input))
           .setForceContentMerge(true)
-          .setRunHooks(true)
+          .setFireRevisionCreated(true)
           .setValidatePolicy(CommitValidators.Policy.GERRIT));
       bu.execute();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 2fbeff1..8909e60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -63,7 +63,7 @@
 
   private String baseCommitish;
   private PersonIdent committerIdent;
-  private boolean runHooks = true;
+  private boolean fireRevisionCreated = true;
   private CommitValidators.Policy validate;
   private boolean forceContentMerge;
   private boolean copyApprovals = true;
@@ -101,8 +101,8 @@
     return this;
   }
 
-  public RebaseChangeOp setRunHooks(boolean runHooks) {
-    this.runHooks = runHooks;
+  public RebaseChangeOp setFireRevisionCreated(boolean fireRevisionCreated) {
+    this.fireRevisionCreated = fireRevisionCreated;
     return this;
   }
 
@@ -151,7 +151,7 @@
         .create(ctl, rebasedPatchSetId, rebasedCommit)
         .setDraft(originalPatchSet.isDraft())
         .setSendMail(false)
-        .setRunHooks(runHooks)
+        .setFireRevisionCreated(fireRevisionCreated)
         .setCopyApprovals(copyApprovals)
         .setMessage(
           "Patch Set " + rebasedPatchSetId.get()
@@ -241,7 +241,7 @@
     if (committerIdent != null) {
       cb.setCommitter(committerIdent);
     } else {
-      cb.setCommitter(ctx.getUser().asIdentifiedUser()
+      cb.setCommitter(ctx.getIdentifiedUser()
           .newCommitterIdent(ctx.getWhen(), ctx.getTimeZone()));
     }
     ObjectId objectId = ctx.getInserter().insert(cb);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index b79bac3..9c4c6d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -136,7 +136,7 @@
           new ChangeMessage.Key(
               change.getId(),
               ChangeUtil.messageUUID(ctx.getDb())),
-          ctx.getUser().getAccountId(),
+          ctx.getAccountId(),
           ctx.getWhen(),
           change.currentPatchSetId());
       message.setMessage(msg.toString());
@@ -148,15 +148,16 @@
       try {
         ReplyToChangeSender cm =
             restoredSenderFactory.create(ctx.getProject(), change.getId());
-        cm.setFrom(ctx.getUser().getAccountId());
-        cm.setChangeMessage(message);
+        cm.setFrom(ctx.getAccountId());
+        cm.setChangeMessage(message.getMessage(), ctx.getWhen());
         cm.send();
       } catch (Exception e) {
         log.error("Cannot email update for change " + change.getId(), e);
       }
       changeRestored.fire(change, patchSet,
-          ctx.getUser().asIdentifiedUser().getAccount(),
-          Strings.emptyToNull(input.message));
+          ctx.getAccount(),
+          Strings.emptyToNull(input.message),
+          ctx.getWhen());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index 86e068c..ade46be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.extensions.events.ChangeReverted;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.BatchUpdate.Context;
@@ -87,6 +88,7 @@
   private final ChangeJson.Factory json;
   private final PersonIdent serverIdent;
   private final ApprovalsUtil approvalsUtil;
+  private final ChangeReverted changeReverted;
 
   @Inject
   Revert(Provider<ReviewDb> db,
@@ -99,7 +101,8 @@
       RevertedSender.Factory revertedSenderFactory,
       ChangeJson.Factory json,
       @GerritPersonIdent PersonIdent serverIdent,
-      ApprovalsUtil approvalsUtil) {
+      ApprovalsUtil approvalsUtil,
+      ChangeReverted changeReverted) {
     this.db = db;
     this.repoManager = repoManager;
     this.changeInserterFactory = changeInserterFactory;
@@ -111,6 +114,7 @@
     this.json = json;
     this.serverIdent = serverIdent;
     this.approvalsUtil = approvalsUtil;
+    this.changeReverted = changeReverted;
   }
 
   @Override
@@ -200,7 +204,7 @@
             db.get(), project, user, now)) {
           bu.setRepository(git, revWalk, oi);
           bu.insertChange(ins);
-          bu.addOp(changeId, new SendEmailOp(ins));
+          bu.addOp(changeId, new NotifyOp(ctl.getChange(), ins));
           bu.addOp(changeToRevert.getId(),
               new PostRevertedMessageOp(computedChangeId));
           bu.execute();
@@ -225,21 +229,24 @@
     return change != null ? change.getStatus().name().toLowerCase() : "deleted";
   }
 
-  private class SendEmailOp extends BatchUpdate.Op {
+  private class NotifyOp extends BatchUpdate.Op {
+    private final Change change;
     private final ChangeInserter ins;
 
-    SendEmailOp(ChangeInserter ins) {
+    NotifyOp(Change change, ChangeInserter ins) {
+      this.change = change;
       this.ins = ins;
     }
 
     @Override
     public void postUpdate(Context ctx) throws Exception {
+      changeReverted.fire(change, ins.getChange(), ctx.getWhen());
       Change.Id changeId = ins.getChange().getId();
       try {
         RevertedSender cm =
             revertedSenderFactory.create(ctx.getProject(), changeId);
-        cm.setFrom(ctx.getUser().getAccountId());
-        cm.setChangeMessage(ins.getChangeMessage());
+        cm.setFrom(ctx.getAccountId());
+        cm.setChangeMessage(ins.getChangeMessage().getMessage(), ctx.getWhen());
         cm.send();
       } catch (Exception err) {
         log.error("Cannot send email for revert change " + changeId, err);
@@ -261,7 +268,7 @@
       ChangeMessage changeMessage = new ChangeMessage(
           new ChangeMessage.Key(change.getId(),
               ChangeUtil.messageUUID(db.get())),
-          ctx.getUser().getAccountId(), ctx.getWhen(), patchSetId);
+          ctx.getAccountId(), ctx.getWhen(), patchSetId);
       StringBuilder msgBuf = new StringBuilder();
       msgBuf.append("Created a revert of this change as ")
           .append("I").append(computedChangeId.name());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index e24a290..69cd439 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
@@ -40,7 +40,6 @@
 
 import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 import java.util.TreeMap;
 
 @Singleton
@@ -67,7 +66,7 @@
     AccountLoader loader = accountLoaderFactory.create(true);
     for (ReviewerResource rsrc : rsrcs) {
       ReviewerInfo info = format(new ReviewerInfo(
-          rsrc.getReviewerUser().getAccountId()),
+          rsrc.getReviewerUser().getAccountId().get()),
           rsrc.getReviewerControl());
       loader.put(info);
       infos.add(info);
@@ -132,18 +131,4 @@
 
     return out;
   }
-
-  public static class ReviewerInfo extends AccountInfo {
-    Map<String, String> approvals;
-
-    protected ReviewerInfo(Account.Id id) {
-      super(id.get());
-    }
-  }
-
-  public static class PostResult {
-    public List<ReviewerInfo> reviewers;
-    public String error;
-    Boolean confirm;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
deleted file mode 100644
index 20078cc..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
+++ /dev/null
@@ -1,187 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import com.google.common.base.Splitter;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.AccountExternalIdAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import org.apache.lucene.analysis.standard.StandardAnalyzer;
-import org.apache.lucene.analysis.util.CharArraySet;
-import org.apache.lucene.document.Document;
-import org.apache.lucene.document.Field.Store;
-import org.apache.lucene.document.IntField;
-import org.apache.lucene.document.StringField;
-import org.apache.lucene.document.TextField;
-import org.apache.lucene.index.DirectoryReader;
-import org.apache.lucene.index.IndexWriter;
-import org.apache.lucene.index.IndexWriterConfig;
-import org.apache.lucene.index.IndexWriterConfig.OpenMode;
-import org.apache.lucene.index.IndexableField;
-import org.apache.lucene.index.Term;
-import org.apache.lucene.search.BooleanClause.Occur;
-import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.PrefixQuery;
-import org.apache.lucene.search.ScoreDoc;
-import org.apache.lucene.search.TopDocs;
-import org.apache.lucene.store.RAMDirectory;
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-/**
- * The suggest oracle may be called many times in rapid succession during the
- * course of one operation.
- * It would be easy to have a simple {@code Cache<Boolean, List<Account>>}
- * with a short expiration time of 30s.
- * Cache only has a single key we're just using Cache for the expiration behavior.
- */
-@Singleton
-public class ReviewerSuggestionCache {
-  private static final Logger log = LoggerFactory
-      .getLogger(ReviewerSuggestionCache.class);
-
-  private static final String ID = "id";
-  private static final String NAME = "name";
-  private static final String EMAIL = "email";
-  private static final String USERNAME = "username";
-  private static final String[] ALL = {ID, NAME, EMAIL, USERNAME};
-
-  private final LoadingCache<Boolean, IndexSearcher> cache;
-  private final Provider<ReviewDb> db;
-
-  @Inject
-  ReviewerSuggestionCache(Provider<ReviewDb> db,
-      @GerritServerConfig Config cfg) {
-    this.db = db;
-    long expiration = ConfigUtil.getTimeUnit(cfg,
-        "suggest", null, "fullTextSearchRefresh",
-        TimeUnit.HOURS.toMillis(1),
-        TimeUnit.MILLISECONDS);
-    this.cache =
-        CacheBuilder.newBuilder().maximumSize(1)
-            .refreshAfterWrite(expiration, TimeUnit.MILLISECONDS)
-            .build(new CacheLoader<Boolean, IndexSearcher>() {
-              @Override
-              public IndexSearcher load(Boolean key) throws Exception {
-                return index();
-              }
-            });
-  }
-
-  public List<AccountInfo> search(String query, int n) throws IOException {
-    IndexSearcher searcher = get();
-    if (searcher == null) {
-      return Collections.emptyList();
-    }
-
-    List<String> segments = Splitter.on(' ').omitEmptyStrings().splitToList(
-        query.toLowerCase());
-    BooleanQuery.Builder q = new BooleanQuery.Builder();
-    for (String field : ALL) {
-      BooleanQuery.Builder and = new BooleanQuery.Builder();
-      for (String s : segments) {
-        and.add(new PrefixQuery(new Term(field, s)), Occur.MUST);
-      }
-      q.add(and.build(), Occur.SHOULD);
-    }
-
-    TopDocs results = searcher.search(q.build(), n);
-    ScoreDoc[] hits = results.scoreDocs;
-
-    List<AccountInfo> result = new LinkedList<>();
-
-    for (ScoreDoc h : hits) {
-      Document doc = searcher.doc(h.doc);
-
-      IndexableField idField = checkNotNull(doc.getField(ID));
-      AccountInfo info = new AccountInfo(idField.numericValue().intValue());
-      info.name = doc.get(NAME);
-      info.email = doc.get(EMAIL);
-      info.username = doc.get(USERNAME);
-      result.add(info);
-    }
-
-    return result;
-  }
-
-  private IndexSearcher get() {
-    try {
-      return cache.get(true);
-    } catch (ExecutionException e) {
-      log.warn("Cannot fetch reviewers from cache", e);
-      return null;
-    }
-  }
-
-  private IndexSearcher index() throws IOException, OrmException {
-    RAMDirectory idx = new RAMDirectory();
-    IndexWriterConfig config = new IndexWriterConfig(
-        new StandardAnalyzer(CharArraySet.EMPTY_SET));
-    config.setOpenMode(OpenMode.CREATE);
-
-    try (IndexWriter writer = new IndexWriter(idx, config)) {
-      for (Account a : db.get().accounts().all()) {
-        if (a.isActive()) {
-          addAccount(writer, a);
-        }
-      }
-    }
-
-    return new IndexSearcher(DirectoryReader.open(idx));
-  }
-
-  private void addAccount(IndexWriter writer, Account a)
-      throws IOException, OrmException {
-    Document doc = new Document();
-    doc.add(new IntField(ID, a.getId().get(), Store.YES));
-    if (a.getFullName() != null) {
-      doc.add(new TextField(NAME, a.getFullName(), Store.YES));
-    }
-    if (a.getPreferredEmail() != null) {
-      doc.add(new TextField(EMAIL, a.getPreferredEmail(), Store.YES));
-      doc.add(new StringField(EMAIL, a.getPreferredEmail().toLowerCase(),
-          Store.YES));
-    }
-    AccountExternalIdAccess extIdAccess = db.get().accountExternalIds();
-    String username = AccountState.getUserName(
-        extIdAccess.byAccount(a.getId()).toList());
-    if (username != null) {
-      doc.add(new StringField(USERNAME, username, Store.YES));
-    }
-    writer.addDocument(doc);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
index 67a1432..34c611b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -58,7 +58,7 @@
   private final HashtagsEdited hashtagsEdited;
   private final HashtagsInput input;
 
-  private boolean runHooks = true;
+  private boolean fireEvent = true;
 
   private Change change;
   private Set<String> toAdd;
@@ -79,8 +79,8 @@
     this.input = input;
   }
 
-  public SetHashtagsOp setRunHooks(boolean runHooks) {
-    this.runHooks = runHooks;
+  public SetHashtagsOp setFireEvent(boolean fireEvent) {
+    this.fireEvent = fireEvent;
     return this;
   }
 
@@ -138,7 +138,7 @@
         new ChangeMessage.Key(
             change.getId(),
             ChangeUtil.messageUUID(ctx.getDb())),
-        ctx.getUser().getAccountId(), ctx.getWhen(),
+        ctx.getAccountId(), ctx.getWhen(),
         change.currentPatchSetId());
     cmsg.setMessage(msg.toString());
     cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
@@ -165,9 +165,9 @@
 
   @Override
   public void postUpdate(Context ctx) throws OrmException {
-    if (updated() && runHooks) {
-      hashtagsEdited.fire(change, ctx.getUser().getAccountId(), updatedHashtags,
-          toAdd, toRemove);
+    if (updated() && fireEvent) {
+      hashtagsEdited.fire(change, ctx.getAccountId(), updatedHashtags,
+          toAdd, toRemove, ctx.getWhen());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
index 3b61033..f159c69 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
@@ -27,7 +27,6 @@
 
 public class SuggestReviewers {
   private static final int DEFAULT_MAX_SUGGESTED = 10;
-  private static final int DEFAULT_MAX_MATCHES = 100;
 
   protected final Provider<ReviewDb> dbProvider;
   protected final IdentifiedUser.GenericFactory identifiedUserFactory;
@@ -36,10 +35,9 @@
   private final boolean suggestAccounts;
   private final int suggestFrom;
   private final int maxAllowed;
+  private final int maxAllowedWithoutConfirmation;
   protected int limit;
   protected String query;
-  private boolean useFullTextSearch;
-  private final int fullTextMaxMatches;
   protected final int maxSuggestedReviewers;
 
   @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT",
@@ -68,14 +66,6 @@
     return suggestFrom;
   }
 
-  public boolean getUseFullTextSearch() {
-    return useFullTextSearch;
-  }
-
-  public int getFullTextMaxMatches() {
-    return fullTextMaxMatches;
-  }
-
   public int getLimit() {
     return limit;
   }
@@ -84,6 +74,10 @@
     return maxAllowed;
   }
 
+  public int getMaxAllowedWithoutConfirmation() {
+    return maxAllowedWithoutConfirmation;
+  }
+
   @Inject
   public SuggestReviewers(AccountVisibility av,
       IdentifiedUser.GenericFactory identifiedUserFactory,
@@ -96,20 +90,19 @@
     this.maxSuggestedReviewers =
         cfg.getInt("suggest", "maxSuggestedReviewers", DEFAULT_MAX_SUGGESTED);
     this.limit = this.maxSuggestedReviewers;
-    this.fullTextMaxMatches =
-        cfg.getInt("suggest", "fullTextSearchMaxMatches",
-            DEFAULT_MAX_MATCHES);
     String suggest = cfg.getString("suggest", null, "accounts");
     if ("OFF".equalsIgnoreCase(suggest)
         || "false".equalsIgnoreCase(suggest)) {
       this.suggestAccounts = false;
     } else {
-      this.useFullTextSearch = cfg.getBoolean("suggest", "fullTextSearch", false);
       this.suggestAccounts = (av != AccountVisibility.NONE);
     }
 
     this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
     this.maxAllowed = cfg.getInt("addreviewer", "maxAllowed",
         PostReviewers.DEFAULT_MAX_REVIEWERS);
+    this.maxAllowedWithoutConfirmation = cfg.getInt(
+        "addreviewer", "maxWithoutConfirmation",
+        PostReviewers.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AdministrateServerGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AdministrateServerGroups.java
new file mode 100644
index 0000000..caeb771
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AdministrateServerGroups.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2010 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.config;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Groups that can always exercise {@code administrateServer} capability.
+ *
+ * <pre>
+ * [capability]
+ *     administrateServer = group Administrators
+ * </pre>
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface AdministrateServerGroups {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java
new file mode 100644
index 0000000..dd3b8329
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2010 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.config;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ServerRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Loads {@link AdministrateServerGroups} from {@code gerrit.config}. */
+public class AdministrateServerGroupsProvider implements Provider<ImmutableSet<GroupReference>> {
+  private final ImmutableSet<GroupReference> groups;
+
+  @Inject
+  public AdministrateServerGroupsProvider(GroupBackend groupBackend,
+      @GerritServerConfig Config config,
+      ThreadLocalRequestContext threadContext,
+      ServerRequestContext serverCtx) {
+    RequestContext ctx = threadContext.setContext(serverCtx);
+    try {
+      ImmutableSet.Builder<GroupReference> builder = ImmutableSet.builder();
+      for (String value : config.getStringList("capability", null, "administrateServer")) {
+        PermissionRule rule = PermissionRule.fromString(value, false);
+        String name = rule.getGroup().getName();
+        GroupReference g = GroupBackends.findBestSuggestion(groupBackend, name);
+        if (g != null) {
+          builder.add(g);
+        } else {
+          Logger log = LoggerFactory.getLogger(getClass());
+          log.warn("Group \"{}\" not available, skipping.", name);
+        }
+      }
+      groups = builder.build();
+    } finally {
+      threadContext.setContext(ctx);
+    }
+  }
+
+  @Override
+  public ImmutableSet<GroupReference> get() {
+    return groups;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AgreementJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AgreementJson.java
new file mode 100644
index 0000000..be5cdd4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AgreementJson.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2016 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.config;
+
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.AgreementInfo;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.GroupJson;
+import com.google.gerrit.server.group.GroupResource;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AgreementJson {
+  private static final Logger log =
+      LoggerFactory.getLogger(AgreementJson.class);
+
+  private final Provider<IdentifiedUser> self;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final GroupControl.GenericFactory genericGroupControlFactory;
+  private final GroupJson groupJson;
+
+  @Inject
+  AgreementJson(Provider<IdentifiedUser> self,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      GroupControl.GenericFactory genericGroupControlFactory,
+      GroupJson groupJson) {
+    this.self = self;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.genericGroupControlFactory = genericGroupControlFactory;
+    this.groupJson = groupJson;
+  }
+
+  public AgreementInfo format(ContributorAgreement ca) {
+    IdentifiedUser user =
+        identifiedUserFactory.create(self.get().getAccountId());
+    AgreementInfo info = new AgreementInfo();
+    info.name = ca.getName();
+    info.description = ca.getDescription();
+    info.url = ca.getAgreementUrl();
+    GroupReference autoVerifyGroup = ca.getAutoVerify();
+    if (autoVerifyGroup != null) {
+      try {
+        GroupControl gc = genericGroupControlFactory.controlFor(
+            user, autoVerifyGroup.getUUID());
+        GroupResource group = new GroupResource(gc);
+        info.autoVerifyGroup = groupJson.format(group);
+      } catch (NoSuchGroupException | OrmException e) {
+        log.warn("autoverify group \"" + autoVerifyGroup.getName() +
+            "\" does not exist, referenced in CLA \"" + ca.getName() + "\"");
+      }
+    }
+    return info;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index f2fc94e..5a40a31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.gwtjsonrpc.server.XsrfException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
index 8e181a9..5b0f73d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.DefaultRealm;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.auth.AuthBackend;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java
index 87d0777..81a3366 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java
@@ -78,6 +78,8 @@
       throw new UnprocessableEntityException("invalid token");
     } catch (EmailTokenVerifier.InvalidTokenException e) {
       throw new UnprocessableEntityException("invalid token");
+    } catch (AccountException e) {
+      throw new UnprocessableEntityException(e.getMessage());
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 6bbfd62..37127c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2016 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.
@@ -34,6 +34,7 @@
 import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.extensions.events.ChangeMergedListener;
 import com.google.gerrit.extensions.events.ChangeRestoredListener;
+import com.google.gerrit.extensions.events.ChangeRevertedListener;
 import com.google.gerrit.extensions.events.CommentAddedListener;
 import com.google.gerrit.extensions.events.DraftPublishedListener;
 import com.google.gerrit.extensions.events.GarbageCollectorListener;
@@ -49,6 +50,7 @@
 import com.google.gerrit.extensions.events.RevisionCreatedListener;
 import com.google.gerrit.extensions.events.TopicEditedListener;
 import com.google.gerrit.extensions.events.UsageDataPublishedListener;
+import com.google.gerrit.extensions.events.VoteDeletedListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -77,6 +79,7 @@
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.AccountVisibility;
 import com.google.gerrit.server.account.AccountVisibilityProvider;
+import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.ChangeUserName;
 import com.google.gerrit.server.account.EmailExpander;
@@ -131,9 +134,11 @@
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.FromAddressGenerator;
 import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
+import com.google.gerrit.server.mail.MailTemplates;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
+import com.google.gerrit.server.mail.MailSoyTofuProvider;
 import com.google.gerrit.server.mail.VelocityRuntimeProvider;
 import com.google.gerrit.server.mime.FileTypeRegistry;
 import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
@@ -167,10 +172,12 @@
 import com.google.inject.Inject;
 import com.google.inject.TypeLiteral;
 import com.google.inject.internal.UniqueAnnotations;
+import com.google.template.soy.tofu.SoyTofu;
 
 import org.apache.velocity.runtime.RuntimeInstance;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.transport.PostReceiveHook;
+import org.eclipse.jgit.transport.PostUploadHook;
 import org.eclipse.jgit.transport.PreUploadHook;
 
 import java.util.List;
@@ -230,6 +237,7 @@
     factory(DeleteReviewerSender.Factory.class);
     factory(AddKeySender.Factory.class);
     factory(BatchUpdate.Factory.class);
+    factory(CapabilityCollection.Factory.class);
     factory(CapabilityControl.Factory.class);
     factory(ChangeData.Factory.class);
     factory(ChangeJson.Factory.class);
@@ -269,8 +277,10 @@
     bind(ApprovalsUtil.class);
 
     bind(RuntimeInstance.class)
-        .toProvider(VelocityRuntimeProvider.class)
-        .in(SINGLETON);
+        .toProvider(VelocityRuntimeProvider.class);
+    bind(SoyTofu.class)
+        .annotatedWith(MailTemplates.class)
+        .toProvider(MailSoyTofuProvider.class);
     bind(FromAddressGenerator.class).toProvider(
         FromAddressGeneratorProvider.class).in(SINGLETON);
     bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class)
@@ -302,8 +312,10 @@
     DynamicSet.setOf(binder(), HashtagsEditedListener.class);
     DynamicSet.setOf(binder(), ChangeMergedListener.class);
     DynamicSet.setOf(binder(), ChangeRestoredListener.class);
+    DynamicSet.setOf(binder(), ChangeRevertedListener.class);
     DynamicSet.setOf(binder(), ReviewerAddedListener.class);
     DynamicSet.setOf(binder(), ReviewerDeletedListener.class);
+    DynamicSet.setOf(binder(), VoteDeletedListener.class);
     DynamicSet.setOf(binder(), RevisionCreatedListener.class);
     DynamicSet.setOf(binder(), TopicEditedListener.class);
     DynamicSet.setOf(binder(), AgreementSignupListener.class);
@@ -311,6 +323,7 @@
     DynamicSet.setOf(binder(), ReceivePackInitializer.class);
     DynamicSet.setOf(binder(), PostReceiveHook.class);
     DynamicSet.setOf(binder(), PreUploadHook.class);
+    DynamicSet.setOf(binder(), PostUploadHook.class);
     DynamicSet.setOf(binder(), ChangeIndexedListener.class);
     DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
     DynamicSet.setOf(binder(), ProjectDeletedListener.class);
@@ -337,7 +350,7 @@
     DynamicMap.mapOf(binder(), DownloadScheme.class);
     DynamicMap.mapOf(binder(), DownloadCommand.class);
     DynamicMap.mapOf(binder(), CloneCommand.class);
-    DynamicMap.mapOf(binder(), ExternalIncludedIn.class);
+    DynamicSet.setOf(binder(), ExternalIncludedIn.class);
     DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
     DynamicSet.setOf(binder(), PatchSetWebLink.class);
     DynamicSet.setOf(binder(), FileWebLink.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java
index 615b4ca..66e45b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java
@@ -14,29 +14,32 @@
 
 package com.google.gerrit.server.config;
 
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.GeneralPreferencesLoader;
 import com.google.gerrit.server.account.VersionedAccountPreferences;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UserConfigSections;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
 
 @Singleton
 public class GetPreferences implements RestReadView<ConfigResource> {
-  private GeneralPreferencesLoader loader;
+  private final GeneralPreferencesLoader loader;
   private final GitRepositoryManager gitMgr;
   private final AllUsersName allUsersName;
 
   @Inject
   public GetPreferences(GeneralPreferencesLoader loader,
-      GitRepositoryManager gitMgr,
-      AllUsersName allUsersName) {
+      GitRepositoryManager gitMgr, AllUsersName allUsersName) {
     this.loader = loader;
     this.gitMgr = gitMgr;
     this.allUsersName = allUsersName;
@@ -45,14 +48,23 @@
   @Override
   public GeneralPreferencesInfo apply(ConfigResource rsrc)
       throws IOException, ConfigInvalidException {
+    return readFromGit(gitMgr, loader, allUsersName, null);
+  }
+
+  static GeneralPreferencesInfo readFromGit(GitRepositoryManager gitMgr,
+      GeneralPreferencesLoader loader, AllUsersName allUsersName,
+      GeneralPreferencesInfo in) throws IOException, ConfigInvalidException,
+          RepositoryNotFoundException {
     try (Repository git = gitMgr.openRepository(allUsersName)) {
-      VersionedAccountPreferences p =
-          VersionedAccountPreferences.forDefault();
+      VersionedAccountPreferences p = VersionedAccountPreferences.forDefault();
       p.load(git);
 
-      GeneralPreferencesInfo a = new GeneralPreferencesInfo();
+      GeneralPreferencesInfo r = loadSection(p.getConfig(),
+          UserConfigSections.GENERAL, null, new GeneralPreferencesInfo(),
+          GeneralPreferencesInfo.defaults(), in);
+
       // TODO(davido): Maintain cache of default values in AllUsers repository
-      return loader.loadFromAllUsers(a, p, git);
+      return loader.loadMyMenusAndUrlAliases(r, p, null);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
index 2b621c2..9e2ad77 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -20,7 +20,18 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
-import com.google.gerrit.common.data.GitwebType;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.extensions.common.AuthInfo;
+import com.google.gerrit.extensions.common.ChangeConfigInfo;
+import com.google.gerrit.extensions.common.DownloadInfo;
+import com.google.gerrit.extensions.common.DownloadSchemeInfo;
+import com.google.gerrit.extensions.common.GerritInfo;
+import com.google.gerrit.extensions.common.PluginConfigInfo;
+import com.google.gerrit.extensions.common.ReceiveInfo;
+import com.google.gerrit.extensions.common.ServerInfo;
+import com.google.gerrit.extensions.common.SshdInfo;
+import com.google.gerrit.extensions.common.SuggestInfo;
+import com.google.gerrit.extensions.common.UserConfigInfo;
 import com.google.gerrit.extensions.config.CloneCommand;
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
@@ -29,8 +40,6 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.EnableSignedPush;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.avatar.AvatarProvider;
@@ -39,14 +48,15 @@
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.documentation.QueryDocumentationExecutor;
 import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
 
 import java.net.MalformedURLException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
@@ -70,6 +80,8 @@
   private final boolean enableSignedPush;
   private final QueryDocumentationExecutor docSearcher;
   private final NotesMigration migration;
+  private final ProjectCache projectCache;
+  private final AgreementJson agreementJson;
 
   @Inject
   public GetServerInfo(
@@ -87,7 +99,9 @@
       DynamicItem<AvatarProvider> avatar,
       @EnableSignedPush boolean enableSignedPush,
       QueryDocumentationExecutor docSearcher,
-      NotesMigration migration) {
+      NotesMigration migration,
+      ProjectCache projectCache,
+      AgreementJson agreementJson) {
     this.config = config;
     this.authConfig = authConfig;
     this.realm = realm;
@@ -103,6 +117,8 @@
     this.enableSignedPush = enableSignedPush;
     this.docSearcher = docSearcher;
     this.migration = migration;
+    this.projectCache = projectCache;
+    this.agreementJson = agreementJson;
   }
 
   @Override
@@ -135,6 +151,18 @@
     info.switchAccountUrl = cfg.getSwitchAccountUrl();
     info.isGitBasicAuth = toBoolean(cfg.isGitBasicAuth());
 
+    if (info.useContributorAgreements != null) {
+      Collection<ContributorAgreement> agreements =
+          projectCache.getAllProjects().getConfig().getContributorAgreements();
+      if (!agreements.isEmpty()) {
+        info.contributorAgreements =
+            Lists.newArrayListWithCapacity(agreements.size());
+        for (ContributorAgreement agreement: agreements) {
+          info.contributorAgreements.add(agreementJson.format(agreement));
+        }
+      }
+    }
+
     switch (info.authType) {
       case LDAP:
       case LDAP_BIND:
@@ -323,90 +351,4 @@
   private static Boolean toBoolean(boolean v) {
     return v ? v : null;
   }
-
-  public static class ServerInfo {
-    public AuthInfo auth;
-    public ChangeConfigInfo change;
-    public DownloadInfo download;
-    public GerritInfo gerrit;
-    public Boolean noteDbEnabled;
-    public PluginConfigInfo plugin;
-    public SshdInfo sshd;
-    public SuggestInfo suggest;
-    public Map<String, String> urlAliases;
-    public UserConfigInfo user;
-    public ReceiveInfo receive;
-  }
-
-  public static class AuthInfo {
-    public AuthType authType;
-    public Boolean useContributorAgreements;
-    public List<Account.FieldName> editableAccountFields;
-    public String loginUrl;
-    public String loginText;
-    public String switchAccountUrl;
-    public String registerUrl;
-    public String registerText;
-    public String editFullNameUrl;
-    public String httpPasswordUrl;
-    public Boolean isGitBasicAuth;
-  }
-
-  public static class ChangeConfigInfo {
-    public Boolean allowBlame;
-    public Boolean allowDrafts;
-    public int largeChange;
-    public String replyLabel;
-    public String replyTooltip;
-    public int updateDelay;
-    public Boolean submitWholeTopic;
-  }
-
-  public static class DownloadInfo {
-    public Map<String, DownloadSchemeInfo> schemes;
-    public List<String> archives;
-  }
-
-  public static class DownloadSchemeInfo {
-    public String url;
-    public Boolean isAuthRequired;
-    public Boolean isAuthSupported;
-    public Map<String, String> commands;
-    public Map<String, String> cloneCommands;
-  }
-
-  public static class GerritInfo {
-    public String allProjects;
-    public String allUsers;
-    public Boolean docSearch;
-    public String docUrl;
-    public Boolean editGpgKeys;
-    public String reportBugUrl;
-    public String reportBugText;
-  }
-
-  public static class GitwebInfo {
-    public String url;
-    public GitwebType type;
-  }
-
-  public static class PluginConfigInfo {
-    public Boolean hasAvatars;
-    public List<String> jsResourcePaths;
-  }
-
-  public static class SshdInfo {
-  }
-
-  public static class SuggestInfo {
-    public int from;
-  }
-
-  public static class UserConfigInfo {
-    public String anonymousCowardName;
-  }
-
-  public static class ReceiveInfo {
-    public Boolean enableSignedPush;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GlobalPluginConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GlobalPluginConfig.java
new file mode 100644
index 0000000..2a2c316
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GlobalPluginConfig.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2016 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.config;
+
+import com.google.gerrit.server.securestore.SecureStore;
+
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Plugin configuration in etc/$PLUGIN.config and etc/$PLUGIN.secure.config.
+ */
+public class GlobalPluginConfig extends Config {
+  private final SecureStore secureStore;
+  private final String pluginName;
+
+  GlobalPluginConfig(String pluginName, Config baseConfig,
+      SecureStore secureStore) {
+    super(baseConfig);
+    this.pluginName = pluginName;
+    this.secureStore = secureStore;
+  }
+
+  @Override
+  public String getString(String section, String subsection, String name) {
+    String secure = secureStore.getForPlugin(
+        pluginName, section, subsection, name);
+    if (secure != null) {
+      return secure;
+    }
+    return super.getString(section, subsection, name);
+  }
+
+  @Override
+  public String[] getStringList(String section, String subsection, String name) {
+    String[] secure = secureStore.getListForPlugin(
+        pluginName, section, subsection, name);
+    if (secure != null && secure.length > 0) {
+      return secure;
+    }
+    return super.getStringList(section, subsection, name);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
index 0307b7c..78af1ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ServerRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import org.slf4j.Logger;
@@ -31,14 +30,12 @@
 import java.util.List;
 import java.util.Set;
 
+/** Parses groups referenced in the {@code gerrit.config} file. */
 public abstract class GroupSetProvider implements
     Provider<Set<AccountGroup.UUID>> {
-  private static final Logger log =
-      LoggerFactory.getLogger(GroupSetProvider.class);
 
   protected Set<AccountGroup.UUID> groupIds;
 
-  @Inject
   protected GroupSetProvider(GroupBackend groupBackend,
       ThreadLocalRequestContext threadContext,
       ServerRequestContext serverCtx, List<String> groupNames) {
@@ -47,10 +44,11 @@
       ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
       for (String n : groupNames) {
         GroupReference g = GroupBackends.findBestSuggestion(groupBackend, n);
-        if (g == null) {
-          log.warn("Group \"{}\" not in database, skipping.", n);
-        } else {
+        if (g != null) {
           builder.add(g.getUUID());
+        } else {
+          Logger log = LoggerFactory.getLogger(getClass());
+          log.warn("Group \"{}\" not available, skipping.", n);
         }
       }
       groupIds = builder.build();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java
index 98bcbbc..b96d5d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java
@@ -113,6 +113,7 @@
     public String command;
     public String remoteName;
     public String projectName;
+    public String queueName;
 
     public TaskInfo(Task<?> task) {
       this.id = IdGenerator.format(task.getTaskId());
@@ -120,6 +121,7 @@
       this.startTime = new Timestamp(task.getStartTime().getTime());
       this.delay = task.getDelay(TimeUnit.MILLISECONDS);
       this.command = task.toString();
+      this.queueName = task.getQueueName();
 
       if (task instanceof ProjectTask) {
         ProjectTask<?> projectTask = ((ProjectTask<?>) task);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
index 95a0f36..eb6169e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.securestore.SecureStore;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -49,6 +50,7 @@
   private final Provider<Config> cfgProvider;
   private final ProjectCache projectCache;
   private final ProjectState.Factory projectStateFactory;
+  private final SecureStore secureStore;
   private final Map<String, Config> pluginConfigs;
 
   private volatile FileSnapshot cfgSnapshot;
@@ -59,13 +61,15 @@
       SitePaths site,
       @GerritServerConfig Provider<Config> cfgProvider,
       ProjectCache projectCache,
-      ProjectState.Factory projectStateFactory) {
+      ProjectState.Factory projectStateFactory,
+      SecureStore secureStore) {
     this.site = site;
     this.cfgProvider = cfgProvider;
     this.projectCache = projectCache;
     this.projectStateFactory = projectStateFactory;
-    this.pluginConfigs = new HashMap<>();
+    this.secureStore = secureStore;
 
+    this.pluginConfigs = new HashMap<>();
     this.cfgSnapshot = FileSnapshot.save(site.gerrit_config.toFile());
     this.cfg = cfgProvider.get();
   }
@@ -260,10 +264,12 @@
     Path pluginConfigFile = site.etc_dir.resolve(pluginName + ".config");
     FileBasedConfig cfg =
         new FileBasedConfig(pluginConfigFile.toFile(), FS.DETECTED);
-    pluginConfigs.put(pluginName, cfg);
+    GlobalPluginConfig pluginConfig =
+        new GlobalPluginConfig(pluginName, cfg, secureStore);
+    pluginConfigs.put(pluginName, pluginConfig);
     if (!cfg.getFile().exists()) {
       log.info("No " + pluginConfigFile.toAbsolutePath() + "; assuming defaults");
-      return cfg;
+      return pluginConfig;
     }
 
     try {
@@ -272,7 +278,7 @@
       log.warn("Failed to load " + pluginConfigFile.toAbsolutePath(), e);
     }
 
-    return cfg;
+    return pluginConfig;
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index cbad98c..cc7857c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap.Entry;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
@@ -33,6 +33,7 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -284,13 +285,13 @@
   public static class UpdateChecker implements GitReferenceUpdatedListener {
     private static final Logger log = LoggerFactory.getLogger(UpdateChecker.class);
 
-    private final MetaDataUpdate.Server metaDataUpdateFactory;
+    private final GitRepositoryManager repoManager;
     private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
 
     @Inject
-    UpdateChecker(MetaDataUpdate.Server metaDataUpdateFactory,
+    UpdateChecker(GitRepositoryManager repoManager,
         DynamicMap<ProjectConfigEntry> pluginConfigEntries) {
-      this.metaDataUpdateFactory = metaDataUpdateFactory;
+      this.repoManager = repoManager;
       this.pluginConfigEntries = pluginConfigEntries;
     }
 
@@ -345,7 +346,11 @@
       if (ObjectId.zeroId().equals(id)) {
         return null;
       }
-      return ProjectConfig.read(metaDataUpdateFactory.create(p), id);
+      try (Repository repo = repoManager.openRepository(p)) {
+        ProjectConfig pc = new ProjectConfig(p);
+        pc.load(repo, id);
+        return pc;
+      }
     }
 
     private static String getValue(ProjectConfig cfg, Entry<ProjectConfigEntry> e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
index bbf2299..c5c75ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
@@ -14,67 +14,104 @@
 
 package com.google.gerrit.server.config;
 
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+import static com.google.gerrit.server.config.ConfigUtil.skipField;
+import static com.google.gerrit.server.config.ConfigUtil.storeSection;
+import static com.google.gerrit.server.config.GetPreferences.readFromGit;
+
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GeneralPreferencesLoader;
 import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.UserConfigSections;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.lang.reflect.Field;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @Singleton
 public class SetPreferences implements
     RestModifyView<ConfigResource, GeneralPreferencesInfo> {
+  private static final Logger log =
+      LoggerFactory.getLogger(SetPreferences.class);
+
   private final GeneralPreferencesLoader loader;
+  private final GitRepositoryManager gitManager;
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final AllUsersName allUsersName;
+  private final AccountCache accountCache;
 
   @Inject
   SetPreferences(GeneralPreferencesLoader loader,
+      GitRepositoryManager gitManager,
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
-      AllUsersName allUsersName) {
+      AllUsersName allUsersName,
+      AccountCache accountCache) {
     this.loader = loader;
+    this.gitManager = gitManager;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.allUsersName = allUsersName;
+    this.accountCache = accountCache;
   }
 
   @Override
   public GeneralPreferencesInfo apply(ConfigResource rsrc,
       GeneralPreferencesInfo i)
           throws BadRequestException, IOException, ConfigInvalidException {
-    if (i.changesPerPage != null || i.showSiteHeader != null
-        || i.useFlashClipboard != null || i.downloadScheme != null
-        || i.downloadCommand != null
-        || i.dateFormat != null || i.timeFormat != null
-        || i.relativeDateInChangeTable != null
-        || i.sizeBarInChangeTable != null
-        || i.legacycidInChangeTable != null
-        || i.muteCommonPathPrefixes != null
-        || i.reviewCategoryStrategy != null
-        || i.signedOffBy != null
-        || i.urlAliases != null
-        || i.emailStrategy != null) {
+    if (!hasSetFields(i)) {
       throw new BadRequestException("unsupported option");
     }
+    return writeToGit(readFromGit(gitManager, loader, allUsersName, i));
+  }
 
-    VersionedAccountPreferences p;
+  private GeneralPreferencesInfo writeToGit(GeneralPreferencesInfo i)
+      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
-      p = VersionedAccountPreferences.forDefault();
+      VersionedAccountPreferences p = VersionedAccountPreferences.forDefault();
       p.load(md);
+      storeSection(p.getConfig(), UserConfigSections.GENERAL, null, i,
+          GeneralPreferencesInfo.defaults());
       com.google.gerrit.server.account.SetPreferences.storeMyMenus(p, i.my);
+      com.google.gerrit.server.account.SetPreferences.storeUrlAliases(p, i.urlAliases);
       p.commit(md);
 
-      GeneralPreferencesInfo a = new GeneralPreferencesInfo();
-      return loader.loadFromAllUsers(a, p, md.getRepository());
+      accountCache.evictAll();
+
+      GeneralPreferencesInfo r = loadSection(p.getConfig(),
+          UserConfigSections.GENERAL, null, new GeneralPreferencesInfo(),
+          GeneralPreferencesInfo.defaults(), null);
+      return loader.loadMyMenusAndUrlAliases(r, p, null);
     }
   }
+
+
+  private static boolean hasSetFields(GeneralPreferencesInfo in) {
+    try {
+      for (Field field : in.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        if (field.get(in) != null) {
+          return true;
+        }
+      }
+    } catch (IllegalAccessException e) {
+      log.warn("Unable to verify input", e);
+    }
+    return false;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/VerboseSuperprojectUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/VerboseSuperprojectUpdate.java
new file mode 100644
index 0000000..f328b1ff
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/VerboseSuperprojectUpdate.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2016 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.config;
+
+/**
+ * Verbosity level of the commit message for submodule subscriptions.
+ */
+public enum VerboseSuperprojectUpdate {
+  /** Do not include any commit messages for the gitlink update. */
+  FALSE,
+
+  /** Only include the commit subjects. */
+  SUBJECT_ONLY,
+
+  /** Include full commit messages. */
+  TRUE
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index bc9cb82..45128bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -35,7 +35,9 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -96,18 +98,21 @@
   private final ChangeIndexer indexer;
   private final Provider<ReviewDb> reviewDb;
   private final Provider<CurrentUser> currentUser;
+  private final ChangeControl.GenericFactory changeControlFactory;
 
   @Inject
   ChangeEditModifier(@GerritPersonIdent PersonIdent gerritIdent,
       GitRepositoryManager gitManager,
       ChangeIndexer indexer,
       Provider<ReviewDb> reviewDb,
-      Provider<CurrentUser> currentUser) {
+      Provider<CurrentUser> currentUser,
+      ChangeControl.GenericFactory changeControlFactory) {
     this.gitManager = gitManager;
     this.indexer = indexer;
     this.reviewDb = reviewDb;
     this.currentUser = currentUser;
     this.tz = gerritIdent.getTimeZone();
+    this.changeControlFactory = changeControlFactory;
   }
 
   /**
@@ -127,10 +132,19 @@
     if (!currentUser.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-
     IdentifiedUser me = currentUser.get().asIdentifiedUser();
     String refPrefix = RefNames.refsEditPrefix(me.getAccountId(), change.getId());
 
+    try {
+      ChangeControl c =
+          changeControlFactory.controlFor(reviewDb.get(), change, me);
+      if (!c.canAddPatchSet(reviewDb.get())) {
+        return RefUpdate.Result.REJECTED;
+      }
+    } catch (NoSuchChangeException e) {
+      return RefUpdate.Result.NO_CHANGE;
+    }
+
     try (Repository repo = gitManager.openRepository(change.getProject())) {
       Map<String, Ref> refs = repo.getRefDatabase().getRefs(refPrefix);
       if (!refs.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 6811056..297a2cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.index.change.ChangeIndexer;
@@ -168,24 +169,67 @@
    * @throws UpdateException
    * @throws RestApiException
    */
-  public void publish(ChangeEdit edit) throws NoSuchChangeException,
+  public void publish(final ChangeEdit edit) throws NoSuchChangeException,
       IOException, OrmException, RestApiException, UpdateException {
     Change change = edit.getChange();
     try (Repository repo = gitManager.openRepository(change.getProject());
         RevWalk rw = new RevWalk(repo);
-        ObjectInserter inserter = repo.newObjectInserter()) {
+        ObjectInserter oi = repo.newObjectInserter()) {
       PatchSet basePatchSet = edit.getBasePatchSet();
       if (!basePatchSet.getId().equals(change.currentPatchSetId())) {
         throw new ResourceConflictException(
             "only edit for current patch set can be published");
       }
 
-      Change updatedChange =
-          insertPatchSet(edit, change, repo, rw, inserter, basePatchSet,
-              squashEdit(rw, inserter, edit.getEditCommit(), basePatchSet));
-      // TODO(davido): This should happen in the same BatchRefUpdate.
-      deleteRef(repo, edit);
-      indexer.index(db.get(), updatedChange);
+      RevCommit squashed = squashEdit(rw, oi, edit.getEditCommit(), basePatchSet);
+      ChangeControl ctl =
+          changeControlFactory.controlFor(db.get(), change, edit.getUser());
+      PatchSet.Id psId =
+          ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
+      PatchSetInserter inserter =
+          patchSetInserterFactory.create(ctl, psId, squashed);
+
+      StringBuilder message = new StringBuilder("Patch Set ")
+        .append(inserter.getPatchSetId().get())
+        .append(": ");
+
+      ProjectState project = projectCache.get(change.getDest().getParentKey());
+      // Previously checked that the base patch set is the current patch set.
+      ObjectId prior = ObjectId.fromString(basePatchSet.getRevision().get());
+      ChangeKind kind = changeKindCache.getChangeKind(project, repo, prior, squashed);
+      if (kind == ChangeKind.NO_CODE_CHANGE) {
+        message.append("Commit message was updated.");
+      } else {
+        message.append("Published edit on patch set ")
+          .append(basePatchSet.getPatchSetId())
+          .append(".");
+      }
+
+      try (BatchUpdate bu = updateFactory.create(
+          db.get(), change.getProject(), ctl.getUser(),
+          TimeUtil.nowTs())) {
+        bu.setRepository(repo, rw, oi);
+        bu.addOp(change.getId(), inserter
+          .setDraft(change.getStatus() == Status.DRAFT ||
+              basePatchSet.isDraft())
+          .setMessage(message.toString()));
+        bu.addOp(change.getId(), new BatchUpdate.Op() {
+          @Override
+          public void updateRepo(RepoContext ctx) throws Exception {
+            deleteRef(ctx.getRepository(), edit);
+          }
+        });
+        bu.execute();
+      } catch (UpdateException e) {
+        if (e.getCause() instanceof IOException && e.getMessage()
+            .equals(String.format("%s: Failed to delete ref %s: %s",
+                IOException.class.getName(), edit.getRefName(),
+                RefUpdate.Result.LOCK_FAILURE.name()))) {
+          throw new ResourceConflictException("edit ref was updated");
+        }
+      }
+
+      indexer.index(db.get(), inserter.getChange());
     }
   }
 
@@ -230,47 +274,6 @@
     return writeSquashedCommit(rw, inserter, parent, edit);
   }
 
-  private Change insertPatchSet(ChangeEdit edit, Change change,
-      Repository repo, RevWalk rw, ObjectInserter oi, PatchSet basePatchSet,
-      RevCommit squashed) throws NoSuchChangeException, RestApiException,
-      UpdateException, OrmException, IOException {
-    ChangeControl ctl =
-        changeControlFactory.controlFor(db.get(), change, edit.getUser());
-    PatchSet.Id psId =
-        ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
-    PatchSetInserter inserter =
-        patchSetInserterFactory.create(ctl, psId, squashed);
-
-    StringBuilder message = new StringBuilder("Patch Set ")
-      .append(inserter.getPatchSetId().get())
-      .append(": ");
-
-    ProjectState project = projectCache.get(change.getDest().getParentKey());
-    // Previously checked that the base patch set is the current patch set.
-    ObjectId prior = ObjectId.fromString(basePatchSet.getRevision().get());
-    ChangeKind kind = changeKindCache.getChangeKind(project, repo, prior, squashed);
-    if (kind == ChangeKind.NO_CODE_CHANGE) {
-      message.append("Commit message was updated.");
-    } else {
-      message.append("Published edit on patch set ")
-        .append(basePatchSet.getPatchSetId())
-        .append(".");
-    }
-
-    try (BatchUpdate bu = updateFactory.create(
-        db.get(), change.getProject(), ctl.getUser(),
-        TimeUtil.nowTs())) {
-      bu.setRepository(repo, rw, oi);
-      bu.addOp(change.getId(), inserter
-        .setDraft(change.getStatus() == Status.DRAFT ||
-            basePatchSet.isDraft())
-        .setMessage(message.toString()));
-      bu.execute();
-    }
-
-    return inserter.getChange();
-  }
-
   private static void deleteRef(Repository repo, ChangeEdit edit)
       throws IOException {
     String refName = edit.getRefName();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
index 9f806be..6029ded 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
@@ -22,20 +22,20 @@
 
 public abstract class ChangeEvent extends RefEvent {
   public Supplier<ChangeAttribute> change;
-  public Project.NameKey projectNameKey;
+  public Project.NameKey project;
   public String refName;
   public Change.Key changeKey;
 
   protected ChangeEvent(String type, Change change) {
     super(type);
-    this.projectNameKey = change.getProject();
+    this.project = change.getProject();
     this.refName = RefNames.fullName(change.getDest().get());
     this.changeKey = change.getKey();
   }
 
   @Override
   public Project.NameKey getProjectNameKey() {
-    return projectNameKey;
+    return project;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ProjectNameKeySerializer.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ProjectNameKeySerializer.java
new file mode 100644
index 0000000..e2f51ac
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ProjectNameKeySerializer.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2016 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.events;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+import java.lang.reflect.Type;
+
+public class ProjectNameKeySerializer
+    implements JsonSerializer<Project.NameKey> {
+  @Override
+  public JsonElement serialize(Project.NameKey project, Type typeOfSrc,
+      JsonSerializationContext context) {
+    return new JsonPrimitive(project.get());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/StreamEventsApiListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
similarity index 91%
rename from gerrit-server/src/main/java/com/google/gerrit/common/StreamEventsApiListener.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 788147f..8d093f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/StreamEventsApiListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -12,11 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common;
+package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.EventDispatcher;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -49,19 +50,6 @@
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
 import com.google.gerrit.server.data.RefUpdateAttribute;
-import com.google.gerrit.server.events.ChangeAbandonedEvent;
-import com.google.gerrit.server.events.ChangeMergedEvent;
-import com.google.gerrit.server.events.ChangeRestoredEvent;
-import com.google.gerrit.server.events.CommentAddedEvent;
-import com.google.gerrit.server.events.DraftPublishedEvent;
-import com.google.gerrit.server.events.EventFactory;
-import com.google.gerrit.server.events.HashtagsChangedEvent;
-import com.google.gerrit.server.events.PatchSetCreatedEvent;
-import com.google.gerrit.server.events.ProjectCreatedEvent;
-import com.google.gerrit.server.events.RefUpdatedEvent;
-import com.google.gerrit.server.events.ReviewerAddedEvent;
-import com.google.gerrit.server.events.ReviewerDeletedEvent;
-import com.google.gerrit.server.events.TopicChangedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -284,7 +272,7 @@
       TopicChangedEvent event = new TopicChangedEvent(change);
 
       event.change = changeAttributeSupplier(change);
-      event.changer = accountAttributeSupplier(ev.getEditor());
+      event.changer = accountAttributeSupplier(ev.getWho());
       event.oldTopic = ev.getOldTopic();
 
       dispatcher.get().postEvent(change, event);
@@ -303,7 +291,7 @@
 
       event.change = changeAttributeSupplier(change);
       event.patchSet = patchSetAttributeSupplier(change, patchSet);
-      event.uploader = accountAttributeSupplier(ev.getUploader());
+      event.uploader = accountAttributeSupplier(ev.getWho());
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException e) {
@@ -333,7 +321,7 @@
   }
 
   @Override
-  public void onReviewerAdded(ReviewerAddedListener.Event ev) {
+  public void onReviewersAdded(ReviewerAddedListener.Event ev) {
     try {
       ChangeNotes notes = getNotes(ev.getChange());
       Change change = notes.getChange();
@@ -342,9 +330,10 @@
       event.change = changeAttributeSupplier(change);
       event.patchSet = patchSetAttributeSupplier(change,
           psUtil.current(db.get(), notes));
-      event.reviewer = accountAttributeSupplier(ev.getReviewer());
-
-      dispatcher.get().postEvent(change, event);
+      for (AccountInfo reviewer : ev.getReviewers()) {
+        event.reviewer = accountAttributeSupplier(reviewer);
+        dispatcher.get().postEvent(change, event);
+      }
     } catch (OrmException e) {
       log.error("Failed to dispatch event", e);
     }
@@ -366,7 +355,7 @@
       HashtagsChangedEvent event = new HashtagsChangedEvent(change);
 
       event.change = changeAttributeSupplier(change);
-      event.editor = accountAttributeSupplier(ev.getEditor());
+      event.editor = accountAttributeSupplier(ev.getWho());
       event.hashtags = hashtagArray(ev.getHashtags());
       event.added = hashtagArray(ev.getAddedHashtags());
       event.removed = hashtagArray(ev.getRemovedHashtags());
@@ -408,7 +397,7 @@
 
       event.change = changeAttributeSupplier(change);
       event.patchSet = patchSetAttributeSupplier(change, ps);
-      event.uploader = accountAttributeSupplier(ev.getPublisher());
+      event.uploader = accountAttributeSupplier(ev.getWho());
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException e) {
@@ -425,7 +414,7 @@
       CommentAddedEvent event = new CommentAddedEvent(change);
 
       event.change = changeAttributeSupplier(change);
-      event.author =  accountAttributeSupplier(ev.getAuthor());
+      event.author =  accountAttributeSupplier(ev.getWho());
       event.patchSet = patchSetAttributeSupplier(change, ps);
       event.comment = ev.getComment();
       event.approvals = approvalsAttributeSupplier(
@@ -445,7 +434,7 @@
       ChangeRestoredEvent event = new ChangeRestoredEvent(change);
 
       event.change = changeAttributeSupplier(change);
-      event.restorer = accountAttributeSupplier(ev.getRestorer());
+      event.restorer = accountAttributeSupplier(ev.getWho());
       event.patchSet = patchSetAttributeSupplier(change,
           psUtil.current(db.get(), notes));
       event.reason = ev.getReason();
@@ -464,7 +453,7 @@
       ChangeMergedEvent event = new ChangeMergedEvent(change);
 
       event.change = changeAttributeSupplier(change);
-      event.submitter = accountAttributeSupplier(ev.getMerger());
+      event.submitter = accountAttributeSupplier(ev.getWho());
       event.patchSet = patchSetAttributeSupplier(change,
           psUtil.current(db.get(), notes));
       event.newRev = ev.getNewRevisionId();
@@ -483,7 +472,7 @@
       ChangeAbandonedEvent event = new ChangeAbandonedEvent(change);
 
       event.change = changeAttributeSupplier(change);
-      event.abandoner = accountAttributeSupplier(ev.getAbandoner());
+      event.abandoner = accountAttributeSupplier(ev.getWho());
       event.patchSet = patchSetAttributeSupplier(change,
           psUtil.current(db.get(), notes));
       event.reason = ev.getReason();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java
index d25046f..be6f692 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java
@@ -14,18 +14,44 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.events.ChangeEvent;
 
+import java.sql.Timestamp;
+
 public abstract class AbstractChangeEvent implements ChangeEvent {
   private final ChangeInfo changeInfo;
+  private final AccountInfo who;
+  private final Timestamp when;
+  private final NotifyHandling notify;
 
-  protected AbstractChangeEvent(ChangeInfo change) {
+  protected AbstractChangeEvent(ChangeInfo change, AccountInfo who,
+      Timestamp when, NotifyHandling notify) {
     this.changeInfo = change;
+    this.who = who;
+    this.when = when;
+    this.notify = notify;
   }
 
   @Override
   public ChangeInfo getChange() {
     return changeInfo;
   }
+
+  @Override
+  public AccountInfo getWho() {
+    return who;
+  }
+
+  @Override
+  public Timestamp getWhen() {
+    return when;
+  }
+
+  @Override
+  public NotifyHandling getNotify() {
+    return notify;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AbstractNoNotifyEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AbstractNoNotifyEvent.java
new file mode 100644
index 0000000..b48d532
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AbstractNoNotifyEvent.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2016 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.extensions.events;
+
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.events.GerritEvent;
+
+/** Intermediate class for events that do not support notification type. */
+public abstract class AbstractNoNotifyEvent implements GerritEvent {
+  @Override
+  public NotifyHandling getNotify() {
+    return NotifyHandling.NONE;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AbstractRevisionEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AbstractRevisionEvent.java
index 35ce9b4a..d3d7e09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AbstractRevisionEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AbstractRevisionEvent.java
@@ -14,17 +14,22 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.RevisionEvent;
 
+import java.sql.Timestamp;
+
 public abstract class AbstractRevisionEvent extends AbstractChangeEvent
     implements RevisionEvent {
 
   private final RevisionInfo revisionInfo;
 
-  protected AbstractRevisionEvent(ChangeInfo change, RevisionInfo revision) {
-    super(change);
+  protected AbstractRevisionEvent(ChangeInfo change, RevisionInfo revision,
+      AccountInfo who, Timestamp when, NotifyHandling notify) {
+    super(change, who, when, notify);
     revisionInfo = revision;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AgreementSignup.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AgreementSignup.java
index 3bc9e88..018d408 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AgreementSignup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AgreementSignup.java
@@ -51,7 +51,8 @@
     }
   }
 
-  private static class Event implements AgreementSignupListener.Event {
+  private static class Event extends AbstractNoNotifyEvent
+      implements AgreementSignupListener.Event {
     private final AccountInfo account;
     private final String agreementName;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
index 6c23f60..dd49272 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
@@ -31,6 +32,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.sql.Timestamp;
 
 public class ChangeAbandoned {
   private static final Logger log =
@@ -47,11 +49,13 @@
   }
 
   public void fire(ChangeInfo change, RevisionInfo revision,
-      AccountInfo abandoner, String reason) {
+      AccountInfo abandoner, String reason, Timestamp when,
+      NotifyHandling notifyHandling) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
-    Event event = new Event(change, revision, abandoner, reason);
+    Event event = new Event(change, revision, abandoner, reason, when,
+        notifyHandling);
     for (ChangeAbandonedListener l : listeners) {
       try {
         l.onChangeAbandoned(event);
@@ -61,7 +65,8 @@
     }
   }
 
-  public void fire(Change change, PatchSet ps, Account abandoner, String reason) {
+  public void fire(Change change, PatchSet ps, Account abandoner, String reason,
+      Timestamp when, NotifyHandling notifyHandling) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
@@ -69,7 +74,7 @@
       fire(util.changeInfo(change),
           util.revisionInfo(change.getProject(), ps),
           util.accountInfo(abandoner),
-          reason);
+          reason, when, notifyHandling);
     } catch (PatchListNotAvailableException | GpgException | IOException
         | OrmException e) {
       log.error("Couldn't fire event", e);
@@ -82,8 +87,8 @@
     private final String reason;
 
     Event(ChangeInfo change, RevisionInfo revision, AccountInfo abandoner,
-        String reason) {
-      super(change, revision);
+        String reason, Timestamp when, NotifyHandling notifyHandling) {
+      super(change, revision, abandoner, when, notifyHandling);
       this.abandoner = abandoner;
       this.reason = reason;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
index 6a27275..94df1d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
@@ -31,6 +32,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.sql.Timestamp;
 
 public class ChangeMerged {
   private static final Logger log =
@@ -47,11 +49,11 @@
   }
 
   public void fire(ChangeInfo change, RevisionInfo revision,
-      AccountInfo merger, String newRevisionId) {
+      AccountInfo merger, String newRevisionId, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
-    Event event = new Event(change, revision, merger, newRevisionId);
+    Event event = new Event(change, revision, merger, newRevisionId, when);
     for (ChangeMergedListener l : listeners) {
       try {
         l.onChangeMerged(event);
@@ -62,7 +64,7 @@
   }
 
   public void fire(Change change, PatchSet ps, Account merger,
-      String newRevisionId) {
+      String newRevisionId, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
@@ -70,7 +72,7 @@
       fire(util.changeInfo(change),
           util.revisionInfo(change.getProject(), ps),
           util.accountInfo(merger),
-          newRevisionId);
+          newRevisionId, when);
     } catch (PatchListNotAvailableException | GpgException | IOException
         | OrmException e) {
       log.error("Couldn't fire event", e);
@@ -83,8 +85,8 @@
     private final String newRevisionId;
 
     Event(ChangeInfo change, RevisionInfo revision, AccountInfo merger,
-        String newRevisionId) {
-      super(change, revision);
+        String newRevisionId, Timestamp when) {
+      super(change, revision, merger, when, NotifyHandling.ALL);
       this.merger = merger;
       this.newRevisionId = newRevisionId;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
index 9981902..c853609 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
@@ -31,6 +32,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.sql.Timestamp;
 
 public class ChangeRestored {
   private static final Logger log =
@@ -47,11 +49,11 @@
   }
 
   public void fire(ChangeInfo change, RevisionInfo revision,
-      AccountInfo restorer, String reason) {
+      AccountInfo restorer, String reason, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
-    Event event = new Event(change, revision, restorer, reason);
+    Event event = new Event(change, revision, restorer, reason, when);
     for (ChangeRestoredListener l : listeners) {
       try {
         l.onChangeRestored(event);
@@ -61,7 +63,8 @@
     }
   }
 
-  public void fire(Change change, PatchSet ps, Account restorer, String reason) {
+  public void fire(Change change, PatchSet ps, Account restorer, String reason,
+      Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
@@ -69,7 +72,7 @@
       fire(util.changeInfo(change),
           util.revisionInfo(change.getProject(), ps),
           util.accountInfo(restorer),
-          reason);
+          reason, when);
     } catch (PatchListNotAvailableException | GpgException | IOException
         | OrmException e) {
       log.error("Couldn't fire event", e);
@@ -83,8 +86,8 @@
     private String reason;
 
     Event(ChangeInfo change, RevisionInfo revision, AccountInfo restorer,
-        String reason) {
-      super(change, revision);
+        String reason, Timestamp when) {
+      super(change, revision, restorer, when, NotifyHandling.ALL);
       this.restorer = restorer;
       this.reason = reason;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeReverted.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
new file mode 100644
index 0000000..f95236d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2016 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.extensions.events;
+
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.events.ChangeRevertedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Timestamp;
+
+public class ChangeReverted {
+  private static final Logger log =
+      LoggerFactory.getLogger(ChangeReverted.class);
+
+  private final DynamicSet<ChangeRevertedListener> listeners;
+  private final EventUtil util;
+
+  @Inject
+  ChangeReverted(DynamicSet<ChangeRevertedListener> listeners,
+      EventUtil util) {
+    this.listeners = listeners;
+    this.util = util;
+  }
+
+  public void fire(Change change, Change revertChange, Timestamp when) {
+    if (!listeners.iterator().hasNext()) {
+      return;
+    }
+    try {
+      fire(util.changeInfo(change), util.changeInfo(revertChange), when);
+    } catch (OrmException e) {
+      log.error("Couldn't fire event", e);
+    }
+  }
+
+  public void fire (ChangeInfo change, ChangeInfo revertChange,
+      Timestamp when) {
+    if (!listeners.iterator().hasNext()) {
+      return;
+    }
+    Event event = new Event(change, revertChange, when);
+    for (ChangeRevertedListener l : listeners) {
+      try {
+        l.onChangeReverted(event);
+      } catch (Exception e) {
+        log.warn("Error in event listener", e);
+      }
+    }
+  }
+
+  private static class Event extends AbstractChangeEvent
+      implements ChangeRevertedListener.Event {
+    private final ChangeInfo revertChange;
+
+    Event(ChangeInfo change, ChangeInfo revertChange, Timestamp when) {
+      super(change, revertChange.owner, when, NotifyHandling.ALL);
+      this.revertChange = revertChange;
+    }
+
+    @Override
+    public ChangeInfo getRevertChange() {
+      return revertChange;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
index 15f82b3..85ad8b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -51,12 +52,12 @@
 
   public void fire(ChangeInfo change, RevisionInfo revision, AccountInfo author,
       String comment, Map<String, ApprovalInfo> approvals,
-      Map<String, ApprovalInfo> oldApprovals) {
+      Map<String, ApprovalInfo> oldApprovals, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
     Event event = new Event(
-        change, revision, author, comment, approvals, oldApprovals);
+        change, revision, author, comment, approvals, oldApprovals, when);
     for (CommentAddedListener l : listeners) {
       try {
         l.onCommentAdded(event);
@@ -68,7 +69,7 @@
 
   public void fire(Change change, PatchSet ps, Account author,
       String comment, Map<String, Short> approvals,
-      Map<String, Short> oldApprovals, Timestamp ts) {
+      Map<String, Short> oldApprovals, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
@@ -77,8 +78,9 @@
           util.revisionInfo(change.getProject(), ps),
           util.accountInfo(author),
           comment,
-          util.approvals(author, approvals, ts),
-          util.approvals(author, oldApprovals, ts));
+          util.approvals(author, approvals, when),
+          util.approvals(author, oldApprovals, when),
+          when);
     } catch (PatchListNotAvailableException | GpgException | IOException
         | OrmException e) {
       log.error("Couldn't fire event", e);
@@ -95,8 +97,8 @@
 
     Event(ChangeInfo change, RevisionInfo revision, AccountInfo author,
         String comment, Map<String, ApprovalInfo> approvals,
-        Map<String, ApprovalInfo> oldApprovals) {
-      super(change, revision);
+        Map<String, ApprovalInfo> oldApprovals, Timestamp when) {
+      super(change, revision, author, when, NotifyHandling.ALL);
       this.author = author;
       this.comment = comment;
       this.approvals = approvals;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
index 433f717..0895cb8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
@@ -31,6 +32,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.sql.Timestamp;
 
 public class DraftPublished {
   private static final Logger log =
@@ -47,11 +49,11 @@
   }
 
   public void fire(ChangeInfo change, RevisionInfo revision,
-      AccountInfo publisher) {
+      AccountInfo publisher, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
-    Event event = new Event(change, revision, publisher);
+    Event event = new Event(change, revision, publisher, when);
     for (DraftPublishedListener l : listeners) {
       try {
         l.onDraftPublished(event);
@@ -61,11 +63,13 @@
     }
   }
 
-  public void fire(Change change, PatchSet patchSet, Account.Id accountId) {
+  public void fire(Change change, PatchSet patchSet, Account.Id accountId,
+      Timestamp when) {
     try {
       fire(util.changeInfo(change),
           util.revisionInfo(change.getProject(), patchSet),
-          util.accountInfo(accountId));
+          util.accountInfo(accountId),
+          when);
     } catch (PatchListNotAvailableException | GpgException | IOException
         | OrmException e) {
       log.error("Couldn't fire event", e);
@@ -76,8 +80,9 @@
       implements DraftPublishedListener.Event {
     private final AccountInfo publisher;
 
-    Event(ChangeInfo change, RevisionInfo revision, AccountInfo publisher) {
-      super(change, revision);
+    Event(ChangeInfo change, RevisionInfo revision, AccountInfo publisher,
+        Timestamp when) {
+      super(change, revision, publisher, when, NotifyHandling.ALL);
       this.publisher = publisher;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
index 0ce2a7e..114338d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -55,7 +55,7 @@
     this.changeDataFactory = changeDataFactory;
     this.db = db;
     this.changeJson = changeJsonFactory.create(
-        EnumSet.of(ListChangesOption.CURRENT_COMMIT));
+        EnumSet.allOf(ListChangesOption.class));
     this.accountCache = accountCache;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 4de43fe..386bcea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -213,5 +214,10 @@
       return String.format("%s[%s,%s: %s -> %s]", getClass().getSimpleName(),
           projectName, ref, oldObjectId, newObjectId);
     }
+
+    @Override
+    public NotifyHandling getNotify() {
+      return NotifyHandling.ALL;
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
index 692f908..fe42d02 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.events.HashtagsEditedListener;
@@ -27,6 +28,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.Set;
 
@@ -44,12 +46,13 @@
     this.util = util;
   }
 
-  public void fire(ChangeInfo change, AccountInfo editor, Collection<String> hashtags,
-      Collection<String> added, Collection<String> removed) {
+  public void fire(ChangeInfo change, AccountInfo editor,
+      Collection<String> hashtags, Collection<String> added,
+      Collection<String> removed, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
-    Event event = new Event(change, editor, hashtags, added, removed);
+    Event event = new Event(change, editor, hashtags, added, removed, when);
     for (HashtagsEditedListener l : listeners) {
       try {
         l.onHashtagsEdited(event);
@@ -60,15 +63,16 @@
   }
 
   public void fire(Change change, Id accountId,
-      ImmutableSortedSet<String> updatedHashtags, Set<String> toAdd,
-      Set<String> toRemove) {
+      ImmutableSortedSet<String> hashtags, Set<String> added,
+      Set<String> removed, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
     try {
       fire(util.changeInfo(change),
           util.accountInfo(accountId),
-          updatedHashtags, toAdd, toRemove);
+          hashtags, added, removed,
+          when);
     } catch (OrmException e) {
       log.error("Couldn't fire event", e);
     }
@@ -83,8 +87,8 @@
     private Collection<String> removedHashtags;
 
     Event(ChangeInfo change, AccountInfo editor, Collection<String> updated,
-        Collection<String> added, Collection<String> removed) {
-      super(change);
+        Collection<String> added, Collection<String> removed, Timestamp when) {
+      super(change, editor, when, NotifyHandling.ALL);
       this.editor = editor;
       this.updatedHashtags = updated;
       this.addedHashtags = added;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/PluginEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/PluginEvent.java
index eadb6b9..fbca02e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/PluginEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/PluginEvent.java
@@ -36,7 +36,8 @@
     }
   }
 
-  private static class Event implements PluginEventListener.Event {
+  private static class Event extends AbstractNoNotifyEvent
+      implements PluginEventListener.Event {
     private final String pluginName;
     private final String type;
     private final String data;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
index ef7f013..35b14dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
@@ -31,6 +34,8 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.List;
 
 public class ReviewerAdded {
   private static final Logger log =
@@ -47,28 +52,40 @@
   }
 
   public void fire(ChangeInfo change, RevisionInfo revision,
-      AccountInfo reviewer) {
+      List<AccountInfo> reviewers, AccountInfo adder, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
-    Event event = new Event(change, revision, reviewer);
+    Event event = new Event(change, revision, reviewers, adder, when);
     for (ReviewerAddedListener l : listeners) {
       try {
-        l.onReviewerAdded(event);
+        l.onReviewersAdded(event);
       } catch (Exception e) {
         log.warn("Error in event listener, e");
       }
     }
   }
 
-  public void fire(Change change, PatchSet patchSet, Account account) {
-    if (!listeners.iterator().hasNext()) {
+  public void fire(Change change, PatchSet patchSet, List<Account.Id> reviewers,
+      Account adder, Timestamp when) {
+    if (!listeners.iterator().hasNext() || reviewers.isEmpty()) {
       return;
     }
+
+    List<AccountInfo> transformed = Lists.transform(reviewers,
+        new Function<Account.Id, AccountInfo>() {
+          @Override
+          public AccountInfo apply(Account.Id account) {
+            return util.accountInfo(account);
+          }
+        });
+
     try {
       fire(util.changeInfo(change),
           util.revisionInfo(change.getProject(), patchSet),
-          util.accountInfo(account));
+          transformed,
+          util.accountInfo(adder),
+          when);
     } catch (PatchListNotAvailableException | GpgException | IOException
         | OrmException e) {
       log.error("Couldn't fire event", e);
@@ -77,16 +94,17 @@
 
   private static class Event extends AbstractRevisionEvent
       implements ReviewerAddedListener.Event {
-    private final AccountInfo reviewer;
+    private final List<AccountInfo> reviewers;
 
-    Event(ChangeInfo change, RevisionInfo revision, AccountInfo reviewer) {
-      super(change, revision);
-      this.reviewer = reviewer;
+    Event(ChangeInfo change, RevisionInfo revision, List<AccountInfo> reviewers,
+        AccountInfo adder, Timestamp when) {
+      super(change, revision, adder, when, NotifyHandling.ALL);
+      this.reviewers = reviewers;
     }
 
     @Override
-    public AccountInfo getReviewer() {
-      return reviewer;
+    public List<AccountInfo> getReviewers() {
+      return reviewers;
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
index 204f014..270eb35 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -50,14 +51,14 @@
   }
 
   public void fire(ChangeInfo change, RevisionInfo revision,
-      AccountInfo reviewer, String message,
+      AccountInfo reviewer, AccountInfo remover, String message,
       Map<String, ApprovalInfo> newApprovals,
-      Map<String, ApprovalInfo> oldApprovals) {
+      Map<String, ApprovalInfo> oldApprovals, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
-    Event event = new Event(change, revision, reviewer, message,
-        newApprovals, oldApprovals);
+    Event event = new Event(change, revision, reviewer, remover, message,
+        newApprovals, oldApprovals, when);
     for (ReviewerDeletedListener listener : listeners) {
       try {
         listener.onReviewerDeleted(event);
@@ -68,9 +69,9 @@
   }
 
   public void fire(Change change, PatchSet patchSet, Account reviewer,
-      String message,
+      Account remover, String message,
       Map<String, Short> newApprovals,
-      Map<String, Short> oldApprovals, Timestamp ts) {
+      Map<String, Short> oldApprovals, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
@@ -78,9 +79,11 @@
       fire(util.changeInfo(change),
           util.revisionInfo(change.getProject(), patchSet),
           util.accountInfo(reviewer),
+          util.accountInfo(remover),
           message,
-          util.approvals(reviewer, newApprovals, ts),
-          util.approvals(reviewer, oldApprovals, ts));
+          util.approvals(reviewer, newApprovals, when),
+          util.approvals(reviewer, oldApprovals, when),
+          when);
     } catch (PatchListNotAvailableException | GpgException | IOException
         | OrmException e) {
       log.error("Couldn't fire event", e);
@@ -96,9 +99,10 @@
     private final Map<String, ApprovalInfo> oldApprovals;
 
     Event(ChangeInfo change, RevisionInfo revision, AccountInfo reviewer,
-        String comment, Map<String, ApprovalInfo> newApprovals,
-        Map<String, ApprovalInfo> oldApprovals) {
-      super(change, revision);
+        AccountInfo remover, String comment,
+        Map<String, ApprovalInfo> newApprovals,
+        Map<String, ApprovalInfo> oldApprovals, Timestamp when) {
+      super(change, revision, remover, when, NotifyHandling.ALL);
       this.reviewer = reviewer;
       this.comment = comment;
       this.newApprovals = newApprovals;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
index 6b2418d..98fa05e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
@@ -31,6 +32,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.sql.Timestamp;
 
 public class RevisionCreated {
   private static final Logger log =
@@ -47,11 +49,11 @@
   }
 
   public void fire(ChangeInfo change, RevisionInfo revision,
-      AccountInfo uploader) {
+      AccountInfo uploader, Timestamp when, NotifyHandling notify) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
-    Event event = new Event(change, revision, uploader);
+    Event event = new Event(change, revision, uploader, when, notify);
     for (RevisionCreatedListener l : listeners) {
       try {
         l.onRevisionCreated(event);
@@ -61,14 +63,16 @@
     }
   }
 
-  public void fire(Change change, PatchSet patchSet, Account.Id uploader) {
+  public void fire(Change change, PatchSet patchSet, Account.Id uploader,
+      Timestamp when, NotifyHandling notify) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
     try {
       fire(util.changeInfo(change),
           util.revisionInfo(change.getProject(), patchSet),
-          util.accountInfo(uploader));
+          util.accountInfo(uploader),
+          when, notify);
     } catch ( PatchListNotAvailableException | GpgException | IOException
         | OrmException e) {
       log.error("Couldn't fire event", e);
@@ -79,8 +83,9 @@
       implements RevisionCreatedListener.Event {
     private final AccountInfo uploader;
 
-    Event(ChangeInfo change, RevisionInfo revision, AccountInfo uploader) {
-      super(change, revision);
+    Event(ChangeInfo change, RevisionInfo revision, AccountInfo uploader,
+        Timestamp when, NotifyHandling notify) {
+      super(change, revision, uploader, when, notify);
       this.uploader = uploader;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
index fc97d58..0a0a8ca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.events.TopicEditedListener;
@@ -26,6 +27,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.sql.Timestamp;
+
 public class TopicEdited {
   private static final Logger log =
       LoggerFactory.getLogger(TopicEdited.class);
@@ -40,11 +43,12 @@
     this.util = util;
   }
 
-  public void fire(ChangeInfo change, AccountInfo editor, String oldTopic) {
+  public void fire(ChangeInfo change, AccountInfo editor, String oldTopic,
+      Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
-    Event event = new Event(change, editor, oldTopic);
+    Event event = new Event(change, editor, oldTopic, when);
     for (TopicEditedListener l : listeners) {
       try {
         l.onTopicEdited(event);
@@ -54,14 +58,16 @@
     }
   }
 
-  public void fire(Change change, Account account, String oldTopicName) {
+  public void fire(Change change, Account account, String oldTopicName,
+      Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
     try {
       fire(util.changeInfo(change),
           util.accountInfo(account),
-          oldTopicName);
+          oldTopicName,
+          when);
     } catch (OrmException e) {
       log.error("Couldn't fire event", e);
     }
@@ -72,8 +78,9 @@
     private final AccountInfo editor;
     private final String oldTopic;
 
-    Event(ChangeInfo change, AccountInfo editor, String oldTopic) {
-      super(change);
+    Event(ChangeInfo change, AccountInfo editor, String oldTopic,
+        Timestamp when) {
+      super(change, editor, when, NotifyHandling.ALL);
       this.editor = editor;
       this.oldTopic = oldTopic;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/VoteDeleted.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
new file mode 100644
index 0000000..7772d9c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
@@ -0,0 +1,133 @@
+// Copyright (C) 2016 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.extensions.events;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.events.VoteDeletedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.GpgException;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Map;
+
+public class VoteDeleted {
+  private static final Logger log =
+      LoggerFactory.getLogger(VoteDeleted.class);
+
+  private final DynamicSet<VoteDeletedListener> listeners;
+  private final EventUtil util;
+
+  @Inject
+  VoteDeleted(DynamicSet<VoteDeletedListener> listeners,
+      EventUtil util) {
+    this.listeners = listeners;
+    this.util = util;
+  }
+
+  public void fire(ChangeInfo change, RevisionInfo revision,
+      Map<String, ApprovalInfo> approvals,
+      Map<String, ApprovalInfo> oldApprovals,
+      NotifyHandling notify, String message,
+      AccountInfo remover, Timestamp when) {
+    if (!listeners.iterator().hasNext()) {
+      return;
+    }
+    Event event = new Event(
+        change, revision, approvals, oldApprovals, notify, message,
+        remover, when);
+    for (VoteDeletedListener l : listeners) {
+      try {
+        l.onVoteDeleted(event);
+      } catch (Exception e) {
+        log.warn("Error in event listener", e);
+      }
+    }
+  }
+
+  public void fire(Change change, PatchSet ps,
+      Map<String, Short> approvals,
+      Map<String, Short> oldApprovals,
+      NotifyHandling notify, String message,
+      Account remover, Timestamp when) {
+    if (!listeners.iterator().hasNext()) {
+      return;
+    }
+    try {
+      fire(util.changeInfo(change),
+          util.revisionInfo(change.getProject(), ps),
+          util.approvals(remover, approvals, when),
+          util.approvals(remover, oldApprovals, when),
+          notify, message,
+          util.accountInfo(remover), when);
+    } catch (PatchListNotAvailableException | GpgException | IOException
+        | OrmException e) {
+      log.error("Couldn't fire event", e);
+    }
+  }
+
+  private static class Event extends AbstractRevisionEvent
+      implements VoteDeletedListener.Event {
+
+    private final Map<String, ApprovalInfo> approvals;
+    private final Map<String, ApprovalInfo> oldApprovals;
+    private final String message;
+
+    Event(ChangeInfo change, RevisionInfo revision,
+        Map<String, ApprovalInfo> approvals,
+        Map<String, ApprovalInfo> oldApprovals,
+        NotifyHandling notify, String message,
+        AccountInfo remover, Timestamp when) {
+      super(change, revision, remover, when, notify);
+      this.approvals = approvals;
+      this.oldApprovals = oldApprovals;
+      this.message = message;
+    }
+
+    @Override
+    public Map<String, ApprovalInfo> getApprovals() {
+      return approvals;
+    }
+
+    @Override
+    public Map<String, ApprovalInfo> getOldApprovals() {
+      return oldApprovals;
+    }
+
+    @Override
+    public Map<String, ApprovalInfo> getRemoved() {
+      return Maps.difference(oldApprovals, approvals).entriesOnlyOnLeft();
+    }
+
+    @Override
+    public String getMessage() {
+      return message;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index 598ed71..eb01de3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -17,6 +17,9 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
@@ -27,9 +30,11 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -38,7 +43,10 @@
 import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -50,6 +58,7 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.gerrit.server.util.RequestId;
 import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
@@ -57,6 +66,7 @@
 import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -160,6 +170,21 @@
       return user;
     }
 
+    public IdentifiedUser getIdentifiedUser() {
+      checkNotNull(user);
+      return user.asIdentifiedUser();
+    }
+
+    public Account getAccount() {
+      checkNotNull(user);
+      return user.asIdentifiedUser().getAccount();
+    }
+
+    public Account.Id getAccountId() {
+      checkNotNull(user);
+      return user.getAccountId();
+    }
+
     public Order getOrder() {
       return order;
     }
@@ -259,7 +284,7 @@
     }
   }
 
-  public static class Op {
+  public static class RepoOnlyOp {
     /**
      * Override this method to update the repo.
      *
@@ -269,6 +294,18 @@
     }
 
     /**
+     * Override this method to do something after the update
+     * e.g. send email or run hooks
+     *
+     * @param ctx context
+     */
+    //TODO(dborowitz): Support async operations?
+    public void postUpdate(Context ctx) throws Exception {
+    }
+  }
+
+  public static class Op extends RepoOnlyOp {
+    /**
      * Override this method to modify a change.
      *
      * @param ctx context
@@ -278,15 +315,6 @@
     public boolean updateChange(ChangeContext ctx) throws Exception {
       return false;
     }
-
-    /**
-     * Override this method to perform operations after the update.
-     *
-     * @param ctx context
-     */
-    // TODO(dborowitz): Support async operations?
-    public void postUpdate(Context ctx) throws Exception {
-    }
   }
 
   public abstract static class InsertChangeOp extends Op {
@@ -300,7 +328,7 @@
    * methods are called after that phase has been completed for <em>all</em> updates.
    */
   public static class Listener {
-    private static final Listener NONE = new Listener();
+    public static final Listener NONE = new Listener();
 
     /**
      * Called after updating all repositories and flushing objects but before
@@ -350,11 +378,19 @@
     return p;
   }
 
-  static void execute(Collection<BatchUpdate> updates, Listener listener)
-      throws UpdateException, RestApiException {
+  static void execute(Collection<BatchUpdate> updates, Listener listener,
+      @Nullable RequestId requestId) throws UpdateException, RestApiException {
     if (updates.isEmpty()) {
       return;
     }
+    if (requestId != null) {
+      for (BatchUpdate u : updates) {
+        checkArgument(u.requestId == null || u.requestId == requestId,
+            "refusing to overwrite RequestId %s in update with %s",
+            u.requestId, requestId);
+        u.setRequestId(requestId);
+      }
+    }
     try {
       Order order = getOrder(updates);
       boolean updateChangesInParallel = getUpdateChangesInParallel(updates);
@@ -428,7 +464,7 @@
       throw new ResourceNotFoundException(e.getMessage(), e);
 
     } catch (Exception e) {
-      Throwables.propagateIfPossible(e);
+      Throwables.throwIfUnchecked(e);
       throw new UpdateException(e);
     }
   }
@@ -446,6 +482,7 @@
   private final ReviewDb db;
   private final SchemaFactory<ReviewDb> schemaFactory;
 
+  private final long logThresholdNanos;
   private final Project.NameKey project;
   private final CurrentUser user;
   private final Timestamp when;
@@ -456,6 +493,7 @@
   private final Map<Change.Id, Change> newChanges = new HashMap<>();
   private final List<CheckedFuture<?, IOException>> indexFutures =
       new ArrayList<>();
+  private final List<RepoOnlyOp> repoOnlyOps = new ArrayList<>();
 
   private Repository repo;
   private ObjectInserter inserter;
@@ -465,9 +503,11 @@
   private boolean closeRepo;
   private Order order;
   private boolean updateChangesInParallel;
+  private RequestId requestId;
 
   @AssistedInject
   BatchUpdate(
+      @GerritServerConfig Config cfg,
       AllUsersName allUsers,
       ChangeControl.GenericFactory changeControlFactory,
       ChangeIndexer indexer,
@@ -496,6 +536,10 @@
     this.schemaFactory = schemaFactory;
     this.updateManagerFactory = updateManagerFactory;
 
+    this.logThresholdNanos = MILLISECONDS.toNanos(
+        ConfigUtil.getTimeUnit(
+            cfg, "change", null, "updateDebugLogThreshold",
+            SECONDS.toMillis(2), MILLISECONDS));
     this.db = db;
     this.project = project;
     this.user = user;
@@ -513,6 +557,11 @@
     }
   }
 
+  public BatchUpdate setRequestId(RequestId requestId) {
+    this.requestId = requestId;
+    return this;
+  }
+
   public BatchUpdate setRepository(Repository repo, RevWalk revWalk,
       ObjectInserter inserter) {
     checkState(this.repo == null, "repo already set");
@@ -568,10 +617,17 @@
 
   public BatchUpdate addOp(Change.Id id, Op op) {
     checkArgument(!(op instanceof InsertChangeOp), "use insertChange");
+    checkNotNull(op);
     ops.put(id, op);
     return this;
   }
 
+  public BatchUpdate addRepoOnlyOp(RepoOnlyOp op) {
+    checkArgument(!(op instanceof Op), "use addOp()");
+    repoOnlyOps.add(op);
+    return this;
+  }
+
   public BatchUpdate insertChange(InsertChangeOp op) {
     Context ctx = new Context();
     Change c = op.createChange(ctx);
@@ -588,32 +644,47 @@
 
   public void execute(Listener listener)
       throws UpdateException, RestApiException {
-    execute(ImmutableList.of(this), listener);
+    execute(ImmutableList.of(this), listener, requestId);
   }
 
   private void executeUpdateRepo() throws UpdateException, RestApiException {
     try {
+      logDebug("Executing updateRepo on {} ops", ops.size());
       RepoContext ctx = new RepoContext();
       for (Op op : ops.values()) {
         op.updateRepo(ctx);
       }
+
+      if (!repoOnlyOps.isEmpty()) {
+        logDebug("Executing updateRepo on {} RepoOnlyOps", ops.size());
+        for (RepoOnlyOp op : repoOnlyOps) {
+          op.updateRepo(ctx);
+        }
+      }
+
       if (inserter != null) {
+        logDebug("Flushing inserter");
         inserter.flush();
+      } else {
+        logDebug("No objects to flush");
       }
     } catch (Exception e) {
-      Throwables.propagateIfPossible(e, RestApiException.class);
+      Throwables.throwIfInstanceOf(e, RestApiException.class);
       throw new UpdateException(e);
     }
   }
 
   private void executeRefUpdates() throws IOException, UpdateException {
     if (commands == null || commands.isEmpty()) {
+      logDebug("No ref updates to execute");
       return;
     }
     // May not be opened if the caller added ref updates but no new objects.
     initRepository();
     batchRefUpdate = repo.getRefDatabase().newBatchUpdate();
     commands.addTo(batchRefUpdate);
+    logDebug("Executing batch of {} ref updates",
+        batchRefUpdate.getCommands().size());
     batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE);
     boolean ok = true;
     for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
@@ -629,15 +700,23 @@
 
   private void executeChangeOps(boolean parallel)
       throws UpdateException, RestApiException {
+    logDebug("Executing change ops (parallel? {})", parallel);
     ListeningExecutorService executor = parallel
         ? changeUpdateExector
         : MoreExecutors.newDirectExecutorService();
 
     List<ChangeTask> tasks = new ArrayList<>(ops.keySet().size());
     try {
+      if (notesMigration.commitChangeWrites() && repo != null) {
+        // A NoteDb change may have been rebuilt since the repo was originally
+        // opened, so make sure we see that.
+        logDebug("Preemptively scanning for repo changes");
+        repo.scanForRepoChanges();
+      }
       if (!ops.isEmpty() && notesMigration.failChangeWrites()) {
         // Fail fast before attempting any writes if changes are read-only, as
         // this is a programmer error.
+        logDebug("Failing early due to read-only Changes table");
         throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY);
       }
       List<ListenableFuture<?>> futures = new ArrayList<>(ops.keySet().size());
@@ -645,18 +724,30 @@
         ChangeTask task =
             new ChangeTask(e.getKey(), e.getValue(), Thread.currentThread());
         tasks.add(task);
+        if (!parallel) {
+          logDebug("Direct execution of task for ops: {}", ops);
+        }
         futures.add(executor.submit(task));
       }
+      if (parallel) {
+        logDebug("Waiting on futures for {} ops spanning {} changes",
+            ops.size(), ops.keySet().size());
+      }
+      // TODO(dborowitz): Timing is wrong for non-parallel updates.
+      long startNanos = System.nanoTime();
       Futures.allAsList(futures).get();
+      maybeLogSlowUpdate(startNanos, "change");
 
       if (notesMigration.commitChangeWrites()) {
+        startNanos = System.nanoTime();
         executeNoteDbUpdates(tasks);
+        maybeLogSlowUpdate(startNanos, "NoteDb");
       }
     } catch (ExecutionException | InterruptedException e) {
-      Throwables.propagateIfInstanceOf(e.getCause(), UpdateException.class);
-      Throwables.propagateIfInstanceOf(e.getCause(), RestApiException.class);
+      Throwables.throwIfInstanceOf(e.getCause(), UpdateException.class);
+      Throwables.throwIfInstanceOf(e.getCause(), RestApiException.class);
       throw new UpdateException(e);
-    } catch (OrmException e) {
+    } catch (OrmException | IOException e) {
       throw new UpdateException(e);
     }
 
@@ -664,12 +755,32 @@
     for (ChangeTask task : tasks) {
       if (task.deleted) {
         indexFutures.add(indexer.deleteAsync(task.id));
-      } else {
+      } else if (task.dirty) {
         indexFutures.add(indexer.indexAsync(project, task.id));
       }
     }
   }
 
+  private static class SlowUpdateException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    private SlowUpdateException(String fmt, Object... args) {
+      super(String.format(fmt, args));
+    }
+  }
+
+  private void maybeLogSlowUpdate(long startNanos, String desc) {
+    long elapsedNanos = System.nanoTime() - startNanos;
+    if (!log.isDebugEnabled() || elapsedNanos <= logThresholdNanos) {
+      return;
+    }
+    // Always log even without RequestId.
+    log.debug("Slow " + desc + " update",
+        new SlowUpdateException(
+            "Slow %s update (%d ms) to %s for %s",
+            desc, NANOSECONDS.toMillis(elapsedNanos), project, ops.keySet()));
+  }
+
   private void executeNoteDbUpdates(List<ChangeTask> tasks) {
     // Aggregate together all NoteDb ref updates from the ops we executed,
     // possibly in parallel. Each task had its own NoteDbUpdateManager instance
@@ -681,24 +792,30 @@
     //
     // See the comments in NoteDbUpdateManager#execute() for why we execute the
     // updates on the change repo first.
+    logDebug("Executing NoteDb updates for {} changes", tasks.size());
     try {
       BatchRefUpdate changeRefUpdate =
           getRepository().getRefDatabase().newBatchUpdate();
       boolean hasAllUsersCommands = false;
       try (ObjectInserter ins = getRepository().newObjectInserter()) {
+        int objs = 0;
         for (ChangeTask task : tasks) {
           if (task.noteDbResult == null) {
-            continue; // No-op update.
+            logDebug("No-op update to {}", task.id);
+            continue;
           }
           for (ReceiveCommand cmd : task.noteDbResult.changeCommands()) {
             changeRefUpdate.addCommand(cmd);
           }
           for (InsertedObject obj : task.noteDbResult.changeObjects()) {
+            objs++;
             ins.insert(obj.type(), obj.data().toByteArray());
           }
           hasAllUsersCommands |=
               !task.noteDbResult.allUsersCommands().isEmpty();
         }
+        logDebug("Collected {} objects and {} ref updates to change repo",
+            objs, changeRefUpdate.getCommands().size());
         executeNoteDbUpdate(getRevWalk(), ins, changeRefUpdate);
       }
 
@@ -706,6 +823,7 @@
         try (Repository allUsersRepo = repoManager.openRepository(allUsers);
             RevWalk allUsersRw = new RevWalk(allUsersRepo);
             ObjectInserter allUsersIns = allUsersRepo.newObjectInserter()) {
+          int objs = 0;
           BatchRefUpdate allUsersRefUpdate =
               allUsersRepo.getRefDatabase().newBatchUpdate();
           for (ChangeTask task : tasks) {
@@ -716,14 +834,19 @@
               allUsersIns.insert(obj.type(), obj.data().toByteArray());
             }
           }
+          logDebug("Collected {} objects and {} ref updates to All-Users",
+              objs, allUsersRefUpdate.getCommands().size());
           executeNoteDbUpdate(allUsersRw, allUsersIns, allUsersRefUpdate);
         }
+      } else {
+        logDebug("No All-Users updates");
       }
     } catch (IOException e) {
       // Ignore all errors trying to update NoteDb at this point. We've
       // already written the NoteDbChangeState to ReviewDb, which means
       // if the state is out of date it will be rebuilt the next time it
       // is needed.
+      // Always log even without RequestId.
       log.debug(
           "Ignoring NoteDb update error after ReviewDb write", e);
     }
@@ -732,6 +855,7 @@
   private void executeNoteDbUpdate(RevWalk rw, ObjectInserter ins,
       BatchRefUpdate bru) throws IOException {
     if (bru.getCommands().isEmpty()) {
+      logDebug("No commands, skipping flush and ref update");
       return;
     }
     ins.flush();
@@ -739,13 +863,7 @@
     bru.execute(rw, NullProgressMonitor.INSTANCE);
     for (ReceiveCommand cmd : bru.getCommands()) {
       if (cmd.getResult() != ReceiveCommand.Result.OK) {
-        // TODO(dborowitz): Not necessary once JGit is updated to include
-        // ba8eb931734d990c5a6a9352e4629fc84a191808.
-        StringBuilder sb = new StringBuilder("Update failed: [\n");
-        for (ReceiveCommand cmd2 : bru.getCommands()) {
-          sb.append(cmd2).append(": ").append(cmd2.getMessage()).append('\n');
-        }
-        throw new IOException(sb.append(']').toString());
+        throw new IOException("Update failed: " + bru);
       }
     }
   }
@@ -756,7 +874,9 @@
     private final Thread mainThread;
 
     NoteDbUpdateManager.StagedResult noteDbResult;
+    boolean dirty;
     boolean deleted;
+    private String taskId;
 
     private ChangeTask(Change.Id id, Collection<Op> changeOps,
         Thread mainThread) {
@@ -767,6 +887,7 @@
 
     @Override
     public Void call() throws Exception {
+      taskId = id.toString() + "-" + Thread.currentThread().getId();
       if (Thread.currentThread() == mainThread) {
         Repository repo = getRepository();
         try (ObjectReader reader = repo.newObjectReader();
@@ -791,21 +912,26 @@
 
     private void call(ReviewDb db, Repository repo, RevWalk rw)
         throws Exception {
+      @SuppressWarnings("resource") // Not always opened.
+      NoteDbUpdateManager updateManager = null;
       try {
         ChangeContext ctx;
-        NoteDbUpdateManager updateManager = null;
-        boolean dirty = false;
         db.changes().beginTransaction(id);
         try {
           ctx = newChangeContext(db, repo, rw, id);
           // Call updateChange on each op.
+          logDebug("Calling updateChange on {} ops", changeOps.size());
           for (Op op : changeOps) {
             dirty |= op.updateChange(ctx);
           }
           if (!dirty) {
+            logDebug("No ops reported dirty, short-circuiting");
             return;
           }
           deleted = ctx.deleted;
+          if (deleted) {
+            logDebug("Change was deleted");
+          }
 
           // Stage the NoteDb update and store its state in the Change.
           if (notesMigration.commitChangeWrites()) {
@@ -816,10 +942,13 @@
           Iterable<Change> cs = changesToUpdate(ctx);
           if (newChanges.containsKey(id)) {
             // Insert rather than upsert in case of a race on change IDs.
+            logDebug("Inserting change");
             db.changes().insert(cs);
           } else if (deleted) {
+            logDebug("Deleting change");
             db.changes().delete(cs);
           } else {
+            logDebug("Updating change");
             db.changes().update(cs);
           }
           db.commit();
@@ -846,8 +975,13 @@
           }
         }
       } catch (Exception e) {
+        logDebug("Error updating change (should be rethrown)", e);
         Throwables.propagateIfPossible(e, RestApiException.class);
         throw new UpdateException(e);
+      } finally {
+        if (updateManager != null) {
+          updateManager.close();
+        }
       }
     }
 
@@ -867,6 +1001,7 @@
 
     private NoteDbUpdateManager stageNoteDbUpdate(ChangeContext ctx,
         boolean deleted) throws OrmException, IOException {
+      logDebug("Staging NoteDb update");
       NoteDbUpdateManager updateManager = updateManagerFactory
           .create(ctx.getProject())
           .setChangeRepo(ctx.getRepository(), ctx.getRevWalk(), null,
@@ -883,9 +1018,22 @@
         // Refused to apply update because NoteDb was out of sync. Go ahead with
         // this ReviewDb update; it's still out of sync, but this is no worse
         // than before, and it will eventually get rebuilt.
+        logDebug("Ignoring OrmConcurrencyException while staging");
       }
       return updateManager;
     }
+
+    private void logDebug(String msg, Throwable t) {
+      if (log.isDebugEnabled()) {
+        BatchUpdate.this.logDebug("[" + taskId + "]" + msg, t);
+      }
+    }
+
+    private void logDebug(String msg, Object... args) {
+      if (log.isDebugEnabled()) {
+        BatchUpdate.this.logDebug("[" + taskId + "]" + msg, args);
+      }
+    }
   }
 
   private static Iterable<Change> changesToUpdate(ChangeContext ctx) {
@@ -901,5 +1049,24 @@
     for (Op op : ops.values()) {
       op.postUpdate(ctx);
     }
+
+    for (RepoOnlyOp op : repoOnlyOps) {
+      op.postUpdate(ctx);
+    }
+  }
+
+  private void logDebug(String msg, Throwable t) {
+    if (requestId != null && log.isDebugEnabled()) {
+      log.debug(requestId + msg, t);
+    }
+  }
+
+  private void logDebug(String msg, Object... args) {
+    // Only log if there is a requestId assigned, since those are the
+    // expensive/complicated requests like MergeOp. Doing it every time would be
+    // noisy.
+    if (requestId != null && log.isDebugEnabled()) {
+      log.debug(requestId + msg, args);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdateReviewDb.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdateReviewDb.java
index 8351e41..1de98d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdateReviewDb.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdateReviewDb.java
@@ -37,6 +37,18 @@
     return changesWrapper;
   }
 
+  @Override
+  public void commit() {
+    throw new UnsupportedOperationException(
+        "do not call commit; BatchUpdate always manages transactions");
+  }
+
+  @Override
+  public void rollback() {
+    throw new UnsupportedOperationException(
+        "do not call rollback; BatchUpdate always manages transactions");
+  }
+
   private static class BatchUpdateChanges extends ChangeAccessWrapper {
     private BatchUpdateChanges(ChangeAccess delegate) {
       super(delegate);
@@ -51,14 +63,14 @@
     @Override
     public void upsert(Iterable<Change> instances) {
       throw new UnsupportedOperationException(
-          "do not call upsert; either use InsertChangeOp for insertion, or"
-          + " ChangeContext#saveChange() for update");
+          "do not call upsert; existing changes are updated automatically,"
+          + " or use InsertChangeOp for insertion");
     }
 
     @Override
     public void update(Iterable<Change> instances) {
       throw new UnsupportedOperationException(
-          "do not call update; use ChangeContext#saveChange()");
+          "do not call update; change is updated automatically");
     }
 
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeAlreadyMergedException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeAlreadyMergedException.java
new file mode 100644
index 0000000..f2e7f78
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeAlreadyMergedException.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+/**
+ * Indicates that the change or commit is already in the source tree.
+ */
+public class ChangeAlreadyMergedException extends MergeIdenticalTreeException {
+  private static final long serialVersionUID = 1L;
+
+  /** @param msg message to return to the client describing the error. */
+  public ChangeAlreadyMergedException(String msg) {
+    super(msg);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
index f19c0aa..66e0704 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
index cfdedd0..5bb4dfd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -17,10 +17,10 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.extensions.events.GarbageCollectorListener;
-import com.google.gerrit.extensions.events.GarbageCollectorListener.Event;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GcConfig;
+import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.api.GarbageCollectCommand;
@@ -115,17 +115,10 @@
   }
 
   private void fire(final Project.NameKey p, final Properties statistics) {
-    Event event = new GarbageCollectorListener.Event() {
-      @Override
-      public String getProjectName() {
-        return p.get();
-      }
-
-      @Override
-      public Properties getStatistics() {
-        return statistics;
-      }
-    };
+    if (!listeners.iterator().hasNext()) {
+      return;
+    }
+    Event event = new Event(p, statistics);
     for (GarbageCollectorListener l : listeners) {
       try {
         l.onGarbageCollected(event);
@@ -204,4 +197,25 @@
       writer.print(message);
     }
   }
+
+  private static class Event extends AbstractNoNotifyEvent
+      implements GarbageCollectorListener.Event {
+    private final Project.NameKey p;
+    private final Properties statistics;
+
+    Event(Project.NameKey p, Properties statistics) {
+      this.p = p;
+      this.statistics = statistics;
+    }
+
+    @Override
+    public String getProjectName() {
+      return p.get();
+    }
+
+    @Override
+    public Properties getStatistics() {
+      return statistics;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
index aa0fc55..e609d68 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
@@ -15,6 +15,9 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicSet;
+
+import org.eclipse.jgit.transport.PostUploadHook;
 
 /** Configures the Git support. */
 public class GitModule extends FactoryModule {
@@ -24,5 +27,7 @@
     factory(MetaDataUpdate.InternalFactory.class);
     bind(MetaDataUpdate.Server.class);
     bind(ReceiveConfig.class);
+    DynamicSet.bind(binder(), PostUploadHook.class)
+        .to(UploadPackMetricsHook.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
index e0c72c6..0e954f3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.util.RequestId;
 import com.google.gerrit.server.util.SubmoduleSectionParser;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -53,7 +54,7 @@
 
   private static final String GIT_MODULES = ".gitmodules";
 
-  private final String submissionId;
+  private final RequestId submissionId;
   Set<SubmoduleSubscription> subscriptions;
 
   @AssistedInject
@@ -64,12 +65,12 @@
     this.submissionId = orm.getSubmissionId();
     Project.NameKey project = branch.getParentKey();
     logDebug("Loading .gitmodules of {} for project {}", branch, project);
+    OpenRepo or;
     try {
-      orm.openRepo(project, false);
+      or = orm.openRepo(project, false);
     } catch (NoSuchProjectException e) {
       throw new IOException(e);
     }
-    OpenRepo or = orm.getRepo(project);
 
     ObjectId id = or.repo.resolve(branch.get());
     if (id == null) {
@@ -109,7 +110,7 @@
 
   private void logDebug(String msg, Object... args) {
     if (log.isDebugEnabled()) {
-      log.debug("[" + submissionId + "]" + msg, args);
+      log.debug(submissionId + msg, args);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/InMemoryInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/InMemoryInserter.java
index 1b551f2..a70c235 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/InMemoryInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/InMemoryInserter.java
@@ -31,6 +31,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
@@ -108,9 +109,16 @@
     }
 
     @Override
-    public Collection<ObjectId> resolve(AbbreviatedObjectId id) {
-      // This method should be unused by ChangeRebuilder.
-      throw new UnsupportedOperationException();
+    public Collection<ObjectId> resolve(AbbreviatedObjectId id)
+        throws IOException {
+      Set<ObjectId> result = new HashSet<>();
+      for (ObjectId insId : inserted.keySet()) {
+        if (id.prefixCompare(insId) == 0) {
+          result.add(insId);
+        }
+      }
+      result.addAll(reader.resolve(id));
+      return result;
     }
 
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index da9cf1d..093d036b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -403,7 +403,8 @@
     private boolean isRepo(Path p) {
       String name = p.getFileName().toString();
       return !name.equals(Constants.DOT_GIT)
-          && name.endsWith(Constants.DOT_GIT_EXT);
+          && (name.endsWith(Constants.DOT_GIT_EXT)
+              || FileKey.isGitRepository(p.toFile(), FS.DETECTED));
     }
 
     private void addProject(Path p) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
index 109fa76..dd6b717 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
@@ -14,10 +14,17 @@
 
 package com.google.gerrit.server.git;
 
-/** Indicates that the commit is already contained in destination banch. */
-public class MergeIdenticalTreeException extends Exception {
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+/**
+ * Indicates that the commit is already contained in destination branch.
+ * Either the commit itself is in the source tree, or the content is merged
+ */
+public class MergeIdenticalTreeException extends RestApiException {
   private static final long serialVersionUID = 1L;
+
+  /** @param msg message to return to the client describing the error. */
   public MergeIdenticalTreeException(String msg) {
-    super(msg, null);
+    super(msg);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index e2ead25..9d62721 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -33,8 +33,6 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
-import com.google.common.hash.Hasher;
-import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.SubmitRecord;
@@ -67,6 +65,7 @@
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.util.RequestId;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
@@ -79,8 +78,6 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -226,18 +223,8 @@
   private final SubmoduleOp.Factory subOpFactory;
   private final MergeOpRepoManager orm;
 
-  private static final String MACHINE_ID;
-  static {
-    String id;
-    try {
-      id = InetAddress.getLocalHost().getHostAddress();
-    } catch (UnknownHostException e) {
-      id = "unknown";
-    }
-    MACHINE_ID = id;
-  }
   private Timestamp ts;
-  private String submissionId;
+  private RequestId submissionId;
   private IdentifiedUser caller;
 
   private CommitStatus commits;
@@ -372,7 +359,8 @@
     return Joiner.on("; ").join(labelResults);
   }
 
-  private void checkSubmitRulesAndState(ChangeSet cs) {
+  private void checkSubmitRulesAndState(ChangeSet cs)
+      throws ResourceConflictException {
     checkArgument(!cs.furtherHiddenChanges(),
         "checkSubmitRulesAndState called for topic with hidden change");
     for (ChangeData cd : cs.changes()) {
@@ -391,6 +379,7 @@
         commits.problem(cd.getId(), msg);
       }
     }
+    commits.maybeFailVerbose();
   }
 
   private void bypassSubmitRules(ChangeSet cs) {
@@ -411,21 +400,13 @@
     }
   }
 
-  private void updateSubmissionId(Change change) {
-    Hasher h = Hashing.sha1().newHasher();
-    h.putLong(Thread.currentThread().getId())
-        .putUnencodedChars(MACHINE_ID);
-    ts = TimeUtil.nowTs();
-    submissionId = change.getId().get() + "-" + ts.getTime() +
-        "-" + h.hash().toString().substring(0, 8);
-  }
-
   public void merge(ReviewDb db, Change change, IdentifiedUser caller,
       boolean checkSubmitRules, SubmitInput submitInput)
       throws OrmException, RestApiException {
     this.submitInput = submitInput;
     this.caller = caller;
-    updateSubmissionId(change);
+    this.ts = TimeUtil.nowTs();
+    submissionId = RequestId.forChange(change);
     this.db = db;
     orm.setContext(db, ts, caller, submissionId);
 
@@ -444,7 +425,6 @@
       if (checkSubmitRules) {
         logDebug("Checking submit rules and state");
         checkSubmitRulesAndState(cs);
-        failFast(cs); // Done checks that don't involve opening repo.
       } else {
         logDebug("Bypassing submit rules");
         bypassSubmitRules(cs);
@@ -461,20 +441,6 @@
     }
   }
 
-  private void failFast(ChangeSet cs) throws ResourceConflictException {
-    if (commits.isOk()) {
-      return;
-    }
-    String msg = "Failed to submit " + cs.size() + " change"
-        + (cs.size() > 1 ? "s" : "") + " due to the following problems:\n";
-    Multimap<Change.Id, String> problems = commits.getProblems();
-    List<String> ps = new ArrayList<>(problems.keySet().size());
-    for (Change.Id id : problems.keySet()) {
-      ps.add("Change " + id + ": " + Joiner.on("; ").join(problems.get(id)));
-    }
-    throw new ResourceConflictException(msg + Joiner.on('\n').join(ps));
-  }
-
   private void integrateIntoHistory(ChangeSet cs)
       throws IntegrationException, RestApiException {
     checkArgument(!cs.furtherHiddenChanges(),
@@ -492,36 +458,28 @@
       throw new IntegrationException("Error reading changes to submit", e);
     }
     Set<Project.NameKey> projects = br.keySet();
-    Collection<Branch.NameKey> branches = cbb.keySet();
+    Set<Branch.NameKey> branches = cbb.keySet();
     openRepos(projects);
 
     for (Branch.NameKey branch : branches) {
       OpenRepo or = orm.getRepo(branch.getParentKey());
       toSubmit.put(branch, validateChangeList(or, cbb.get(branch)));
     }
-    failFast(cs); // Done checks that don't involve running submit strategies.
-
-    List<SubmitStrategy> strategies = new ArrayList<>(branches.size());
-    for (Branch.NameKey branch : branches) {
-      OpenRepo or = orm.getRepo(branch.getParentKey());
-      OpenBranch ob = or.getBranch(branch);
-      BranchBatch submitting = toSubmit.get(branch);
-      checkNotNull(submitting.submitType(),
-          "null submit type for %s; expected to previously fail fast",
-          submitting);
-      Set<CodeReviewCommit> commitsToSubmit = commits(submitting.changes());
-      ob.mergeTip = new MergeTip(ob.oldTip, commitsToSubmit);
-      SubmitStrategy strategy = createStrategy(or, ob.mergeTip, branch,
-          submitting.submitType(), ob.oldTip);
-      strategies.add(strategy);
-      strategy.addOps(or.getUpdate(), commitsToSubmit);
-    }
-
+    // Done checks that don't involve running submit strategies.
+    commits.maybeFailVerbose();
+    SubmoduleOp submoduleOp = subOpFactory.create(branches, orm);
     try {
+      List<SubmitStrategy> strategies = getSubmitStrategies(toSubmit, submoduleOp);
+      Set<Project.NameKey> allProjects = submoduleOp.getProjectsInOrder();
+      // in case superproject subscription is disabled, allProjects would be null
+      if (allProjects == null) {
+        allProjects = projects;
+      }
       BatchUpdate.execute(
-          batchUpdates(projects),
-          new SubmitStrategyListener(submitInput, strategies, commits));
-    } catch (UpdateException e) {
+          orm.batchUpdates(allProjects),
+          new SubmitStrategyListener(submitInput, strategies, commits),
+          submissionId);
+    } catch (UpdateException | SubmoduleException e) {
       // BatchUpdate may have inadvertently wrapped an IntegrationException
       // thrown by some legacy SubmitStrategyOp code that intended the error
       // message to be user-visible. Copy the message from the wrapped
@@ -533,20 +491,44 @@
       if (e.getCause() instanceof IntegrationException) {
         msg = e.getCause().getMessage();
       } else {
-        msg = "Error submitting change" + (cs.size() != 1 ? "s" : "");
+        msg = "Error submitting change" + (cs.size() != 1 ? "s" : "") + ": \n"
+            + e.getMessage();
       }
       throw new IntegrationException(msg, e);
     }
-
-    updateSuperProjects(br.values());
   }
 
-  private List<BatchUpdate> batchUpdates(Collection<Project.NameKey> projects) {
-    List<BatchUpdate> updates = new ArrayList<>(projects.size());
-    for (Project.NameKey project : projects) {
-      updates.add(orm.getRepo(project).getUpdate());
+  private List<SubmitStrategy> getSubmitStrategies(
+      Map<Branch.NameKey, BranchBatch> toSubmit, SubmoduleOp submoduleOp)
+      throws IntegrationException {
+    List<SubmitStrategy> strategies = new ArrayList<>();
+    Set<Branch.NameKey> allBranches = submoduleOp.getBranchesInOrder();
+    // in case superproject subscription is disabled, allBranches would be null
+    if (allBranches == null) {
+      allBranches = toSubmit.keySet();
     }
-    return updates;
+
+    for (Branch.NameKey branch : allBranches) {
+      OpenRepo or = orm.getRepo(branch.getParentKey());
+      if (toSubmit.containsKey(branch)) {
+        BranchBatch submitting = toSubmit.get(branch);
+        OpenBranch ob = or.getBranch(branch);
+        checkNotNull(submitting.submitType(),
+            "null submit type for %s; expected to previously fail fast",
+            submitting);
+        Set<CodeReviewCommit> commitsToSubmit = commits(submitting.changes());
+        ob.mergeTip = new MergeTip(ob.oldTip, commitsToSubmit);
+        SubmitStrategy strategy = createStrategy(or, ob.mergeTip, branch,
+            submitting.submitType(), ob.oldTip, submoduleOp);
+        strategies.add(strategy);
+        strategy.addOps(or.getUpdate(), commitsToSubmit);
+      } else {
+        // no open change for this branch
+        // add submodule triggered op into BatchUpdate
+        submoduleOp.addOp(or.getUpdate(), branch);
+      }
+    }
+    return strategies;
   }
 
   private Set<CodeReviewCommit> commits(List<ChangeData> cds) {
@@ -563,10 +545,10 @@
 
   private SubmitStrategy createStrategy(OpenRepo or,
       MergeTip mergeTip, Branch.NameKey destBranch, SubmitType submitType,
-      CodeReviewCommit branchTip) throws IntegrationException {
+      CodeReviewCommit branchTip, SubmoduleOp submoduleOp) throws IntegrationException {
     return submitStrategyFactory.create(submitType, db, or.repo, or.rw, or.ins,
         or.canMergeFlag, getAlreadyAccepted(or, branchTip), destBranch, caller,
-        mergeTip, commits, submissionId, submitInput.notify);
+        mergeTip, commits, submissionId, submitInput.notify, submoduleOp);
   }
 
   private Set<RevCommit> getAlreadyAccepted(OpenRepo or,
@@ -581,7 +563,10 @@
       for (Ref r : or.repo.getRefDatabase().getRefs(Constants.R_HEADS)
           .values()) {
         try {
-          alreadyAccepted.add(or.rw.parseCommit(r.getObjectId()));
+          CodeReviewCommit aac = or.rw.parseCommit(r.getObjectId());
+          if (!commits.commits.values().contains(aac)) {
+            alreadyAccepted.add(aac);
+          }
         } catch (IncorrectObjectTypeException iote) {
           // Not a commit? Skip over it.
         }
@@ -738,18 +723,6 @@
     }
   }
 
-  private void updateSuperProjects(Collection<Branch.NameKey> branches) {
-    logDebug("Updating superprojects");
-    SubmoduleOp subOp = subOpFactory.create(orm);
-    try {
-      subOp.updateSuperProjects(branches);
-      logDebug("Updating superprojects done");
-    } catch (SubmoduleException e) {
-      logError("The gitlinks were not updated according to the "
-          + "subscriptions", e);
-    }
-  }
-
   private void openRepos(Collection<Project.NameKey> projects)
       throws IntegrationException {
     for (Project.NameKey project : projects) {
@@ -771,6 +744,7 @@
       for (ChangeData cd : internalChangeQuery.byProjectOpen(destProject)) {
         try (BatchUpdate bu = batchUpdateFactory.create(db, destProject,
             internalUserFactory.create(), ts)) {
+          bu.setRequestId(submissionId);
           bu.addOp(cd.getId(), new BatchUpdate.Op() {
             @Override
             public boolean updateChange(ChangeContext ctx) throws OrmException {
@@ -807,28 +781,28 @@
 
   private void logDebug(String msg, Object... args) {
     if (log.isDebugEnabled()) {
-      log.debug("[" + submissionId + "]" + msg, args);
+      log.debug(submissionId + msg, args);
     }
   }
 
   private void logWarn(String msg, Throwable t) {
     if (log.isWarnEnabled()) {
-      log.warn("[" + submissionId + "]" + msg, t);
+      log.warn(submissionId + msg, t);
     }
   }
 
   private void logWarn(String msg) {
     if (log.isWarnEnabled()) {
-      log.warn("[" + submissionId + "]" + msg);
+      log.warn(submissionId + msg);
     }
   }
 
   private void logError(String msg, Throwable t) {
     if (log.isErrorEnabled()) {
       if (t != null) {
-        log.error("[" + submissionId + "]" + msg, t);
+        log.error(submissionId + msg, t);
       } else {
-        log.error("[" + submissionId + "]" + msg);
+        log.error(submissionId + msg);
       }
     }
   }
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 06ef492..fb4c2d4 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
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.RequestId;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -38,7 +39,10 @@
 
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
@@ -93,12 +97,21 @@
     BatchUpdate getUpdate() {
       checkState(db != null, "call setContext before getUpdate");
       if (update == null) {
-        update = batchUpdateFactory.create(db, getProjectName(), caller, ts);
-        update.setRepository(repo, rw, ins);
+        update = batchUpdateFactory.create(db, getProjectName(), caller, ts)
+            .setRepository(repo, rw, ins)
+            .setRequestId(submissionId);
       }
       return update;
     }
 
+    /**
+     * Make sure the update has already executed before reset it.
+     * TODO:czhen Have a flag in BatchUpdate to mark if it has been executed
+     */
+    void resetUpdate() {
+      update = null;
+    }
+
     void close() {
       if (update != null) {
         update.close();
@@ -142,7 +155,7 @@
   private ReviewDb db;
   private Timestamp ts;
   private IdentifiedUser caller;
-  private String submissionId;
+  private RequestId submissionId;
 
   @Inject
   MergeOpRepoManager(
@@ -157,14 +170,14 @@
   }
 
   void setContext(ReviewDb db, Timestamp ts, IdentifiedUser caller,
-      String submissionId) {
+      RequestId submissionId) {
     this.db = db;
     this.ts = ts;
     this.caller = caller;
     this.submissionId = submissionId;
   }
 
-  public String getSubmissionId() {
+  public RequestId getSubmissionId() {
     return submissionId;
   }
 
@@ -174,16 +187,17 @@
     return or;
   }
 
-  public void openRepo(Project.NameKey project, boolean abortIfOpen)
+  public OpenRepo openRepo(Project.NameKey project, boolean abortIfOpen)
       throws NoSuchProjectException, IOException {
     if (abortIfOpen) {
       checkState(!openRepos.containsKey(project),
           "repo already opened: %s", project);
-    } else {
-      if (openRepos.containsKey(project)) {
-        return;
-      }
     }
+
+    if (openRepos.containsKey(project)) {
+      return openRepos.get(project);
+    }
+
     ProjectState projectState = projectCache.get(project);
     if (projectState == null) {
       throw new NoSuchProjectException(project);
@@ -192,11 +206,20 @@
       OpenRepo or =
           new OpenRepo(repoManager.openRepository(project), projectState);
       openRepos.put(project, or);
+      return or;
     } catch (RepositoryNotFoundException e) {
       throw new NoSuchProjectException(project);
     }
   }
 
+  public List<BatchUpdate> batchUpdates(Collection<Project.NameKey> projects) {
+    List<BatchUpdate> updates = new ArrayList<>(projects.size());
+    for (Project.NameKey project : projects) {
+      updates.add(getRepo(project).getUpdate());
+    }
+    return updates;
+  }
+
   @Override
   public void close() {
     for (OpenRepo repo : openRepos.values()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
index 284e9ed..8d847e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -261,11 +262,19 @@
         continue;
       }
       for (ChangeData topicCd : query().byTopicOpen(topic)) {
-        topicCd.changeControl(user);
-        if (topicCd.changeControl().isVisible(db, topicCd)) {
-          visibleChanges.add(topicCd);
-        } else {
-          nonVisibleChanges.add(topicCd);
+        try {
+          topicCd.changeControl(user);
+          if (topicCd.changeControl().isVisible(db, topicCd)) {
+            visibleChanges.add(topicCd);
+          } else {
+            nonVisibleChanges.add(topicCd);
+          }
+        } catch (OrmException e) {
+          if (e.getCause() instanceof NoSuchChangeException) {
+            // Ignore and skip this change
+          } else {
+            throw e;
+          }
         }
       }
       topicsSeen.add(topic);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 7c36961..89ec1d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -19,12 +19,15 @@
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -45,11 +48,13 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
+import org.eclipse.jgit.errors.AmbiguousObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.NoMergeBaseException;
 import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
@@ -60,6 +65,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.merge.Merger;
+import org.eclipse.jgit.merge.ResolveMerger;
 import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
 import org.eclipse.jgit.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.FooterKey;
@@ -202,6 +208,43 @@
     throw new MergeConflictException("merge conflict");
   }
 
+  public static RevCommit createMergeCommit(Repository repo, ObjectInserter inserter,
+      RevCommit mergeTip, RevCommit originalCommit, String mergeStrategy,
+      PersonIdent committerIndent, String commitMsg, RevWalk rw)
+      throws IOException, MergeIdenticalTreeException, MergeConflictException {
+
+    if (rw.isMergedInto(originalCommit, mergeTip)) {
+      throw new ChangeAlreadyMergedException(
+          "'" + originalCommit.getName() + "' has already been merged");
+    }
+
+    Merger m = newMerger(repo, inserter, mergeStrategy);
+    if (m.merge(false, mergeTip, originalCommit)) {
+      ObjectId tree = m.getResultTreeId();
+
+      CommitBuilder mergeCommit = new CommitBuilder();
+      mergeCommit.setTreeId(tree);
+      mergeCommit.setParentIds(mergeTip, originalCommit);
+      mergeCommit.setAuthor(committerIndent);
+      mergeCommit.setCommitter(committerIndent);
+      mergeCommit.setMessage(commitMsg);
+      return rw.parseCommit(inserter.insert(mergeCommit));
+    }
+    List<String> conflicts = ImmutableList.of();
+    if (m instanceof ResolveMerger) {
+      conflicts = ((ResolveMerger) m).getUnmergedPaths();
+    }
+    throw new MergeConflictException(createConflictMessage(conflicts));
+  }
+
+  public static String createConflictMessage(List<String> conflicts) {
+    StringBuilder sb = new StringBuilder("merge conflict(s)");
+    for (String c : conflicts) {
+      sb.append('\n' + c);
+    }
+    return sb.toString();
+  }
+
   public String createCherryPickCommitMessage(RevCommit n, ChangeControl ctl,
       PatchSet.Id psId) {
     Change c = ctl.getChange();
@@ -412,7 +455,10 @@
         m.setBase(toMerge.getParent(0));
         return m.merge(mergeTip, toMerge);
       } catch (IOException e) {
-        throw new IntegrationException("Cannot merge " + toMerge.name(), e);
+        throw new IntegrationException(
+            String.format("Cannot merge commit %s with mergetip %s",
+                toMerge.name(), mergeTip.name()),
+            e);
       }
     }
 
@@ -591,11 +637,17 @@
 
   public static ThreeWayMerger newThreeWayMerger(Repository repo,
       final ObjectInserter inserter, String strategyName) {
+    Merger m = newMerger(repo, inserter, strategyName);
+    checkArgument(m instanceof ThreeWayMerger,
+        "merge strategy %s does not support three-way merging", strategyName);
+    return (ThreeWayMerger) m;
+  }
+
+  public static Merger newMerger(Repository repo,
+      final ObjectInserter inserter, String strategyName) {
     MergeStrategy strategy = MergeStrategy.get(strategyName);
     checkArgument(strategy != null, "invalid merge strategy: %s", strategyName);
     Merger m = strategy.newMerger(repo, true);
-    checkArgument(m instanceof ThreeWayMerger,
-        "merge strategy %s does not support three-way merging", strategyName);
     m.setObjectInserter(new ObjectInserter.Filter() {
       @Override
       protected ObjectInserter delegate() {
@@ -610,7 +662,7 @@
       public void close() {
       }
     });
-    return (ThreeWayMerger) m;
+    return m;
   }
 
   public void markCleanMerges(final RevWalk rw,
@@ -693,4 +745,21 @@
     }
     return null;
   }
+
+  public static RevCommit resolveCommit(Repository repo, RevWalk rw, String str)
+      throws BadRequestException, ResourceNotFoundException, IOException {
+    try {
+      ObjectId commitId = repo.resolve(str);
+      if (commitId == null) {
+        throw new BadRequestException(
+            "Cannot resolve '" + str + "' to a commit");
+      }
+      return rw.parseCommit(commitId);
+    } catch (AmbiguousObjectException | IncorrectObjectTypeException |
+        RevisionSyntaxException e) {
+      throw new BadRequestException(e.getMessage());
+    } catch (MissingObjectException e) {
+      throw new ResourceNotFoundException(e.getMessage());
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
index 3ecc28a..2ccc849 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -152,14 +152,14 @@
     ChangeMessage msg = new ChangeMessage(
         new ChangeMessage.Key(change.getId(),
             ChangeUtil.messageUUID(ctx.getDb())),
-        ctx.getUser().getAccountId(), ctx.getWhen(), psId);
+        ctx.getAccountId(), ctx.getWhen(), psId);
     msg.setMessage(msgBuf.toString());
     cmUtil.addChangeMessage(ctx.getDb(), update, msg);
 
     PatchSetApproval submitter = new PatchSetApproval(
           new PatchSetApproval.Key(
               change.currentPatchSetId(),
-              ctx.getUser().getAccountId(),
+              ctx.getAccountId(),
               LabelId.legacySubmit()),
               (short) 1, ctx.getWhen());
     update.putApproval(submitter.getLabel(), submitter.getValue());
@@ -180,7 +180,7 @@
         try {
           MergedSender cm =
               mergedSenderFactory.create(ctx.getProject(), psId.getParentKey());
-          cm.setFrom(ctx.getUser().getAccountId());
+          cm.setFrom(ctx.getAccountId());
           cm.setPatchSet(patchSet, info);
           cm.send();
         } catch (Exception e) {
@@ -195,8 +195,9 @@
     }));
 
     changeMerged.fire(change, patchSet,
-        ctx.getUser().asIdentifiedUser().getAccount(),
-        patchSet.getRevision().get());
+        ctx.getAccount(),
+        patchSet.getRevision().get(),
+        ctx.getWhen());
   }
 
   private PatchSetInfo getPatchSetInfo(ChangeContext ctx) throws IOException {
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 6d9d759..64d9a9c 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
@@ -132,7 +132,8 @@
   private static final String KEY_STATE = "state";
 
   private static final String SUBSCRIBE_SECTION = "allowSuperproject";
-  private static final String SUBSCRIBE_REFS = "refs";
+  private static final String SUBSCRIBE_MATCH_REFS = "matching";
+  private static final String SUBSCRIBE_MULTI_MATCH_REFS = "all";
 
   private static final String DASHBOARD = "dashboard";
   private static final String KEY_DEFAULT = "default";
@@ -848,8 +849,12 @@
         Project.NameKey p = new Project.NameKey(projectName);
         SubscribeSection ss = new SubscribeSection(p);
         for (String s : rc.getStringList(SUBSCRIBE_SECTION,
-            projectName, SUBSCRIBE_REFS)) {
-          ss.addRefSpec(s);
+            projectName, SUBSCRIBE_MULTI_MATCH_REFS)) {
+          ss.addMultiMatchRefSpec(s);
+        }
+        for (String s : rc.getStringList(SUBSCRIBE_SECTION,
+            projectName, SUBSCRIBE_MATCH_REFS)) {
+          ss.addMatchingRefSpec(s);
         }
         subscribeSections.put(p, ss);
       }
@@ -1238,8 +1243,13 @@
   private void saveSubscribeSections(Config rc) {
     for (Project.NameKey p : subscribeSections.keySet()) {
       SubscribeSection s = subscribeSections.get(p);
-      for (RefSpec r : s.getRefSpecs()) {
-        rc.setString(SUBSCRIBE_SECTION, p.get(), SUBSCRIBE_REFS, r.toString());
+      for (RefSpec r : s.getMatchingRefSpecs()) {
+        rc.setString(SUBSCRIBE_SECTION, p.get(),
+            SUBSCRIBE_MATCH_REFS, r.toString());
+      }
+      for (RefSpec r : s.getMultiMatchRefSpecs()) {
+        rc.setString(SUBSCRIBE_SECTION, p.get(),
+            SUBSCRIBE_MULTI_MATCH_REFS, r.toString());
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 450c198..2f73360 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -59,7 +59,7 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -76,6 +76,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
@@ -99,6 +100,9 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.git.validators.RefOperationValidationException;
+import com.google.gerrit.server.git.validators.RefOperationValidators;
+import com.google.gerrit.server.git.validators.ValidationMessage;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
@@ -114,6 +118,7 @@
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.MagicBranch;
+import com.google.gerrit.server.util.RequestId;
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gwtorm.server.OrmException;
@@ -287,6 +292,7 @@
   private final ProjectCache projectCache;
   private final String canonicalWebUrl;
   private final CommitValidators.Factory commitValidatorsFactory;
+  private final RefOperationValidators.Factory refValidatorsFactory;
   private final TagCache tagCache;
   private final AccountCache accountCache;
   private final ChangeInserter.Factory changeInserterFactory;
@@ -306,6 +312,7 @@
   private final Repository repo;
   private final ReceivePack rp;
   private final NoteMap rejectCommits;
+  private final RequestId receiveId;
   private MagicBranchInput magicBranch;
   private boolean newChangeForAllNotInTarget;
 
@@ -326,7 +333,7 @@
   private final NotesMigration notesMigration;
   private final ChangeEditUtil editUtil;
 
-  private final List<CommitValidationMessage> messages = new ArrayList<>();
+  private final List<ValidationMessage> messages = new ArrayList<>();
   private ListMultimap<Error, String> errors = LinkedListMultimap.create();
   private Task newProgress;
   private Task replaceProgress;
@@ -352,6 +359,7 @@
       @Nullable SearchingChangeCacheImpl changeCache,
       ChangeInserter.Factory changeInserterFactory,
       CommitValidators.Factory commitValidatorsFactory,
+      RefOperationValidators.Factory refValidatorsFactory,
       @CanonicalWebUrl String canonicalWebUrl,
       RequestScopePropagator requestScopePropagator,
       SshInfo sshInfo,
@@ -389,6 +397,7 @@
     this.accountCache = accountCache;
     this.changeInserterFactory = changeInserterFactory;
     this.commitValidatorsFactory = commitValidatorsFactory;
+    this.refValidatorsFactory = refValidatorsFactory;
     this.requestScopePropagator = requestScopePropagator;
     this.sshInfo = sshInfo;
     this.allProjectsName = allProjectsName;
@@ -405,6 +414,7 @@
     this.repo = repo;
     this.rp = new ReceivePack(repo);
     this.rejectCommits = BanCommit.loadRejectCommitsMap(repo, rp.getRevWalk());
+    this.receiveId = RequestId.forProject(project.getNameKey());
 
     this.subOpFactory = subOpFactory;
     this.mergeOpProvider = mergeOpProvider;
@@ -540,7 +550,7 @@
   }
 
   void sendMessages() {
-    for (CommitValidationMessage m : messages) {
+    for (ValidationMessage m : messages) {
       if (m.isError()) {
         messageSender.sendError(m.getMessage());
       } else {
@@ -567,10 +577,12 @@
     }
     preparePatchSetsForReplace();
 
+    logDebug("Executing batch with {} commands", batch.getCommands().size());
     if (!batch.getCommands().isEmpty()) {
       try {
         if (!batch.isAllowNonFastForwards() && magicBranch != null
             && magicBranch.edit) {
+          logDebug("Allowing non-fast-forward for edit ref");
           batch.setAllowNonFastForwards(true);
         }
         batch.execute(rp.getRevWalk(), commandProgress);
@@ -582,7 +594,7 @@
             cnt++;
           }
         }
-        log.error(String.format(
+        logError(String.format(
             "Failed to store %d refs in %s", cnt, project.getName()), err);
       }
     }
@@ -592,6 +604,7 @@
     replaceProgress.end();
 
     if (!errors.isEmpty()) {
+      logDebug("Handling error conditions: {}", errors.keySet());
       for (Error error : errors.keySet()) {
         rp.sendMessage(buildError(error, errors.get(error)));
       }
@@ -601,54 +614,60 @@
 
     Set<Branch.NameKey> branches = new HashSet<>();
     for (ReceiveCommand c : batch.getCommands()) {
-        if (c.getResult() == OK) {
-          String refName = c.getRefName();
-          if (c.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward
-              tagCache.updateFastForward(project.getNameKey(),
-                  refName,
-                  c.getOldId(),
-                  c.getNewId());
-          }
+      if (c.getResult() == OK) {
+        String refName = c.getRefName();
+        if (c.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward
+          logDebug("Updating tag cache on fast-forward of {}", c.getRefName());
+          tagCache.updateFastForward(project.getNameKey(),
+              refName,
+              c.getOldId(),
+              c.getNewId());
+        }
 
-          if (isHead(c) || isConfig(c)) {
-            switch (c.getType()) {
-              case CREATE:
-              case UPDATE:
-              case UPDATE_NONFASTFORWARD:
-                autoCloseChanges(c);
-                branches.add(new Branch.NameKey(project.getNameKey(),
-                    refName));
-                break;
+        if (isHead(c) || isConfig(c)) {
+          switch (c.getType()) {
+            case CREATE:
+            case UPDATE:
+            case UPDATE_NONFASTFORWARD:
+              autoCloseChanges(c);
+              branches.add(new Branch.NameKey(project.getNameKey(),
+                  refName));
+              break;
 
-              case DELETE:
-                break;
-            }
-          }
-
-          if (isConfig(c)) {
-            projectCache.evict(project);
-            ProjectState ps = projectCache.get(project.getNameKey());
-            repoManager.setProjectDescription(project.getNameKey(), //
-                ps.getProject().getDescription());
-          }
-
-          if (!MagicBranch.isMagicBranch(refName)
-              && !refName.startsWith(REFS_CHANGES)) {
-            // We only fire gitRefUpdated for direct refs updates.
-            // Events for change refs are fired when they are created.
-            //
-            gitRefUpdated.fire(project.getNameKey(), c, user.getAccount());
+            case DELETE:
+              break;
           }
         }
+
+        if (isConfig(c)) {
+          logDebug("Reloading project in cache");
+          projectCache.evict(project);
+          ProjectState ps = projectCache.get(project.getNameKey());
+          repoManager.setProjectDescription(project.getNameKey(), //
+              ps.getProject().getDescription());
+        }
+
+        if (!MagicBranch.isMagicBranch(refName)
+            && !refName.startsWith(REFS_CHANGES)) {
+          logDebug("Firing ref update for {}", c.getRefName());
+          // We only fire gitRefUpdated for direct refs updates.
+          // Events for change refs are fired when they are created.
+          //
+          gitRefUpdated.fire(project.getNameKey(), c, user.getAccount());
+        } else {
+          logDebug("Assuming ref update event for {} has fired",
+              c.getRefName());
+        }
+      }
     }
 
     // Update superproject gitlinks if required.
     try (MergeOpRepoManager orm = ormProvider.get()) {
-      orm.setContext(db, TimeUtil.nowTs(), user, "receiveID");
-      SubmoduleOp op = subOpFactory.create(orm);
-      op.updateSuperProjects(branches);
+      orm.setContext(db, TimeUtil.nowTs(), user, receiveId);
+      SubmoduleOp op = subOpFactory.create(branches, orm);
+      op.updateSuperProjects();
     } catch (SubmoduleException e) {
-      log.error("Can't update the superprojects", e);
+      logError("Can't update the superprojects", e);
     }
 
     closeProgress.end();
@@ -669,8 +688,9 @@
       addMessage("");
       addMessage("New Changes:");
       for (CreateRequest c : created) {
-        addMessage(formatChangeUrl(canonicalWebUrl, c.change,
-            c.change.getSubject(), false));
+        addMessage(
+            formatChangeUrl(canonicalWebUrl, c.change, c.change.getSubject(),
+                c.change.getStatus() == Change.Status.DRAFT, false));
       }
       addMessage("");
     }
@@ -695,22 +715,36 @@
       addMessage("Updated Changes:");
       boolean edit = magicBranch != null && magicBranch.edit;
       for (ReplaceRequest u : updated) {
+        String subject;
+        if (edit) {
+          try {
+            subject =
+                rp.getRevWalk().parseCommit(u.newCommitId).getShortMessage();
+          } catch (IOException e) {
+            // Log and fall back to original change subject
+            logWarn("failed to get subject for edit patch set", e);
+            subject = u.notes.getChange().getSubject();
+          }
+        } else {
+          subject = u.info.getSubject();
+        }
         addMessage(formatChangeUrl(canonicalWebUrl, u.notes.getChange(),
-            u.info.getSubject(), edit));
+            subject, u.replaceOp != null && u.replaceOp.getPatchSet().isDraft(),
+            edit));
       }
       addMessage("");
     }
   }
 
   private static String formatChangeUrl(String url, Change change,
-      String subject, boolean edit) {
+      String subject, boolean draft, boolean edit) {
     StringBuilder m = new StringBuilder()
         .append("  ")
         .append(url)
         .append(change.getChangeId())
         .append(" ")
         .append(ChangeUtil.cropSubject(subject));
-    if (change.getStatus() == Change.Status.DRAFT) {
+    if (draft) {
       m.append(" [DRAFT]");
     }
     if (edit) {
@@ -732,30 +766,39 @@
           okToInsert++;
         }
       } else if (replace.cmd != null && replace.cmd.getResult() == OK) {
+        String refName = replace.inputCommand.getRefName();
         checkState(
-            NEW_PATCHSET.matcher(replace.inputCommand.getRefName()).matches(),
+            NEW_PATCHSET.matcher(refName).matches(),
             "expected a new patch set command as input when creating %s;"
                 + " got %s",
-            replace.cmd.getRefName(), replace.inputCommand.getRefName());
+            replace.cmd.getRefName(), refName);
         try {
+          logDebug("One-off insertion of patch set for {}", refName);
           replace.insertPatchSetWithoutBatchUpdate();
           replace.inputCommand.setResult(OK);
         } catch (IOException | UpdateException | RestApiException err) {
           reject(replace.inputCommand, "internal server error");
-          log.error(String.format(
+          logError(String.format(
               "Cannot add patch set to change %d in project %s",
               e.getKey().get(), project.getName()), err);
         }
       } else if (replace.inputCommand.getResult() == NOT_ATTEMPTED) {
         reject(replace.inputCommand, "internal server error");
-        log.error(String.format("Replacement for project %s was not attempted",
+        logError(String.format("Replacement for project %s was not attempted",
             project.getName()));
       }
     }
 
-    if (magicBranch == null || magicBranch.cmd.getResult() != NOT_ATTEMPTED) {
-      // refs/for/ or refs/drafts/ not used, or it already failed earlier.
-      // No need to continue.
+    // refs/for/ or refs/drafts/ not used, or it already failed earlier.
+    // No need to continue.
+    if (magicBranch == null) {
+      logDebug("No magic branch, nothing more to do");
+      return;
+    } else if (magicBranch.cmd.getResult() != NOT_ATTEMPTED) {
+      logWarn(String.format(
+          "Skipping change updates on %s because ref update failed: %s %s",
+          project.getName(), magicBranch.cmd.getResult(),
+          Strings.nullToEmpty(magicBranch.cmd.getMessage())));
       return;
     }
 
@@ -769,7 +812,7 @@
                 create.cmd.getResult(),
                 Strings.nullToEmpty(create.cmd.getMessage())).trim();
         lastCreateChangeErrors.add(createChangeResult);
-        log.error(String.format("Command %s on %s:%s not completed: %s",
+        logError(String.format("Command %s on %s:%s not completed: %s",
             create.cmd.getType(),
             project.getName(),
             create.cmd.getRefName(),
@@ -777,12 +820,15 @@
       }
     }
 
+    logDebug("Counted {} ok to insert, out of {} to replace and {} new",
+        okToInsert, replaceCount, newChanges.size());
+
     if (okToInsert != replaceCount + newChanges.size()) {
       // One or more new references failed to create. Assume the
       // system isn't working correctly anymore and abort.
       reject(magicBranch.cmd, "Unable to create changes: "
           + Joiner.on(' ').join(lastCreateChangeErrors));
-      log.error(String.format(
+      logError(String.format(
           "Only %d of %d new change refs created in %s; aborting",
           okToInsert, replaceCount + newChanges.size(), project.getName()));
       return;
@@ -793,6 +839,7 @@
         ObjectInserter ins = repo.newObjectInserter()) {
       bu.setRepository(repo, rp.getRevWalk(), ins)
           .updateChangesInParallel();
+      bu.setRequestId(receiveId);
       for (ReplaceRequest replace : replaceByChange.values()) {
         if (replace.inputCommand == magicBranch.cmd) {
           replace.addOps(bu, replaceProgress);
@@ -807,6 +854,7 @@
         update.addOps(bu);
       }
 
+      logDebug("Executing batch");
       try {
         bu.execute();
       } catch (UpdateException e) {
@@ -816,6 +864,7 @@
       for (ReplaceRequest replace : replaceByChange.values()) {
         String rejectMessage = replace.getRejectMessage();
         if (rejectMessage != null) {
+          logDebug("Rejecting due to message from ReplaceOp");
           reject(replace.inputCommand, rejectMessage);
         }
       }
@@ -824,7 +873,7 @@
       addMessage(e.getMessage());
       reject(magicBranch.cmd, "conflict");
     } catch (RestApiException | IOException err) {
-      log.error("Can't insert change/patch set for " + project.getName(), err);
+      logError("Can't insert change/patch set for " + project.getName(), err);
       reject(magicBranch.cmd, "internal server error: " + err.getMessage());
     }
 
@@ -835,7 +884,7 @@
         addMessage(e.getMessage());
         reject(magicBranch.cmd, "conflict");
       } catch (RestApiException | OrmException e) {
-        log.error("Error submit changes to " + project.getName(), e);
+        logError("Error submitting changes to " + project.getName(), e);
         reject(magicBranch.cmd, "error during submit");
       }
     }
@@ -866,10 +915,11 @@
   }
 
   private void parseCommands(Collection<ReceiveCommand> commands) {
+    logDebug("Parsing {} commands", commands.size());
     for (ReceiveCommand cmd : commands) {
       if (cmd.getResult() != NOT_ATTEMPTED) {
         // Already rejected by the core receive process.
-        //
+        logDebug("Already processed by core: {} {}", cmd.getResult(), cmd);
         continue;
       }
 
@@ -886,9 +936,12 @@
 
       if (projectControl.getProjectState().isAllUsers()
           && RefNames.REFS_USERS_SELF.equals(cmd.getRefName())) {
+        String newName = RefNames.refsUsers(user.getAccountId());
+        logDebug("Swapping out command for {} to {}",
+            RefNames.REFS_USERS_SELF, newName);
         final ReceiveCommand orgCmd = cmd;
-        cmd = new ReceiveCommand(cmd.getOldId(), cmd.getNewId(),
-            RefNames.refsUsers(user.getAccountId()), cmd.getType()) {
+        cmd = new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), newName,
+            cmd.getType()) {
           @Override
           public void setResult(Result s, String m) {
             super.setResult(s, m);
@@ -933,6 +986,7 @@
       }
 
       if (isConfig(cmd)) {
+        logDebug("Processing {} command", cmd.getRefName());
         if (!projectControl.isOwner()) {
           reject(cmd, "not project owner");
           continue;
@@ -951,7 +1005,7 @@
                   addError("  " + err.getMessage());
                 }
                 reject(cmd, "invalid project configuration");
-                log.error("User " + user.getUserName()
+                logError("User " + user.getUserName()
                     + " tried to push invalid project configuration "
                     + cmd.getNewId().name() + " for " + project.getName());
                 continue;
@@ -1012,7 +1066,7 @@
               }
             } catch (Exception e) {
               reject(cmd, "invalid project configuration");
-              log.error("User " + user.getUserName()
+              logError("User " + user.getUserName()
                   + " tried to push invalid project configuration "
                   + cmd.getNewId().name() + " for " + project.getName(), e);
               continue;
@@ -1035,19 +1089,22 @@
     try {
       obj = rp.getRevWalk().parseAny(cmd.getNewId());
     } catch (IOException err) {
-      log.error("Invalid object " + cmd.getNewId().name() + " for "
+      logError("Invalid object " + cmd.getNewId().name() + " for "
           + cmd.getRefName() + " creation", err);
       reject(cmd, "invalid object");
       return;
     }
+    logDebug("Creating {}", cmd);
 
     if (isHead(cmd) && !isCommit(cmd)) {
       return;
     }
 
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
-    rp.getRevWalk().reset();
-    if (ctl.canCreate(db, rp.getRevWalk(), obj)) {
+    if (ctl.canCreate(db, rp.getRepository(), obj)) {
+      if (!validRefOperation(cmd)) {
+        return;
+      }
       validateNewCommits(ctl, cmd);
       batch.addCommand(cmd);
     } else {
@@ -1056,12 +1113,16 @@
   }
 
   private void parseUpdate(ReceiveCommand cmd) {
+    logDebug("Updating {}", cmd);
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (ctl.canUpdate()) {
       if (isHead(cmd) && !isCommit(cmd)) {
         return;
       }
 
+      if (!validRefOperation(cmd)) {
+        return;
+      }
       validateNewCommits(ctl, cmd);
       batch.addCommand(cmd);
     } else {
@@ -1079,7 +1140,7 @@
     try {
       obj = rp.getRevWalk().parseAny(cmd.getNewId());
     } catch (IOException err) {
-      log.error("Invalid object " + cmd.getNewId().name() + " for "
+      logError("Invalid object " + cmd.getNewId().name() + " for "
           + cmd.getRefName(), err);
       reject(cmd, "invalid object");
       return false;
@@ -1093,11 +1154,15 @@
   }
 
   private void parseDelete(ReceiveCommand cmd) {
+    logDebug("Deleting {}", cmd);
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (ctl.getRefName().startsWith(REFS_CHANGES)) {
       errors.put(Error.DELETE_CHANGES, ctl.getRefName());
       reject(cmd, "cannot delete changes");
     } else if (ctl.canDelete()) {
+      if (!validRefOperation(cmd)) {
+        return;
+      }
       batch.addCommand(cmd);
     } else {
       if (RefNames.REFS_CONFIG.equals(ctl.getRefName())) {
@@ -1116,11 +1181,12 @@
     } catch (IncorrectObjectTypeException notCommit) {
       newObject = null;
     } catch (IOException err) {
-      log.error("Invalid object " + cmd.getNewId().name() + " for "
+      logError("Invalid object " + cmd.getNewId().name() + " for "
           + cmd.getRefName() + " forced update", err);
       reject(cmd, "invalid object");
       return;
     }
+    logDebug("Rewinding {}", cmd);
 
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (newObject != null) {
@@ -1131,6 +1197,9 @@
     }
 
     if (ctl.canForceUpdate()) {
+      if (!validRefOperation(cmd)) {
+        return;
+      }
       batch.setAllowNonFastForwards(true).addCommand(cmd);
     } else {
       cmd.setResult(REJECTED_NONFASTFORWARD, " need '"
@@ -1285,6 +1354,7 @@
       return;
     }
 
+    logDebug("Found magic branch {}", cmd.getRefName());
     magicBranch = new MagicBranchInput(cmd, labelTypes, notesMigration);
     magicBranch.reviewer.addAll(reviewersFromCommandLine);
     magicBranch.cc.addAll(ccFromCommandLine);
@@ -1296,6 +1366,7 @@
       ref = magicBranch.parse(clp, repo, rp.getAdvertisedRefs().keySet());
     } catch (CmdLineException e) {
       if (!clp.wasHelpRequestedByOption()) {
+        logDebug("Invalid branch syntax");
         reject(cmd, e.getMessage());
         return;
       }
@@ -1311,9 +1382,11 @@
     }
     if (projectControl.getProjectState().isAllUsers()
         && RefNames.REFS_USERS_SELF.equals(ref)) {
+      logDebug("Handling {}", RefNames.REFS_USERS_SELF);
       ref = RefNames.refsUsers(user.getAccountId());
     }
     if (!rp.getAdvertisedRefs().containsKey(ref) && !ref.equals(readHEAD(repo))) {
+      logDebug("Ref {} not found", ref);
       if (ref.startsWith(Constants.R_HEADS)) {
         String n = ref.substring(Constants.R_HEADS.length());
         reject(cmd, "branch " + n + " not found");
@@ -1364,9 +1437,10 @@
     RevCommit tip;
     try {
       tip = walk.parseCommit(magicBranch.cmd.getNewId());
+      logDebug("Tip of push: {}", tip.name());
     } catch (IOException ex) {
       magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
-      log.error("Invalid pack upload; one or more objects weren't sent", ex);
+      logError("Invalid pack upload; one or more objects weren't sent", ex);
       return;
     }
 
@@ -1375,10 +1449,12 @@
     if (tip.getParentCount() > 1
         || magicBranch.base != null
         || tip.getParentCount() == 0) {
+      logDebug("Forcing newChangeForAllNotInTarget = false");
       newChangeForAllNotInTarget = false;
     }
 
     if (magicBranch.base != null) {
+      logDebug("Handling %base: {}", magicBranch.base);
       magicBranch.baseCommit = Lists.newArrayListWithCapacity(
           magicBranch.base.size());
       for (ObjectId id : magicBranch.base) {
@@ -1391,7 +1467,7 @@
           reject(cmd, "base not found");
           return;
         } catch (IOException e) {
-          log.warn(String.format(
+          logWarn(String.format(
               "Project %s cannot read %s",
               project.getName(), id.name()), e);
           reject(cmd, "internal server error");
@@ -1399,6 +1475,7 @@
         }
       }
     } else if (newChangeForAllNotInTarget) {
+      logDebug("Handling newChangeForAllNotInTarget");
       String destBranch = magicBranch.dest.get();
       try {
         Ref r = repo.getRefDatabase().exactRef(destBranch);
@@ -1410,8 +1487,9 @@
         ObjectId baseHead = r.getObjectId();
         magicBranch.baseCommit =
             Collections.singletonList(walk.parseCommit(baseHead));
+        logDebug("Set baseCommit = {}", magicBranch.baseCommit.get(0).name());
       } catch (IOException ex) {
-        log.warn(String.format("Project %s cannot read %s", project.getName(),
+        logWarn(String.format("Project %s cannot read %s", project.getName(),
             destBranch), ex);
         reject(cmd, "internal server error");
         return;
@@ -1429,9 +1507,11 @@
         // The destination branch does not yet exist. Assume the
         // history being sent for review will start it and thus
         // is "connected" to the branch.
+        logDebug("Branch is unborn");
         return;
       }
       RevCommit h = walk.parseCommit(targetRef.getObjectId());
+      logDebug("Current branch tip: {}", h.name());
       RevFilter oldRevFilter = walk.getRevFilter();
       try {
         walk.reset();
@@ -1447,7 +1527,7 @@
       }
     } catch (IOException e) {
       magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
-      log.error("Invalid pack upload; one or more objects weren't sent", e);
+      logError("Invalid pack upload; one or more objects weren't sent", e);
     }
   }
 
@@ -1461,6 +1541,7 @@
   }
 
   private void parseReplaceCommand(ReceiveCommand cmd, Change.Id changeId) {
+    logDebug("Parsing replace command");
     if (cmd.getType() != ReceiveCommand.Type.CREATE) {
       reject(cmd, "invalid usage");
       return;
@@ -1469,8 +1550,9 @@
     RevCommit newCommit;
     try {
       newCommit = rp.getRevWalk().parseCommit(cmd.getNewId());
+      logDebug("Replacing with {}", newCommit);
     } catch (IOException e) {
-      log.error("Cannot parse " + cmd.getNewId().name() + " as commit", e);
+      logError("Cannot parse " + cmd.getNewId().name() + " as commit", e);
       reject(cmd, "invalid commit");
       return;
     }
@@ -1480,11 +1562,11 @@
       changeEnt = notesFactory.createChecked(db, project.getNameKey(), changeId)
           .getChange();
     } catch (OrmException e) {
-      log.error("Cannot lookup existing change " + changeId, e);
+      logError("Cannot lookup existing change " + changeId, e);
       reject(cmd, "database error");
       return;
     } catch (NoSuchChangeException e) {
-      log.error("Change not found " + changeId, e);
+      logError("Change not found " + changeId, e);
       reject(cmd, "change " + changeId + " not found");
       return;
     }
@@ -1493,6 +1575,7 @@
       return;
     }
 
+    logDebug("Replacing change {}", changeEnt.getId());
     requestReplace(cmd, true, changeEnt, newCommit);
   }
 
@@ -1514,10 +1597,11 @@
   }
 
   private void selectNewAndReplacedChangesFromMagicBranch() {
+    logDebug("Finding new and replaced changes");
     newChanges = new ArrayList<>();
 
     SetMultimap<ObjectId, Ref> existing = changeRefsById();
-    GroupCollector groupCollector = GroupCollector.create(refsById, db, psUtil,
+    GroupCollector groupCollector = GroupCollector.create(changeRefsById(), db, psUtil,
         notesFactory, project.getNameKey());
 
     rp.getRevWalk().reset();
@@ -1527,11 +1611,15 @@
       rp.getRevWalk().markStart(
           rp.getRevWalk().parseCommit(magicBranch.cmd.getNewId()));
       if (magicBranch.baseCommit != null) {
+        logDebug("Marking {} base commits uninteresting",
+            magicBranch.baseCommit.size());
         for (RevCommit c : magicBranch.baseCommit) {
           rp.getRevWalk().markUninteresting(c);
         }
         Ref targetRef = allRefs.get(magicBranch.ctl.getRefName());
         if (targetRef != null) {
+          logDebug("Marking target ref {} ({}) uninteresting",
+              magicBranch.ctl.getRefName(), targetRef.getObjectId().name());
           rp.getRevWalk().markUninteresting(
               rp.getRevWalk().parseCommit(targetRef.getObjectId()));
         }
@@ -1545,14 +1633,19 @@
       Set<Change.Key> newChangeIds = new HashSet<>();
       int maxBatchChanges =
           receiveConfig.getEffectiveMaxBatchChangesLimit(user);
+      int total = 0;
+      int alreadyTracked = 0;
       for (;;) {
         RevCommit c = rp.getRevWalk().next();
         if (c == null) {
           break;
         }
+        total++;
+        String name = c.name();
         groupCollector.visit(c);
         Collection<Ref> existingRefs = existing.get(c);
         if (!existingRefs.isEmpty()) { // Commit is already tracked.
+          alreadyTracked++;
           // Corner cases where an existing commit might need a new group:
           // A) Existing commit has a null group; wasn't assigned during schema
           //    upgrade, or schema upgrade is performed on a running server.
@@ -1564,16 +1657,23 @@
           //      B will be in existing so we aren't replacing the patch set. It
           //      used to have its own group, but now needs to to be changed to
           //      A's group.
+          // C) Commit is a PatchSet of a pre-existing change uploaded with a
+          //    different target branch.
           for (Ref ref : existingRefs) {
             updateGroups.add(new UpdateGroupsRequest(ref, c));
           }
-          continue;
+          if (!(newChangeForAllNotInTarget || magicBranch.base != null)) {
+            continue;
+          }
+          logDebug("Creating new change for {} even though it is already tracked",
+              name);
         }
 
         if (!validCommit(
             rp.getRevWalk(), magicBranch.ctl, 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");
           return;
         }
 
@@ -1582,6 +1682,9 @@
           reject(magicBranch.cmd,
               "Pushing merges in commit chains with 'all not in target' is not allowed,\n"
             + "to override please set the base manually");
+          logDebug("Rejecting merge commit {} with newChangeForAllNotInTarget",
+              name);
+          // TODO(dborowitz): Should we early return here?
         }
 
         List<String> idList = c.getFooterLines(CHANGE_ID);
@@ -1599,8 +1702,9 @@
         }
 
         pending.add(new ChangeLookup(c, new Change.Key(idStr)));
-        if (maxBatchChanges != 0
-            && pending.size() + newChanges.size() > maxBatchChanges) {
+        int n = pending.size() + newChanges.size();
+        if (maxBatchChanges != 0 && n > maxBatchChanges) {
+          logDebug("{} changes exceeds limit of {}", n, maxBatchChanges);
           reject(magicBranch.cmd,
               "the number of pushed changes in a batch exceeds the max limit "
                   + maxBatchChanges);
@@ -1608,9 +1712,15 @@
           return;
         }
       }
+      logDebug("Finished initial RevWalk with {} commits total: {} already"
+          + " tracked, {} new changes with no Change-Id, and {} deferred"
+          + " lookups", total, alreadyTracked, newChanges.size(),
+          pending.size());
 
-      for (ChangeLookup p : pending) {
+      for (Iterator<ChangeLookup> itr = pending.iterator(); itr.hasNext();) {
+        ChangeLookup p = itr.next();
         if (newChangeIds.contains(p.changeKey)) {
+          logDebug("Multiple commits with Change-Id {}", p.changeKey);
           reject(magicBranch.cmd, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
           newChanges = Collections.emptyList();
           return;
@@ -1618,6 +1728,15 @@
 
         List<ChangeData> changes = p.destChanges;
         if (changes.size() > 1) {
+          logDebug("Multiple changes in project with Change-Id {}: {}",
+              p.changeKey, Lists.transform(
+                  changes,
+                  new Function<ChangeData, String>() {
+                    @Override
+                    public String apply(ChangeData in) {
+                      return in.getId().toString();
+                    }
+                  }));
           // WTF, multiple changes in this project have the same key?
           // Since the commit is new, the user should recreate it with
           // a different Change-Id. In practice, we should never see
@@ -1631,6 +1750,21 @@
         if (changes.size() == 1) {
           // Schedule as a replacement to this one matching change.
           //
+
+          RevId currentPs = changes.get(0).currentPatchSet().getRevision();
+          // If Commit is already current PatchSet of target Change.
+          if (p.commit.name().equals(currentPs.get())) {
+            if (pending.size() == 1) {
+              // There are no commits left to check, all commits in pending were already
+              // current PatchSet of the corresponding target changes.
+              reject(magicBranch.cmd, "commit(s) already exists (as current patchset)");
+            } else {
+              // Commit is already current PatchSet.
+              // Remove from pending and try next commit.
+              itr.remove();
+              continue;
+            }
+          }
           if (requestReplace(
               magicBranch.cmd, false, changes.get(0).change(), p.commit)) {
             continue;
@@ -1650,16 +1784,18 @@
         }
         newChanges.add(new CreateRequest(p.commit, magicBranch.dest.get()));
       }
+      logDebug("Finished deferred lookups with {} updates and {} new changes",
+          replaceByChange.size(), newChanges.size());
     } catch (IOException e) {
       // Should never happen, the core receive process would have
       // identified the missing object earlier before we got control.
       //
       magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
-      log.error("Invalid pack upload; one or more objects weren't sent", e);
+      logError("Invalid pack upload; one or more objects weren't sent", e);
       newChanges = Collections.emptyList();
       return;
     } catch (OrmException e) {
-      log.error("Cannot query database to locate prior changes", e);
+      logError("Cannot query database to locate prior changes", e);
       reject(magicBranch.cmd, "database error");
       newChanges = Collections.emptyList();
       return;
@@ -1676,9 +1812,12 @@
 
     try {
       SortedSetMultimap<ObjectId, String> groups = groupCollector.getGroups();
-      for (CreateRequest create : newChanges) {
+      List<Integer> newIds = seq.nextChangeIds(newChanges.size());
+      for (int i = 0; i < newChanges.size(); i++) {
+        CreateRequest create = newChanges.get(i);
+        create.setChangeId(newIds.get(i));
         batch.addCommand(create.cmd);
-        create.groups = ImmutableList.copyOf(groups.get(create.commitId));
+        create.groups = ImmutableList.copyOf(groups.get(create.commit));
       }
       for (ReplaceRequest replace : replaceByChange.values()) {
         replace.groups = ImmutableList.copyOf(groups.get(replace.newCommitId));
@@ -1686,25 +1825,29 @@
       for (UpdateGroupsRequest update : updateGroups) {
         update.groups = ImmutableList.copyOf((groups.get(update.commit)));
       }
+      logDebug("Finished updating groups from GroupCollector");
     } catch (OrmException | NoSuchChangeException e) {
-      log.error("Error collecting groups for changes", e);
+      logError("Error collecting groups for changes", e);
       reject(magicBranch.cmd, "internal server error");
       return;
     }
   }
 
   private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) {
+    int i = 0;
     for (Ref ref : allRefs.values()) {
       if ((ref.getName().startsWith(R_HEADS) || ref.getName().equals(forRef))
           && ref.getObjectId() != null) {
         try {
           rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
+          i++;
         } catch (IOException e) {
-          log.warn(String.format("Invalid ref %s in %s",
+          logWarn(String.format("Invalid ref %s in %s",
               ref.getName(), project.getName()), e);
         }
       }
     }
+    logDebug("Marked {} heads as uninteresting", i);
   }
 
   private static boolean isValidChangeId(String idStr) {
@@ -1724,32 +1867,40 @@
   }
 
   private class CreateRequest {
-    final ObjectId commitId;
-    final ReceiveCommand cmd;
-    final ChangeInserter ins;
+    final RevCommit commit;
+    private final String refName;
+
     Change.Id changeId;
+    ReceiveCommand cmd;
+    ChangeInserter ins;
     List<String> groups = ImmutableList.of();
 
     Change change;
 
-    CreateRequest(RevCommit c, String refName)
-        throws OrmException {
-      commitId = c.copy();
-      changeId = new Change.Id(seq.nextChangeId());
-      ins = changeInserterFactory.create(changeId, c, refName)
+    CreateRequest(RevCommit commit, String refName) {
+      this.commit = commit;
+      this.refName = refName;
+    }
+
+    private void setChangeId(int id) {
+      changeId = new Change.Id(id);
+      ins = changeInserterFactory.create(changeId, commit, refName)
           .setDraft(magicBranch.draft)
           .setTopic(magicBranch.topic)
           // Changes already validated in validateNewCommits.
           .setValidatePolicy(CommitValidators.Policy.NONE);
-      cmd = new ReceiveCommand(ObjectId.zeroId(), c,
+      cmd = new ReceiveCommand(ObjectId.zeroId(), commit,
           ins.getPatchSetId().toRefName());
       ins.setUpdateRefCommand(cmd);
+      if (rp.getPushCertificate() != null) {
+        ins.setPushCertificate(rp.getPushCertificate().toTextWithSignature());
+      }
     }
 
     private void addOps(BatchUpdate bu) throws RestApiException {
+      checkState(changeId != null, "must call setChangeId before addOps");
       try {
         RevWalk rw = rp.getRevWalk();
-        RevCommit commit = rw.parseCommit(commitId);
         rw.parseBody(commit);
         final PatchSet.Id psId = ins.setGroups(groups).getPatchSetId();
         Account.Id me = user.getAccountId();
@@ -1760,12 +1911,13 @@
         recipients.add(magicBranch.getMailRecipients());
         approvals = magicBranch.labels;
         recipients.add(getRecipientsFromFooters(
-            accountResolver, magicBranch.draft, footerLines));
+            db, accountResolver, magicBranch.draft, footerLines));
         recipients.remove(me);
         StringBuilder msg = new StringBuilder(
             ApprovalsUtil.renderMessageWithApprovals(
                 psId.get(), approvals,
                 Collections.<String, PatchSetApproval> emptyMap()));
+        msg.append('.');
         if (!Strings.isNullOrEmpty(magicBranch.message)) {
           msg.append("\n").append(magicBranch.message);
         }
@@ -1783,7 +1935,7 @@
           bu.addOp(
               changeId,
               hashtagsFactory.create(new HashtagsInput(magicBranch.hashtags))
-                .setRunHooks(false));
+                .setFireEvent(false));
         }
         if (!Strings.isNullOrEmpty(magicBranch.topic)) {
           bu.addOp(
@@ -1818,7 +1970,7 @@
     for (CreateRequest r : create) {
       checkNotNull(r.change,
           "cannot submit new change %s; op may not have run", r.changeId);
-      bySha.put(r.commitId, r.change);
+      bySha.put(r.commit, r.change);
     }
     for (ReplaceRequest r : replace) {
       bySha.put(r.newCommitId, r.notes.getChange());
@@ -1827,6 +1979,8 @@
     checkNotNull(tipChange,
         "tip of push does not correspond to a change; found these changes: %s",
         bySha);
+    logDebug("Processing submit with tip change {} ({})",
+        tipChange.getId(), magicBranch.cmd.getNewId());
     try (MergeOp op  = mergeOpProvider.get()) {
       op.merge(db, tipChange, user, false, new SubmitInput());
     }
@@ -1846,7 +2000,7 @@
         }
       }
     } catch (OrmException err) {
-      log.error(String.format(
+      logError(String.format(
           "Cannot read database before replacement for project %s",
           project.getName()), err);
       for (ReplaceRequest req : replaceByChange.values()) {
@@ -1855,7 +2009,7 @@
         }
       }
     } catch (IOException err) {
-      log.error(String.format(
+      logError(String.format(
           "Cannot read repository before replacement for project %s",
           project.getName()), err);
       for (ReplaceRequest req : replaceByChange.values()) {
@@ -1864,6 +2018,7 @@
         }
       }
     }
+    logDebug("Read {} changes to replace", replaceByChange.size());
 
     for (ReplaceRequest req : replaceByChange.values()) {
       if (req.inputCommand.getResult() == NOT_ATTEMPTED && req.cmd != null) {
@@ -1935,7 +2090,7 @@
               rp.getRevWalk().parseCommit(ref.getObjectId()),
               PatchSet.Id.fromRef(ref.getName()));
         } catch (IOException err) {
-          log.warn(String.format(
+          logWarn(String.format(
               "Project %s contains invalid change ref %s",
               project.getName(), ref.getName()), err);
         }
@@ -1975,12 +2130,6 @@
 
       RevCommit newCommit = rp.getRevWalk().parseCommit(newCommitId);
       RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
-      if (newCommit.equals(priorCommit)) {
-        // Ignore requests to make the change its current state.
-        skip = true;
-        reject(inputCommand, "commit already exists (as current patchset)");
-        return false;
-      }
 
       changeCtl = projectControl.controlFor(notes);
       if (!changeCtl.canAddPatchSet(db)) {
@@ -1988,7 +2137,8 @@
         if (changeCtl.isPatchSetLocked(db)) {
           locked = ". Change is patch set locked.";
         }
-        reject(inputCommand, "cannot replace " + ontoChange + locked);
+        reject(inputCommand, "cannot add patch set to "
+            + ontoChange + locked);
         return false;
       } else if (notes.getChange().getStatus().isClosed()) {
         reject(inputCommand, "change " + ontoChange + " closed");
@@ -2071,7 +2221,7 @@
       try {
         edit = editUtil.byChange(changeCtl);
       } catch (AuthException | IOException e) {
-        log.error("Cannot retrieve edit", e);
+        logError("Cannot retrieve edit", e);
         return false;
       }
 
@@ -2152,6 +2302,7 @@
             projectControl.getProject().getNameKey(), user, TimeUtil.nowTs());
           ObjectInserter ins = repo.newObjectInserter()) {
         bu.setRepository(repo, rp.getRevWalk(), ins);
+        bu.setRequestId(receiveId);
         addOps(bu, replaceProgress);
         bu.execute();
       }
@@ -2235,7 +2386,7 @@
       return false;
     }
     for (int i = 0; i < a.getParentCount(); i++) {
-      if (a.getParent(i) != b.getParent(i)) {
+      if (!a.getParent(i).equals(b.getParent(i))) {
         return false;
       }
     }
@@ -2266,6 +2417,21 @@
     }
   }
 
+  private boolean validRefOperation(ReceiveCommand cmd) {
+    RefOperationValidators refValidators =
+        refValidatorsFactory.create(getProject(), user, cmd);
+
+    try {
+      messages.addAll(refValidators.validateForRefOperation());
+    } catch (RefOperationValidationException e) {
+      messages.addAll(Lists.newArrayList(e.getMessages()));
+      reject(cmd, e.getMessage());
+      return false;
+    }
+
+    return true;
+  }
+
   private void validateNewCommits(RefControl ctl, ReceiveCommand cmd) {
     if (ctl.canForgeAuthor()
         && ctl.canForgeCommitter()
@@ -2276,6 +2442,7 @@
         && !RefNames.REFS_CONFIG.equals(ctl.getRefName())
         && !(MagicBranch.isMagicBranch(cmd.getRefName())
             || NEW_PATCHSET.matcher(cmd.getRefName()).matches())) {
+      logDebug("Short-circuiting new commit validation");
       return;
     }
 
@@ -2289,11 +2456,13 @@
       if (!(parsedObject instanceof RevCommit)) {
         return;
       }
+      SetMultimap<ObjectId, Ref> existing = changeRefsById();
       walk.markStart((RevCommit)parsedObject);
       markHeadsAsUninteresting(walk, cmd.getRefName());
-      Set<ObjectId> existing = changeRefsById().keySet();
+      int i = 0;
       for (RevCommit c; (c = walk.next()) != null;) {
-        if (existing.contains(c)) {
+        i++;
+        if (existing.keySet().contains(c)) {
           continue;
         } else if (!validCommit(walk, ctl, cmd, c)) {
           break;
@@ -2310,15 +2479,16 @@
               accountCache.evict(a.getId());
             }
           } catch (OrmException e) {
-            log.warn("Cannot default full_name", e);
+            logWarn("Cannot default full_name", e);
           } finally {
             defaultName = false;
           }
         }
       }
+      logDebug("Validated {} new commits", i);
     } catch (IOException err) {
       cmd.setResult(REJECTED_MISSING_OBJECT);
-      log.error("Invalid pack upload; one or more objects weren't sent", err);
+      logError("Invalid pack upload; one or more objects weren't sent", err);
     }
   }
 
@@ -2340,6 +2510,7 @@
       messages.addAll(commitValidators.validateForReceiveCommits(
           receiveEvent, rejectCommits));
     } catch (CommitValidationException e) {
+      logDebug("Commit validation failed on {}", c.name());
       messages.addAll(e.getMessages());
       reject(cmd, e.getMessage());
       return false;
@@ -2349,6 +2520,7 @@
   }
 
   private void autoCloseChanges(final ReceiveCommand cmd) {
+    logDebug("Starting auto-closing of changes");
     String refName = cmd.getRefName();
     checkState(!MagicBranch.isMagicBranch(refName),
         "shouldn't be auto-closing changes on magic branch %s", refName);
@@ -2360,6 +2532,7 @@
         ObjectInserter ins = repo.newObjectInserter()) {
       bu.setRepository(repo, rp.getRevWalk(), ins)
           .updateChangesInParallel();
+      bu.setRequestId(receiveId);
       // TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
 
       RevCommit newTip = rw.parseCommit(cmd.getNewId());
@@ -2376,10 +2549,13 @@
       Map<Change.Key, ChangeNotes> byKey = null;
       List<ReplaceRequest> replaceAndClose = new ArrayList<>();
 
+      int existingPatchSets = 0;
+      int newPatchSets = 0;
       COMMIT: for (RevCommit c; (c = rw.next()) != null;) {
         rw.parseBody(c);
 
         for (Ref ref : byCommit.get(c.copy())) {
+          existingPatchSets++;
           PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
           bu.addOp(
               psId.getParentKey(),
@@ -2395,6 +2571,7 @@
 
           ChangeNotes onto = byKey.get(new Change.Key(changeId.trim()));
           if (onto != null) {
+            newPatchSets++;
             // Hold onto this until we're done with the walk, as the call to
             // req.validate below calls isMergedInto which resets the walk.
             ReplaceRequest req =
@@ -2409,6 +2586,7 @@
       for (final ReplaceRequest req : replaceAndClose) {
         Change.Id id = req.notes.getChangeId();
         if (!req.validate(true)) {
+          logDebug("Not closing {} because validation failed", id);
           continue;
         }
         req.addOps(bu, null);
@@ -2425,11 +2603,13 @@
         bu.addOp(id, new ChangeProgressOp(closeProgress));
       }
 
+      logDebug("Auto-closing {} changes with existing patch sets and {} with"
+          + " new patch sets", existingPatchSets, newPatchSets);
       bu.execute();
     } catch (RestApiException e) {
-      log.error("Can't insert patchset", e);
+      logError("Can't insert patchset", e);
     } catch (IOException | OrmException | UpdateException e) {
-      log.error("Can't scan for changes to close", e);
+      logError("Can't scan for changes to close", e);
     }
   }
 
@@ -2458,4 +2638,38 @@
   private static boolean isConfig(ReceiveCommand cmd) {
     return cmd.getRefName().equals(RefNames.REFS_CONFIG);
   }
+
+  private void logDebug(String msg, Object... args) {
+    if (log.isDebugEnabled()) {
+      log.debug(receiveId + msg, args);
+    }
+  }
+
+  private void logWarn(String msg, Throwable t) {
+    if (log.isWarnEnabled()) {
+      if (t != null) {
+        log.warn(receiveId + msg, t);
+      } else {
+        log.warn(receiveId + msg);
+      }
+    }
+  }
+
+  private void logWarn(String msg) {
+    logWarn(msg, null);
+  }
+
+  private void logError(String msg, Throwable t) {
+    if (log.isErrorEnabled()) {
+      if (t != null) {
+        log.error(receiveId + msg, t);
+      } else {
+        log.error(receiveId + msg);
+      }
+    }
+  }
+
+  private void logError(String msg) {
+    logError(msg, null);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index c0bfccb..a70fa7a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -239,6 +240,9 @@
     }
 
     boolean draft = magicBranch != null && magicBranch.draft;
+    if (change.getStatus() == Change.Status.DRAFT && !draft) {
+      update.setStatus(Change.Status.NEW);
+    }
     newPatchSet = psUtil.insert(
         ctx.getDb(), ctx.getRevWalk(), update, patchSetId, commit, draft, groups,
         pushCertificate != null
@@ -246,26 +250,30 @@
           : null);
 
     recipients.add(getRecipientsFromFooters(
-        accountResolver, draft, commit.getFooterLines()));
-    recipients.remove(ctx.getUser().getAccountId());
+        ctx.getDb(), accountResolver, draft, commit.getFooterLines()));
+    recipients.remove(ctx.getAccountId());
     ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getControl());
     MailRecipients oldRecipients =
         getRecipientsFromReviewers(cd.reviewers());
-    approvalCopier.copy(ctx.getDb(), ctx.getControl(), newPatchSet);
+    Iterable<PatchSetApproval> newApprovals =
+        approvalsUtil.addApprovals(ctx.getDb(), update,
+            projectControl.getLabelTypes(), newPatchSet, ctx.getControl(),
+            approvals);
+    approvalCopier.copy(ctx.getDb(), ctx.getControl(), newPatchSet,
+        newApprovals);
     approvalsUtil.addReviewers(ctx.getDb(), update,
         projectControl.getLabelTypes(), change, newPatchSet, info,
         recipients.getReviewers(), oldRecipients.getAll());
-    approvalsUtil.addApprovals(ctx.getDb(), update,
-        projectControl.getLabelTypes(), newPatchSet, ctx.getControl(),
-        approvals);
     recipients.add(oldRecipients);
 
     String approvalMessage = ApprovalsUtil.renderMessageWithApprovals(
         patchSetId.get(), approvals, scanLabels(ctx, approvals));
-    StringBuilder message = new StringBuilder(approvalMessage);
     String kindMessage = changeKindMessage(changeKind);
+    StringBuilder message = new StringBuilder(approvalMessage);
     if (!Strings.isNullOrEmpty(kindMessage)) {
       message.append(kindMessage);
+    } else {
+      message.append('.');
     }
     if (!Strings.isNullOrEmpty(reviewMessage)) {
       message.append("\n").append(reviewMessage);
@@ -273,7 +281,7 @@
     msg = new ChangeMessage(
         new ChangeMessage.Key(change.getId(),
             ChangeUtil.messageUUID(ctx.getDb())),
-        ctx.getUser().getAccountId(), ctx.getWhen(), patchSetId);
+        ctx.getAccountId(), ctx.getWhen(), patchSetId);
     msg.setMessage(message.toString());
     cmUtil.addChangeMessage(ctx.getDb(), update, msg);
 
@@ -292,9 +300,9 @@
       case MERGE_FIRST_PARENT_UPDATE:
       case TRIVIAL_REBASE:
       case NO_CHANGE:
-        return ": Patch Set " + priorPatchSetId.get() + " was rebased";
+        return ": Patch Set " + priorPatchSetId.get() + " was rebased.";
       case NO_CODE_CHANGE:
-        return ": Commit message was updated";
+        return ": Commit message was updated.";
       case REWORK:
       default:
         return null;
@@ -308,7 +316,7 @@
     if (!approvals.isEmpty()) {
       for (PatchSetApproval a : approvalsUtil.byPatchSetUser(ctx.getDb(),
           ctx.getControl(), priorPatchSetId,
-          ctx.getUser().getAccountId())) {
+          ctx.getAccountId())) {
         if (a.isLegacySubmit()) {
           continue;
         }
@@ -360,7 +368,7 @@
     // special because its ref is actually updated by ReceiveCommits, so from
     // BatchUpdate's perspective there is no ref update. Thus we have to fire it
     // manually.
-    Account account = ctx.getUser().asIdentifiedUser().getAccount();
+    final Account account = ctx.getAccount();
     gitRefUpdated.fire(ctx.getProject(), newPatchSet.getRefName(),
         ObjectId.zeroId(), commit, account);
 
@@ -371,9 +379,9 @@
           try {
             ReplacePatchSetSender cm = replacePatchSetFactory.create(
                 projectControl.getProject().getNameKey(), change.getId());
-            cm.setFrom(ctx.getUser().getAccountId());
+            cm.setFrom(account.getId());
             cm.setPatchSet(newPatchSet, info);
-            cm.setChangeMessage(msg);
+            cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
             if (magicBranch != null && magicBranch.notify != null) {
               cm.setNotify(magicBranch.notify);
             }
@@ -398,7 +406,11 @@
       }
     }
 
-    revisionCreated.fire(change, newPatchSet, ctx.getUser().getAccountId());
+    NotifyHandling notify = magicBranch != null && magicBranch.notify != null
+        ? magicBranch.notify
+        : NotifyHandling.ALL;
+    revisionCreated.fire(change, newPatchSet, ctx.getAccountId(),
+        ctx.getWhen(), notify);
     try {
       fireCommentAddedEvent(ctx);
     } catch (Exception e) {
@@ -437,7 +449,7 @@
     }
 
     commentAdded.fire(change, newPatchSet,
-        ctx.getUser().asIdentifiedUser().getAccount(), null,
+        ctx.getAccount(), null,
         allApprovals, oldApprovals, ctx.getWhen());
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java
index d7e8446..5a3b4ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.git;
 
 /** Indicates the gitlink's update cannot be processed at this time. */
-class SubmoduleException extends Exception {
+public class SubmoduleException extends Exception {
   private static final long serialVersionUID = 1L;
 
   SubmoduleException(final String msg) {
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 edaed13..4795a31 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
@@ -15,17 +15,20 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Multimap;
-import com.google.gerrit.common.Nullable;
+import com.google.common.collect.SetMultimap;
 import com.google.gerrit.common.data.SubscribeSection;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.extensions.restapi.RestApiException;
 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.client.SubmoduleSubscription;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.config.VerboseSuperprojectUpdate;
+import com.google.gerrit.server.git.BatchUpdate.Listener;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
 import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -39,98 +42,231 @@
 import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
 import org.eclipse.jgit.dircache.DirCacheEntry;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.RefSpec;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 public class SubmoduleOp {
+
+  /**
+   * Only used for branches without code review changes
+   */
+  public class GitlinkOp extends BatchUpdate.RepoOnlyOp {
+    private final Branch.NameKey branch;
+
+    GitlinkOp(Branch.NameKey branch) {
+      this.branch = branch;
+    }
+
+    @Override
+    public void updateRepo(RepoContext ctx) throws Exception {
+      CodeReviewCommit c = composeGitlinksCommit(branch);
+      if (c != null) {
+        ctx.addRefUpdate(new ReceiveCommand(c.getParent(0), c, branch.get()));
+        addBranchTip(branch, c);
+      }
+    }
+  }
+
   public interface Factory {
-    SubmoduleOp create(MergeOpRepoManager orm);
+    SubmoduleOp create(
+        Set<Branch.NameKey> updatedBranches, MergeOpRepoManager orm);
   }
 
   private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class);
 
   private final GitModules.Factory gitmodulesFactory;
   private final PersonIdent myIdent;
-  private final GitReferenceUpdated gitRefUpdated;
   private final ProjectCache projectCache;
   private final ProjectState.Factory projectStateFactory;
-  private final Account account;
-  private final boolean verboseSuperProject;
+  private final VerboseSuperprojectUpdate verboseSuperProject;
   private final boolean enableSuperProjectSubscriptions;
+  private final Multimap<Branch.NameKey, SubmoduleSubscription> targets;
+  private final Set<Branch.NameKey> updatedBranches;
   private final MergeOpRepoManager orm;
+  private final Map<Branch.NameKey, CodeReviewCommit> branchTips;
+  private final Map<Branch.NameKey, GitModules> branchGitModules;
+  private final ImmutableSet<Branch.NameKey> sortedBranches;
 
   @AssistedInject
   public SubmoduleOp(
       GitModules.Factory gitmodulesFactory,
       @GerritPersonIdent PersonIdent myIdent,
       @GerritServerConfig Config cfg,
-      GitReferenceUpdated gitRefUpdated,
       ProjectCache projectCache,
       ProjectState.Factory projectStateFactory,
-      @Nullable Account account,
-      @Assisted MergeOpRepoManager orm) {
+      @Assisted Set<Branch.NameKey> updatedBranches,
+      @Assisted MergeOpRepoManager orm) throws SubmoduleException {
     this.gitmodulesFactory = gitmodulesFactory;
     this.myIdent = myIdent;
-    this.gitRefUpdated = gitRefUpdated;
     this.projectCache = projectCache;
     this.projectStateFactory = projectStateFactory;
-    this.account = account;
-    this.verboseSuperProject = cfg.getBoolean("submodule",
-        "verboseSuperprojectUpdate", true);
+    this.verboseSuperProject =
+        cfg.getEnum("submodule", null, "verboseSuperprojectUpdate",
+            VerboseSuperprojectUpdate.TRUE);
     this.enableSuperProjectSubscriptions = cfg.getBoolean("submodule",
         "enableSuperProjectSubscriptions", true);
     this.orm = orm;
+    this.updatedBranches = updatedBranches;
+    this.targets = HashMultimap.create();
+    this.branchTips = new HashMap<>();
+    this.branchGitModules = new HashMap<>();
+    this.sortedBranches = calculateSubscriptionMap();
   }
 
-  public Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src,
+  private ImmutableSet<Branch.NameKey> calculateSubscriptionMap()
+      throws SubmoduleException {
+    if (!enableSuperProjectSubscriptions) {
+      logDebug("Updating superprojects disabled");
+      return null;
+    }
+
+    logDebug("Calculating superprojects - submodules map");
+    LinkedHashSet<Branch.NameKey> allVisited = new LinkedHashSet<>();
+    for (Branch.NameKey updatedBranch : updatedBranches) {
+      if (allVisited.contains(updatedBranch)) {
+        continue;
+      }
+
+      searchForSuperprojects(updatedBranch, new LinkedHashSet<Branch.NameKey>(),
+          allVisited);
+    }
+
+    // Since the searchForSuperprojects will add the superprojects before one
+    // submodule in sortedBranches, need reverse the order of it
+    reverse(allVisited);
+    return ImmutableSet.copyOf(allVisited);
+  }
+
+  private void searchForSuperprojects(Branch.NameKey current,
+      LinkedHashSet<Branch.NameKey> currentVisited,
+      LinkedHashSet<Branch.NameKey> allVisited)
+      throws SubmoduleException {
+    logDebug("Now processing " + current);
+
+    if (currentVisited.contains(current)) {
+      throw new SubmoduleException(
+          "Branch level circular subscriptions detected:  " +
+              printCircularPath(currentVisited, current));
+    }
+
+    if (allVisited.contains(current)) {
+      return;
+    }
+
+    currentVisited.add(current);
+    try {
+      Collection<SubmoduleSubscription> subscriptions =
+          superProjectSubscriptionsForSubmoduleBranch(current);
+      for (SubmoduleSubscription sub : subscriptions) {
+        Branch.NameKey superProject = sub.getSuperProject();
+        searchForSuperprojects(superProject, currentVisited, allVisited);
+        targets.put(superProject, sub);
+      }
+    } catch (IOException e) {
+      throw new SubmoduleException("Cannot find superprojects for " + current,
+          e);
+    }
+    currentVisited.remove(current);
+    allVisited.add(current);
+  }
+
+  private static <T> void reverse(LinkedHashSet<T> set) {
+    if (set == null) {
+      return;
+    }
+
+    Deque<T> q = new ArrayDeque<>(set);
+    set.clear();
+
+    while (!q.isEmpty()) {
+      set.add(q.removeLast());
+    }
+  }
+
+  private <T> String printCircularPath(LinkedHashSet<T> p, T target) {
+    StringBuilder sb = new StringBuilder();
+    sb.append(target);
+    ArrayList<T> reverseP = new ArrayList<>(p);
+    Collections.reverse(reverseP);
+    for (T t : reverseP) {
+      sb.append("->");
+      sb.append(t);
+      if (t.equals(target)) {
+        break;
+      }
+    }
+    return sb.toString();
+  }
+
+  private Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src,
       SubscribeSection s) throws IOException {
-    Collection<Branch.NameKey> ret = new ArrayList<>();
+    Collection<Branch.NameKey> ret = new HashSet<>();
     logDebug("Inspecting SubscribeSection " + s);
-    for (RefSpec r : s.getRefSpecs()) {
-      logDebug("Inspecting ref " + r);
-      if (r.matchSource(src.get())) {
-        if (r.getDestination() == null) {
-          // no need to care for wildcard, as we matched already
-          try {
-            orm.openRepo(s.getProject(), false);
-          } catch (NoSuchProjectException e) {
-            // A project listed a non existent project to be allowed
-            // to subscribe to it. Allow this for now.
-            continue;
-          }
-          OpenRepo or = orm.getRepo(s.getProject());
-          for (Ref ref : or.repo.getRefDatabase().getRefs(
-              RefNames.REFS_HEADS).values()) {
-            ret.add(new Branch.NameKey(s.getProject(), ref.getName()));
-          }
-        } else if (r.isWildcard()) {
-          // refs/heads/*:refs/heads/*
-          ret.add(new Branch.NameKey(s.getProject(),
-              r.expandFromSource(src.get()).getDestination()));
-        } else {
-          // e.g. refs/heads/master:refs/heads/stable
-          ret.add(new Branch.NameKey(s.getProject(), r.getDestination()));
+    for (RefSpec r : s.getMatchingRefSpecs()) {
+      logDebug("Inspecting [matching] ref " + r);
+      if (!r.matchSource(src.get())) {
+        continue;
+      }
+      if (r.isWildcard()) {
+        // refs/heads/*[:refs/somewhere/*]
+        ret.add(new Branch.NameKey(s.getProject(),
+            r.expandFromSource(src.get()).getDestination()));
+      } else {
+        // e.g. refs/heads/master[:refs/heads/stable]
+        String dest = r.getDestination();
+        if (dest == null) {
+          dest = r.getSource();
+        }
+        ret.add(new Branch.NameKey(s.getProject(), dest));
+      }
+    }
+
+    for (RefSpec r : s.getMultiMatchRefSpecs()) {
+      logDebug("Inspecting [all] ref " + r);
+      if (!r.matchSource(src.get())) {
+        continue;
+      }
+      OpenRepo or;
+      try {
+        or = orm.openRepo(s.getProject(), false);
+      } catch (NoSuchProjectException e) {
+        // A project listed a non existent project to be allowed
+        // to subscribe to it. Allow this for now, i.e. no exception is
+        // thrown.
+        continue;
+      }
+
+      for (Ref ref : or.repo.getRefDatabase().getRefs(
+          RefNames.REFS_HEADS).values()) {
+        if (r.getDestination() != null && !r.matchDestination(ref.getName())) {
+          continue;
+        }
+        Branch.NameKey b = new Branch.NameKey(s.getProject(), ref.getName());
+        if (!ret.contains(b)) {
+          ret.add(b);
         }
       }
     }
@@ -154,8 +290,7 @@
       for (Branch.NameKey targetBranch : branches) {
         Project.NameKey targetProject = targetBranch.getParentKey();
         try {
-          orm.openRepo(targetProject, false);
-          OpenRepo or = orm.getRepo(targetProject);
+          OpenRepo or = orm.openRepo(targetProject, false);
           ObjectId id = or.repo.resolve(targetBranch.get());
           if (id == null) {
             logDebug("The branch " + targetBranch + " doesn't exist.");
@@ -165,242 +300,290 @@
           logDebug("The project " + targetProject + " doesn't exist");
           continue;
         }
-        GitModules m = gitmodulesFactory.create(targetBranch, orm);
-        for (SubmoduleSubscription ss : m.subscribedTo(srcBranch)) {
-          logDebug("Checking SubmoduleSubscription " + ss);
-          if (projectCache.get(ss.getSubmodule().getParentKey()) != null) {
-            logDebug("Adding SubmoduleSubscription " + ss);
-            ret.add(ss);
-          }
+
+        GitModules m = branchGitModules.get(targetBranch);
+        if (m == null) {
+          m = gitmodulesFactory.create(targetBranch, orm);
+          branchGitModules.put(targetBranch, m);
         }
+        ret.addAll(m.subscribedTo(srcBranch));
       }
     }
     logDebug("Calculated superprojects for " + srcBranch + " are " + ret);
     return ret;
   }
 
-  protected void updateSuperProjects(Collection<Branch.NameKey> updatedBranches)
-      throws SubmoduleException {
-    if (!enableSuperProjectSubscriptions) {
-      logDebug("Updating superprojects disabled");
+  public void updateSuperProjects() throws SubmoduleException {
+    ImmutableSet<Project.NameKey> projects = getProjectsInOrder();
+    if (projects == null) {
       return;
     }
-    logDebug("Updating superprojects");
 
-    Multimap<Branch.NameKey, SubmoduleSubscription> targets =
-        HashMultimap.create();
-
-    for (Branch.NameKey updatedBranch : updatedBranches) {
-      logDebug("Now processing " + updatedBranch);
-      Set<Branch.NameKey> checkedTargets = new HashSet<>();
-      Set<Branch.NameKey> targetsToProcess = new HashSet<>();
-      targetsToProcess.add(updatedBranch);
-
-      while (!targetsToProcess.isEmpty()) {
-        Set<Branch.NameKey> newTargets = new HashSet<>();
-        for (Branch.NameKey b : targetsToProcess) {
-          try {
-            Collection<SubmoduleSubscription> subs =
-                superProjectSubscriptionsForSubmoduleBranch(b);
-            for (SubmoduleSubscription sub : subs) {
-              Branch.NameKey dst = sub.getSuperProject();
-              targets.put(dst, sub);
-              newTargets.add(dst);
-            }
-          } catch (IOException e) {
-            throw new SubmoduleException("Cannot find superprojects for " + b, e);
+    SetMultimap<Project.NameKey, Branch.NameKey> dst = branchesByProject();
+    LinkedHashSet<Project.NameKey> superProjects = new LinkedHashSet<>();
+    try {
+      for (Project.NameKey project : projects) {
+        // only need superprojects
+        if (dst.containsKey(project)) {
+          superProjects.add(project);
+          // get a new BatchUpdate for the super project
+          OpenRepo or = orm.openRepo(project, false);
+          for (Branch.NameKey branch : dst.get(project)) {
+            addOp(or.getUpdate(), branch);
           }
         }
-        logDebug("adding to done " + targetsToProcess);
-        checkedTargets.addAll(targetsToProcess);
-        logDebug("completely done with " + checkedTargets);
-
-        Set<Branch.NameKey> intersection = new HashSet<>(checkedTargets);
-        intersection.retainAll(newTargets);
-        if (!intersection.isEmpty()) {
-          throw new SubmoduleException(
-              "Possible circular subscription involving " + updatedBranch);
-        }
-
-        targetsToProcess = newTargets;
       }
-    }
-
-    for (Branch.NameKey dst : targets.keySet()) {
-      try {
-        updateGitlinks(dst, targets.get(dst));
-      } catch (SubmoduleException e) {
-        throw new SubmoduleException("Cannot update gitlinks for " + dst, e);
-      }
+      BatchUpdate.execute(orm.batchUpdates(superProjects), Listener.NONE,
+          orm.getSubmissionId());
+    } catch (RestApiException | UpdateException | IOException |
+        NoSuchProjectException e) {
+      throw new SubmoduleException("Cannot update gitlinks", e);
     }
   }
 
   /**
-   * Update the submodules in one branch of one repository.
-   *
-   * @param subscriber the branch of the repository which should be changed.
-   * @param updates submodule updates which should be updated to.
-   * @throws SubmoduleException
+   * Create a separate gitlink commit
    */
-  private void updateGitlinks(Branch.NameKey subscriber,
-      Collection<SubmoduleSubscription> updates)
-          throws SubmoduleException {
-    PersonIdent author = null;
-    StringBuilder msgbuf = new StringBuilder("Update git submodules\n\n");
-    boolean sameAuthorForAll = true;
-
+  public CodeReviewCommit composeGitlinksCommit(final Branch.NameKey subscriber)
+      throws IOException, SubmoduleException {
+    OpenRepo or;
     try {
-      orm.openRepo(subscriber.getParentKey(), false);
+      or = orm.openRepo(subscriber.getParentKey(), false);
     } catch (NoSuchProjectException | IOException e) {
       throw new SubmoduleException("Cannot access superproject", e);
     }
-    OpenRepo or = orm.getRepo(subscriber.getParentKey());
-    try {
-      Ref r = or.repo.exactRef(subscriber.get());
-      if (r == null) {
-        throw new SubmoduleException(
-            "The branch was probably deleted from the subscriber repository");
-      }
 
-      DirCache dc = readTree(r, or.rw);
-      DirCacheEditor ed = dc.editor();
+    CodeReviewCommit currentCommit;
+    Ref r = or.repo.exactRef(subscriber.get());
+    if (r == null) {
+      throw new SubmoduleException(
+          "The branch was probably deleted from the subscriber repository");
+    }
+    currentCommit = or.rw.parseCommit(r.getObjectId());
 
-      for (SubmoduleSubscription s : updates) {
-        try {
-          orm.openRepo(s.getSubmodule().getParentKey(), false);
-        } catch (NoSuchProjectException | IOException e) {
-          throw new SubmoduleException("Cannot access submodule", e);
-        }
-        OpenRepo subOr = orm.getRepo(s.getSubmodule().getParentKey());
-        Repository subrepo = subOr.repo;
-
-        Ref ref = subrepo.getRefDatabase().exactRef(s.getSubmodule().get());
-        if (ref == null) {
-          ed.add(new DeletePath(s.getPath()));
-          continue;
-        }
-
-        final ObjectId updateTo = ref.getObjectId();
-        RevCommit newCommit = subOr.rw.parseCommit(updateTo);
-
-        subOr.rw.parseBody(newCommit);
+    StringBuilder msgbuf = new StringBuilder("");
+    PersonIdent author = null;
+    DirCache dc = readTree(or.rw, currentCommit);
+    DirCacheEditor ed = dc.editor();
+    for (SubmoduleSubscription s : targets.get(subscriber)) {
+      RevCommit newCommit = updateSubmodule(dc, ed, msgbuf, s);
+      if (newCommit != null) {
         if (author == null) {
           author = newCommit.getAuthorIdent();
         } else if (!author.equals(newCommit.getAuthorIdent())) {
-          sameAuthorForAll = false;
-        }
-
-        DirCacheEntry dce = dc.getEntry(s.getPath());
-        ObjectId oldId;
-        if (dce != null) {
-          if (!dce.getFileMode().equals(FileMode.GITLINK)) {
-            log.error("Requested to update gitlink " + s.getPath() + " in "
-                + s.getSubmodule().getParentKey().get() + " but entry "
-                + "doesn't have gitlink file mode.");
-            continue;
-          }
-          oldId = dce.getObjectId();
-        } else {
-          // This submodule did not exist before. We do not want to add
-          // the full submodule history to the commit message, so omit it.
-          oldId = updateTo;
-        }
-
-        ed.add(new PathEdit(s.getPath()) {
-          @Override
-          public void apply(DirCacheEntry ent) {
-            ent.setFileMode(FileMode.GITLINK);
-            ent.setObjectId(updateTo);
-          }
-        });
-        if (verboseSuperProject) {
-          msgbuf.append("Project: " + s.getSubmodule().getParentKey().get());
-          msgbuf.append(" " + s.getSubmodule().getShortName());
-          msgbuf.append(" " + updateTo.getName());
-          msgbuf.append("\n\n");
-
-          try {
-            subOr.rw.resetRetain(subOr.canMergeFlag);
-            subOr.rw.markStart(newCommit);
-            subOr.rw.markUninteresting(subOr.rw.parseCommit(oldId));
-            for (RevCommit c : subOr.rw) {
-              subOr.rw.parseBody(c);
-              msgbuf.append(c.getFullMessage() + "\n\n");
-            }
-          } catch (IOException e) {
-            throw new SubmoduleException("Could not perform a revwalk to "
-                + "create superproject commit message", e);
-          }
+          author = myIdent;
         }
       }
-      ed.finish();
+    }
+    ed.finish();
+    ObjectId newTreeId = dc.writeTree(or.ins);
 
-      if (!sameAuthorForAll || author == null) {
-        author = myIdent;
+    // Gitlinks are already in the branch, return null
+    if (newTreeId.equals(currentCommit.getTree())) {
+      return null;
+    }
+    CommitBuilder commit = new CommitBuilder();
+    commit.setTreeId(newTreeId);
+    commit.setParentId(currentCommit);
+    StringBuilder commitMsg = new StringBuilder("Update git submodules\n\n");
+    if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) {
+      commitMsg.append(msgbuf);
+    }
+    commit.setMessage(commitMsg.toString());
+    commit.setAuthor(author);
+    commit.setCommitter(myIdent);
+    ObjectId id = or.ins.insert(commit);
+    return or.rw.parseCommit(id);
+  }
+
+  /**
+   * Amend an existing commit with gitlink updates
+   */
+  public CodeReviewCommit composeGitlinksCommit(
+      final Branch.NameKey subscriber, CodeReviewCommit currentCommit)
+      throws IOException, SubmoduleException {
+    OpenRepo or;
+    try {
+      or = orm.openRepo(subscriber.getParentKey(), false);
+    } catch (NoSuchProjectException | IOException e) {
+      throw new SubmoduleException("Cannot access superproject", e);
+    }
+
+    StringBuilder msgbuf = new StringBuilder("");
+    DirCache dc = readTree(or.rw, currentCommit);
+    DirCacheEditor ed = dc.editor();
+    for (SubmoduleSubscription s : targets.get(subscriber)) {
+      updateSubmodule(dc, ed, msgbuf, s);
+    }
+    ed.finish();
+    ObjectId newTreeId = dc.writeTree(or.ins);
+
+    // Gitlinks are already updated, just return the commit
+    if (newTreeId.equals(currentCommit.getTree())) {
+      return currentCommit;
+    }
+    or.rw.parseBody(currentCommit);
+    CommitBuilder commit = new CommitBuilder();
+    commit.setTreeId(newTreeId);
+    commit.setParentIds(currentCommit.getParents());
+    if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) {
+      //TODO:czhen handle cherrypick footer
+      commit.setMessage(
+          currentCommit.getFullMessage() + "\n\n*submodules:\n" + msgbuf.toString());
+    } else {
+      commit.setMessage(currentCommit.getFullMessage());
+    }
+    commit.setAuthor(currentCommit.getAuthorIdent());
+    commit.setCommitter(myIdent);
+    ObjectId id = or.ins.insert(commit);
+    return or.rw.parseCommit(id);
+  }
+
+  private RevCommit updateSubmodule(DirCache dc, DirCacheEditor ed,
+      StringBuilder msgbuf, final SubmoduleSubscription s)
+      throws SubmoduleException, IOException {
+    OpenRepo subOr;
+    try {
+      subOr = orm.openRepo(s.getSubmodule().getParentKey(), false);
+    } catch (NoSuchProjectException | IOException e) {
+      throw new SubmoduleException("Cannot access submodule", e);
+    }
+
+    DirCacheEntry dce = dc.getEntry(s.getPath());
+    RevCommit oldCommit = null;
+    if (dce != null) {
+      if (!dce.getFileMode().equals(FileMode.GITLINK)) {
+        String errMsg = "Requested to update gitlink " + s.getPath() + " in "
+            + s.getSubmodule().getParentKey().get() + " but entry "
+            + "doesn't have gitlink file mode.";
+        throw new SubmoduleException(errMsg);
       }
+      oldCommit = subOr.rw.parseCommit(dce.getObjectId());
+    }
 
-      ObjectInserter oi = or.repo.newObjectInserter();
-      ObjectId tree = dc.writeTree(oi);
+    final RevCommit newCommit;
+    if (branchTips.containsKey(s.getSubmodule())) {
+      newCommit = branchTips.get(s.getSubmodule());
+    } else {
+      Ref ref = subOr.repo.getRefDatabase().exactRef(s.getSubmodule().get());
+      if (ref == null) {
+        ed.add(new DeletePath(s.getPath()));
+        return null;
+      }
+      newCommit = subOr.rw.parseCommit(ref.getObjectId());
+    }
 
-      ObjectId currentCommitId =
-          or.repo.exactRef(subscriber.get()).getObjectId();
+    if (Objects.equals(newCommit, oldCommit)) {
+      // gitlink have already been updated for this submodule
+      return null;
+    }
+    ed.add(new PathEdit(s.getPath()) {
+      @Override
+      public void apply(DirCacheEntry ent) {
+        ent.setFileMode(FileMode.GITLINK);
+        ent.setObjectId(newCommit.getId());
+      }
+    });
 
-      CommitBuilder commit = new CommitBuilder();
-      commit.setTreeId(tree);
-      commit.setParentIds(new ObjectId[] {currentCommitId});
-      commit.setAuthor(author);
-      commit.setCommitter(myIdent);
-      commit.setMessage(msgbuf.toString());
-      oi.insert(commit);
-      oi.flush();
+    if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) {
+      createSubmoduleCommitMsg(msgbuf, s, subOr, newCommit, oldCommit);
+    }
+    subOr.rw.parseBody(newCommit);
+    return newCommit;
+  }
 
-      ObjectId commitId = oi.idFor(Constants.OBJ_COMMIT, commit.build());
+  private void createSubmoduleCommitMsg(StringBuilder msgbuf,
+      SubmoduleSubscription s, OpenRepo subOr, RevCommit newCommit, RevCommit oldCommit)
+      throws SubmoduleException {
+    msgbuf.append("* Update " + s.getPath());
+    msgbuf.append(" from branch '" + s.getSubmodule().getShortName() + "'");
 
-      final RefUpdate rfu = or.repo.updateRef(subscriber.get());
-      rfu.setForceUpdate(false);
-      rfu.setNewObjectId(commitId);
-      rfu.setExpectedOldObjectId(currentCommitId);
-      rfu.setRefLogMessage("Submit to " + subscriber.getParentKey().get(), true);
+    // newly created submodule gitlink, do not append whole history
+    if (oldCommit == null) {
+      return;
+    }
 
-      switch (rfu.update()) {
-        case NEW:
-        case FAST_FORWARD:
-          gitRefUpdated.fire(subscriber.getParentKey(), rfu, account);
-          // TODO since this is performed "in the background" no mail will be
-          // sent to inform users about the updated branch
-          break;
-        case FORCED:
-        case IO_FAILURE:
-        case LOCK_FAILURE:
-        case NOT_ATTEMPTED:
-        case NO_CHANGE:
-        case REJECTED:
-        case REJECTED_CURRENT_BRANCH:
-        case RENAMED:
-        default:
-          throw new IOException(rfu.getResult().name());
+    try {
+      subOr.rw.resetRetain(subOr.canMergeFlag);
+      subOr.rw.markStart(newCommit);
+      subOr.rw.markUninteresting(oldCommit);
+      for (RevCommit c : subOr.rw) {
+        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    "));
+        }
       }
     } catch (IOException e) {
-      throw new SubmoduleException("Cannot update gitlinks for "
-          + subscriber.get(), e);
+      throw new SubmoduleException("Could not perform a revwalk to "
+          + "create superproject commit message", e);
     }
   }
 
-  private static DirCache readTree(final Ref branch, RevWalk rw)
-      throws MissingObjectException, IncorrectObjectTypeException,
-      IOException {
+  private static DirCache readTree(RevWalk rw, ObjectId base)
+      throws IOException {
     final DirCache dc = DirCache.newInCore();
     final DirCacheBuilder b = dc.builder();
     b.addTree(new byte[0], // no prefix path
         DirCacheEntry.STAGE_0, // standard stage
-        rw.getObjectReader(), rw.parseTree(branch.getObjectId()));
+        rw.getObjectReader(), rw.parseTree(base));
     b.finish();
     return dc;
   }
 
+  public SetMultimap<Project.NameKey, Branch.NameKey> branchesByProject() {
+    SetMultimap<Project.NameKey, Branch.NameKey> ret = HashMultimap.create();
+    for (Branch.NameKey branch : targets.keySet()) {
+      ret.put(branch.getParentKey(), branch);
+    }
+
+    return ret;
+  }
+
+  public ImmutableSet<Project.NameKey> getProjectsInOrder()
+      throws SubmoduleException {
+    if (sortedBranches == null) {
+      return null;
+    }
+
+    LinkedHashSet<Project.NameKey> projects = new LinkedHashSet<>();
+    Project.NameKey prev = null;
+    for (Branch.NameKey branch : sortedBranches) {
+      Project.NameKey project = branch.getParentKey();
+      if (!project.equals(prev)) {
+        if (projects.contains(project)) {
+          throw new SubmoduleException(
+              "Project level circular subscriptions detected:  " +
+                  printCircularPath(projects, project));
+        }
+        projects.add(project);
+      }
+      prev = project;
+    }
+
+    return ImmutableSet.copyOf(projects);
+  }
+
+  public ImmutableSet<Branch.NameKey> getBranchesInOrder() {
+    return sortedBranches;
+  }
+
+  public boolean hasSubscription(Branch.NameKey branch) {
+    return targets.containsKey(branch);
+  }
+
+  public void addBranchTip(Branch.NameKey branch, CodeReviewCommit tip) {
+    branchTips.put(branch, tip);
+  }
+
+  public void addOp(BatchUpdate bu, Branch.NameKey branch) {
+    bu.addRepoOnlyOp(new GitlinkOp(branch));
+  }
+
   private void logDebug(String msg, Object... args) {
     if (log.isDebugEnabled()) {
-      log.debug("[" + orm.getSubmissionId() + "]" + msg, args);
+      log.debug(orm.getSubmissionId() + msg, args);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index eb4d400..6334cd2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -78,7 +78,7 @@
     }
   }
 
-  private RevCommit revision;
+  protected RevCommit revision;
   protected ObjectReader reader;
   protected ObjectInserter inserter;
   protected DirCache newTree;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index cd43fc3..c339d70 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -137,6 +137,11 @@
         if (ref.getObjectId() != null) {
           deferredTags.add(ref);
         }
+      } else if (name.startsWith(RefNames.REFS_SEQUENCES)) {
+        // Sequences are internal database implementation details.
+        if (viewMetadata) {
+          result.put(name, ref);
+        }
       } else if (projectCtl.controlForRef(ref.getLeaf().getName()).isVisible()) {
         // Use the leaf to lookup the control data. If the reference is
         // symbolic we want the control around the final target. If its
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
index 3a0db3e..00ab31b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -152,6 +152,15 @@
     return result;
   }
 
+  public Executor getExecutor(String queueName) {
+    for (Executor e : queues) {
+      if (e.queueName.equals(queueName)) {
+        return e;
+      }
+    }
+    return null;
+  }
+
   private void stop() {
     for (final Executor p : queues) {
       p.shutdown();
@@ -170,8 +179,9 @@
   /** An isolated queue. */
   public class Executor extends ScheduledThreadPoolExecutor {
     private final ConcurrentHashMap<Integer, Task<?>> all;
+    private final String queueName;
 
-    Executor(final int corePoolSize, final String prefix) {
+    Executor(int corePoolSize, final String prefix) {
       super(corePoolSize, new ThreadFactory() {
         private final ThreadFactory parent = Executors.defaultThreadFactory();
         private final AtomicInteger tid = new AtomicInteger(1);
@@ -190,6 +200,7 @@
           0.75f, // load factor
           corePoolSize + 4 // concurrency level
           );
+      queueName = prefix;
     }
 
     public void unregisterWorkQueue() {
@@ -325,6 +336,10 @@
       return startTime;
     }
 
+    public String getQueueName() {
+      return executor.queueName;
+    }
+
     @Override
     public boolean cancel(boolean mayInterruptIfRunning) {
       if (task.cancel(mayInterruptIfRunning)) {
@@ -413,10 +428,12 @@
             if (field.getType().isAssignableFrom(trustedFutureInterruptibleTask)) {
               field.setAccessible(true);
               Object innerObj = field.get(runnable);
-              for (Field innerField : innerObj.getClass().getDeclaredFields()) {
-                if (innerField.getType().isAssignableFrom(Callable.class)) {
-                  innerField.setAccessible(true);
-                  return ((Callable<?>) innerField.get(innerObj)).toString();
+              if (innerObj != null) {
+                for (Field innerField : innerObj.getClass().getDeclaredFields()) {
+                  if (innerField.getType().isAssignableFrom(Callable.class)) {
+                    innerField.setAccessible(true);
+                    return ((Callable<?>) innerField.get(innerObj)).toString();
+                  }
                 }
               }
             }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 91d2cc7..4a5e94d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -74,11 +74,12 @@
     }
 
     @Override
-    protected void updateRepoImpl(RepoContext ctx) {
+    protected void updateRepoImpl(RepoContext ctx) throws IntegrationException {
       // The branch is unborn. Take fast-forward resolution to create the
       // branch.
-      args.mergeTip.moveTipTo(toMerge, toMerge);
-      toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
+      CodeReviewCommit newCommit = amendGitlink(toMerge);
+      args.mergeTip.moveTipTo(newCommit, toMerge);
+      newCommit.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
     }
   }
 
@@ -105,7 +106,8 @@
     }
 
     @Override
-    protected void updateRepoImpl(RepoContext ctx) throws IOException {
+    protected void updateRepoImpl(RepoContext ctx)
+        throws IntegrationException, IOException {
       // If there is only one parent, a cherry-pick can be done by taking the
       // delta relative to that one parent and redoing that on the current merge
       // tip.
@@ -132,6 +134,7 @@
       }
       // Initial copy doesn't have new patch set ID since change hasn't been
       // updated yet.
+      newCommit = amendGitlink(newCommit);
       newCommit.copyFrom(toMerge);
       newCommit.setPatchsetId(psId);
       newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
@@ -189,12 +192,13 @@
       // was configured.
       MergeTip mergeTip = args.mergeTip;
       if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) {
-        mergeTip.moveTipTo(toMerge, toMerge);
+        mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
       } else {
         PersonIdent myIdent = new PersonIdent(args.serverIdent, ctx.getWhen());
         CodeReviewCommit result = args.mergeUtil.mergeOneCommit(myIdent,
             myIdent, args.repo, args.rw, args.inserter, args.destBranch,
             mergeTip.getCurrentTip(), toMerge);
+        result = amendGitlink(result);
         mergeTip.moveTipTo(result, toMerge);
       }
       args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOp.java
index cd99daf..0e69128 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOp.java
@@ -18,16 +18,13 @@
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.IntegrationException;
 
-import java.io.IOException;
-
 class FastForwardOp extends SubmitStrategyOp {
   FastForwardOp(SubmitStrategy.Arguments args, CodeReviewCommit toMerge) {
     super(args, toMerge);
   }
 
   @Override
-  public void updateRepoImpl(RepoContext ctx)
-      throws IntegrationException, IOException {
-    args.mergeTip.moveTipTo(toMerge, toMerge);
+  protected void updateRepoImpl(RepoContext ctx) throws IntegrationException {
+    args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
index b00b998..dfa13dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
@@ -32,7 +32,7 @@
     List<CodeReviewCommit> sorted =
         args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
-    if (args.mergeTip.getInitialTip() == null) {
+    if (args.mergeTip.getInitialTip() == null && !sorted.isEmpty()) {
       // The branch is unborn. Take a fast-forward resolution to
       // create the branch.
       CodeReviewCommit first = sorted.remove(0);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
index b840861..0e2cbd7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
@@ -32,17 +32,10 @@
     List<CodeReviewCommit> sorted =
         args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
-    CodeReviewCommit firstFastForward;
-    if (args.mergeTip.getInitialTip() == null) {
-      if (sorted.isEmpty()) {
-        throw new IntegrationException("nothing to merge");
-      }
-      firstFastForward = sorted.remove(0);
-    } else {
-      firstFastForward = args.mergeUtil.getFirstFastForward(
+    CodeReviewCommit firstFastForward = args.mergeUtil.getFirstFastForward(
           args.mergeTip.getInitialTip(), args.rw, sorted);
-    }
-    if (!firstFastForward.equals(args.mergeTip.getInitialTip())) {
+    if (firstFastForward != null &&
+        !firstFastForward.equals(args.mergeTip.getInitialTip())) {
       ops.add(new FastForwardOp(args, firstFastForward));
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
index cea3429..b1590bf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
@@ -30,7 +30,7 @@
   @Override
   public void updateRepoImpl(RepoContext ctx)
       throws IntegrationException, IOException {
-    PersonIdent caller = ctx.getUser().asIdentifiedUser().newCommitterIdent(
+    PersonIdent caller = ctx.getIdentifiedUser().newCommitterIdent(
         ctx.getWhen(), ctx.getTimeZone());
     if (args.mergeTip.getCurrentTip() == null) {
       throw new IllegalStateException("cannot merge commit " + toMerge.name()
@@ -44,6 +44,6 @@
         args.mergeUtil.mergeOneCommit(caller, args.serverIdent,
             ctx.getRepository(), args.rw, ctx.getInserter(), args.destBranch,
             args.mergeTip.getCurrentTip(), toMerge);
-    args.mergeTip.moveTipTo(merged, toMerge);
+    args.mergeTip.moveTipTo(amendGitlink(merged), toMerge);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
index 4d51aea..f183772d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
@@ -49,6 +49,17 @@
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
     boolean first = true;
 
+    for (CodeReviewCommit c : sorted) {
+      if (c.getParentCount() > 1) {
+        // Since there is a merge commit, sort and prune again using
+        // MERGE_IF_NECESSARY semantics to avoid creating duplicate
+        // commits.
+        //
+        sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted);
+        break;
+      }
+    }
+
     while (!sorted.isEmpty()) {
       CodeReviewCommit n = sorted.remove(0);
       if (first && args.mergeTip.getInitialTip() == null) {
@@ -71,11 +82,12 @@
     }
 
     @Override
-    public void updateRepoImpl(RepoContext ctx) {
+    public void updateRepoImpl(RepoContext ctx) throws IntegrationException {
       // The branch is unborn. Take fast-forward resolution to create the
       // branch.
       toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
-      args.mergeTip.moveTipTo(toMerge, toMerge);
+      CodeReviewCommit newCommit = amendGitlink(toMerge);
+      args.mergeTip.moveTipTo(newCommit, toMerge);
       acceptMergeTip(args.mergeTip);
     }
   }
@@ -110,8 +122,7 @@
       // BatchUpdate how to produce CodeReviewRevWalks.
       if (args.mergeUtil.canFastForward(args.mergeSorter,
           args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
-        toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
-        args.mergeTip.moveTipTo(toMerge, toMerge);
+        args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
         acceptMergeTip(args.mergeTip);
         return;
       }
@@ -121,7 +132,7 @@
           ctx.getDb(), toMerge.getControl().getNotes(), toMerge.getPatchsetId());
       rebaseOp = args.rebaseFactory.create(
             toMerge.getControl(), origPs, args.mergeTip.getCurrentTip().name())
-          .setRunHooks(false)
+          .setFireRevisionCreated(false)
           // Bypass approval copier since SubmitStrategyOp copy all approvals
           // later anyway.
           .setCopyApprovals(false)
@@ -134,6 +145,7 @@
             "Cannot rebase " + toMerge.name() + ": " + e.getMessage(), e);
       }
       newCommit = args.rw.parseCommit(rebaseOp.getRebasedCommit());
+      newCommit = amendGitlink(newCommit);
       newCommit.copyFrom(toMerge);
       newCommit.setStatusCode(CommitMergeStatus.CLEAN_REBASE);
       newCommit.setPatchsetId(rebaseOp.getPatchSetId());
@@ -182,13 +194,13 @@
       // configured.
       MergeTip mergeTip = args.mergeTip;
       if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) {
-        mergeTip.moveTipTo(toMerge, toMerge);
+        mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
         acceptMergeTip(mergeTip);
       } else {
         CodeReviewCommit newTip = args.mergeUtil.mergeOneCommit(
             args.serverIdent, args.serverIdent, args.repo, args.rw,
             args.inserter, args.destBranch, mergeTip.getCurrentTip(), toMerge);
-        mergeTip.moveTipTo(newTip, toMerge);
+        mergeTip.moveTipTo(amendGitlink(newTip), toMerge);
       }
       args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
           mergeTip.getCurrentTip(), args.alreadyAccepted);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index 9f48dc6..36de70e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -17,7 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.collect.Sets;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -41,11 +41,13 @@
 import com.google.gerrit.server.git.MergeSorter;
 import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.SubmoduleOp;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.RequestId;
 import com.google.inject.Module;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -92,8 +94,9 @@
           RevFlag canMergeFlag,
           ReviewDb db,
           Set<RevCommit> alreadyAccepted,
-          String submissionId,
-          NotifyHandling notifyHandling);
+          RequestId submissionId,
+          NotifyHandling notifyHandling,
+          SubmoduleOp submoduleOp);
     }
 
     final AccountCache accountCache;
@@ -122,9 +125,10 @@
     final RevFlag canMergeFlag;
     final ReviewDb db;
     final Set<RevCommit> alreadyAccepted;
-    final String submissionId;
+    final RequestId submissionId;
     final SubmitType submitType;
     final NotifyHandling notifyHandling;
+    final SubmoduleOp submoduleOp;
 
     final ProjectState project;
     final MergeSorter mergeSorter;
@@ -158,9 +162,10 @@
         @Assisted RevFlag canMergeFlag,
         @Assisted ReviewDb db,
         @Assisted Set<RevCommit> alreadyAccepted,
-        @Assisted String submissionId,
+        @Assisted RequestId submissionId,
         @Assisted SubmitType submitType,
-        @Assisted NotifyHandling notifyHandling) {
+        @Assisted NotifyHandling notifyHandling,
+        @Assisted SubmoduleOp submoduleOp) {
       this.accountCache = accountCache;
       this.approvalsUtil = approvalsUtil;
       this.batchUpdateFactory = batchUpdateFactory;
@@ -190,6 +195,7 @@
       this.submissionId = submissionId;
       this.submitType = submitType;
       this.notifyHandling = notifyHandling;
+      this.submoduleOp = submoduleOp;
 
       this.project = checkNotNull(projectCache.get(destBranch.getParentKey()),
             "project not found: %s", destBranch.getParentKey());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
index 1e3fdbe..6bb6fa6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -23,6 +23,8 @@
 import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeOp.CommitStatus;
 import com.google.gerrit.server.git.MergeTip;
+import com.google.gerrit.server.git.SubmoduleOp;
+import com.google.gerrit.server.util.RequestId;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -52,11 +54,12 @@
       Repository repo, CodeReviewRevWalk rw, ObjectInserter inserter,
       RevFlag canMergeFlag, Set<RevCommit> alreadyAccepted,
       Branch.NameKey destBranch, IdentifiedUser caller, MergeTip mergeTip,
-      CommitStatus commits, String submissionId, NotifyHandling notifyHandling)
+      CommitStatus commits, RequestId submissionId, NotifyHandling notifyHandling,
+      SubmoduleOp submoduleOp)
       throws IntegrationException {
     SubmitStrategy.Arguments args = argsFactory.create(submitType, destBranch,
         commits, rw, caller, mergeTip, inserter, repo, canMergeFlag, db,
-        alreadyAccepted, submissionId, notifyHandling);
+        alreadyAccepted, submissionId, notifyHandling, submoduleOp);
     switch (submitType) {
       case CHERRY_PICK:
         return new CherryPick(args);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java
index 95b3ff4..eedfe70 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java
@@ -64,12 +64,6 @@
     if (failAfterRefUpdates) {
       throw new ResourceConflictException("Failing after ref updates");
     }
-    for (SubmitStrategy strategy : strategies) {
-      SubmitStrategy.Arguments args = strategy.args;
-      if (args.mergeTip.getCurrentTip().equals(args.mergeTip.getInitialTip())) {
-        continue;
-      }
-    }
   }
 
   private void findUnmergedChanges(List<Change.Id> alreadyMerged)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index 37e236c..72bfedf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -46,6 +46,7 @@
 import com.google.gerrit.server.git.LabelNormalizer;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.SubmoduleException;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
@@ -136,6 +137,7 @@
         tipAfter,
         getDest().get());
     ctx.addRefUpdate(command);
+    args.submoduleOp.addBranchTip(getDest(), tipAfter);
   }
 
   private void checkProjectConfig(RepoContext ctx, CodeReviewCommit commit)
@@ -336,7 +338,7 @@
     submitter = new PatchSetApproval(
           new PatchSetApproval.Key(
               psId,
-              ctx.getUser().getAccountId(),
+              ctx.getAccountId(),
               LabelId.legacySubmit()),
               (short) 1, ctx.getWhen());
     byKey.put(submitter.getKey(), submitter);
@@ -471,7 +473,7 @@
     }
     ChangeMessage m = new ChangeMessage(
         new ChangeMessage.Key(psId.getParentKey(), uuid),
-        ctx.getUser().getAccountId(), ctx.getWhen(), psId);
+        ctx.getAccountId(), ctx.getWhen(), psId);
     m.setMessage(body);
     return m;
   }
@@ -482,7 +484,7 @@
     ReviewDb db = ctx.getDb();
     logDebug("Setting change {} merged", c.getId());
     c.setStatus(Change.Status.MERGED);
-    c.setSubmissionId(args.submissionId);
+    c.setSubmissionId(args.submissionId.toStringForStorage());
 
     // TODO(dborowitz): We need to be able to change the author of the message,
     // which is not the user from the update context. addMergedMessage was able
@@ -527,7 +529,8 @@
           updatedChange,
           mergedPatchSet,
           args.accountCache.get(submitter.getAccountId()).getAccount(),
-          args.mergeTip.getCurrentTip().name());
+          args.mergeTip.getCurrentTip().name(),
+          ctx.getWhen());
     }
   }
 
@@ -555,24 +558,52 @@
   protected void postUpdateImpl(Context ctx) throws Exception {
   }
 
+  /**
+   * Amend the commit with gitlink update
+   * @param commit
+   */
+  protected CodeReviewCommit amendGitlink(CodeReviewCommit commit)
+      throws IntegrationException {
+    CodeReviewCommit newCommit = commit;
+    // Modify the commit with gitlink update
+    if (args.submoduleOp.hasSubscription(args.destBranch)) {
+      try {
+        newCommit =
+            args.submoduleOp.composeGitlinksCommit(args.destBranch, commit);
+        newCommit.copyFrom(commit);
+        if (commit.equals(toMerge)) {
+          newCommit.setPatchsetId(ChangeUtil.nextPatchSetId(
+              args.repo, toMerge.change().currentPatchSetId()));
+          args.commits.put(newCommit);
+        }
+      } catch (SubmoduleException | IOException e) {
+        throw new IntegrationException(
+            "cannot update gitlink for the commit at branch: "
+                + args.destBranch);
+      }
+    }
+
+    return newCommit;
+  }
+
   protected final void logDebug(String msg, Object... args) {
     if (log.isDebugEnabled()) {
-      log.debug("[" + this.args.submissionId + "]" + msg, args);
+      log.debug(this.args.submissionId + msg, args);
     }
   }
 
   protected final void logWarn(String msg, Throwable t) {
     if (log.isWarnEnabled()) {
-      log.warn("[" + args.submissionId + "]" + msg, t);
+      log.warn(args.submissionId + msg, t);
     }
   }
 
   protected void logError(String msg, Throwable t) {
     if (log.isErrorEnabled()) {
       if (t != null) {
-        log.error("[" + args.submissionId + "]" + msg, t);
+        log.error(args.submissionId + msg, t);
       } else {
-        log.error("[" + args.submissionId + "]" + msg);
+        log.error(args.submissionId + msg);
       }
     }
   }
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 1fd89f7..d4956ab 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
@@ -22,8 +22,12 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.WatchConfig;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.events.CommitReceivedEvent;
@@ -86,22 +90,26 @@
   private final SshInfo sshInfo;
   private final Repository repo;
   private final DynamicSet<CommitValidationListener> commitValidationListeners;
+  private final AllUsersName allUsers;
 
   @Inject
-  CommitValidators(@GerritPersonIdent final PersonIdent gerritIdent,
-      @CanonicalWebUrl @Nullable final String canonicalWebUrl,
-      @GerritServerConfig final Config config,
-      final DynamicSet<CommitValidationListener> commitValidationListeners,
-      @Assisted final SshInfo sshInfo,
-      @Assisted final Repository repo, @Assisted final RefControl refControl) {
+  CommitValidators(@GerritPersonIdent PersonIdent gerritIdent,
+      @CanonicalWebUrl @Nullable String canonicalWebUrl,
+      @GerritServerConfig Config config,
+      DynamicSet<CommitValidationListener> commitValidationListeners,
+      AllUsersName allUsers,
+      @Assisted SshInfo sshInfo,
+      @Assisted Repository repo,
+      @Assisted RefControl refControl) {
     this.gerritIdent = gerritIdent;
-    this.refControl = refControl;
     this.canonicalWebUrl = canonicalWebUrl;
     this.installCommitMsgHookCommand =
         config.getString("gerrit", null, "installCommitMsgHookCommand");
+    this.commitValidationListeners = commitValidationListeners;
+    this.allUsers = allUsers;
     this.sshInfo = sshInfo;
     this.repo = repo;
-    this.commitValidationListeners = commitValidationListeners;
+    this.refControl = refControl;
   }
 
   public List<CommitValidationMessage> validateForReceiveCommits(
@@ -122,7 +130,7 @@
       validators.add(new ChangeIdValidator(refControl, canonicalWebUrl,
           installCommitMsgHookCommand, sshInfo));
     }
-    validators.add(new ConfigValidator(refControl, repo));
+    validators.add(new ConfigValidator(refControl, repo, allUsers));
     validators.add(new BannedCommitsValidator(rejectCommits));
     validators.add(new PluginCommitValidationListener(commitValidationListeners));
 
@@ -156,7 +164,7 @@
       validators.add(new ChangeIdValidator(refControl, canonicalWebUrl,
           installCommitMsgHookCommand, sshInfo));
     }
-    validators.add(new ConfigValidator(refControl, repo));
+    validators.add(new ConfigValidator(refControl, repo, allUsers));
     validators.add(new PluginCommitValidationListener(commitValidationListeners));
 
     List<CommitValidationMessage> messages = new LinkedList<>();
@@ -321,10 +329,13 @@
   public static class ConfigValidator implements CommitValidationListener {
     private final RefControl refControl;
     private final Repository repo;
+    private final AllUsersName allUsers;
 
-    public ConfigValidator(RefControl refControl, Repository repo) {
+    public ConfigValidator(RefControl refControl, Repository repo,
+        AllUsersName allUsers) {
       this.refControl = refControl;
       this.repo = repo;
+      this.allUsers = allUsers;
     }
 
     @Override
@@ -346,15 +357,43 @@
             }
             throw new ConfigInvalidException("invalid project configuration");
           }
-        } catch (Exception e) {
+        } catch (ConfigInvalidException | IOException e) {
           log.error("User " + currentUser.getUserName()
-              + " tried to push invalid project configuration "
-              + receiveEvent.command.getNewId().name() + " for "
+              + " tried to push an invalid project configuration "
+              + receiveEvent.command.getNewId().name() + " for project "
               + receiveEvent.project.getName(), e);
           throw new CommitValidationException("invalid project configuration",
               messages);
         }
       }
+
+      if (allUsers.equals(
+              refControl.getProjectControl().getProject().getNameKey())
+          && RefNames.isRefsUsers(refControl.getRefName())) {
+        List<CommitValidationMessage> messages = new LinkedList<>();
+        Account.Id accountId = Account.Id.fromRef(refControl.getRefName());
+        if (accountId != null) {
+          try {
+            WatchConfig wc = new WatchConfig(accountId);
+            wc.load(repo, receiveEvent.command.getNewId());
+            if (!wc.getValidationErrors().isEmpty()) {
+              addError("Invalid project configuration:", messages);
+              for (ValidationError err : wc.getValidationErrors()) {
+                addError("  " + err.getMessage(), messages);
+              }
+              throw new ConfigInvalidException("invalid watch configuration");
+            }
+          } catch (IOException | ConfigInvalidException e) {
+            log.error("User " + currentUser.getUserName()
+                + " tried to push an invalid watch configuration "
+                + receiveEvent.command.getNewId().name() + " for account "
+                + accountId.get(), e);
+            throw new CommitValidationException("invalid watch configuration",
+                messages);
+          }
+        }
+      }
+
       return Collections.emptyList();
     }
   }
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 7dd2d81..c10b279 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.Lists;
 import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -27,7 +28,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
@@ -125,11 +125,11 @@
     GroupControl control = resource.getControl();
 
     Set<Account.Id> newMemberIds = new HashSet<>();
-    for (String nameOrEmail : input.members) {
-      Account a = findAccount(nameOrEmail);
+    for (String nameOrEmailOrId : input.members) {
+      Account a = findAccount(nameOrEmailOrId);
       if (!a.isActive()) {
         throw new UnprocessableEntityException(String.format(
-            "Account Inactive: %s", nameOrEmail));
+            "Account Inactive: %s", nameOrEmailOrId));
       }
 
       if (!control.canAddMember()) {
@@ -142,10 +142,10 @@
     return toAccountInfoList(newMemberIds);
   }
 
-  private Account findAccount(String nameOrEmail) throws AuthException,
+  Account findAccount(String nameOrEmailOrId) throws AuthException,
       UnprocessableEntityException, OrmException, IOException {
     try {
-      return accounts.parse(nameOrEmail).getAccount();
+      return accounts.parse(nameOrEmailOrId).getAccount();
     } catch (UnprocessableEntityException e) {
       // might be because the account does not exist or because the account is
       // not visible
@@ -153,9 +153,9 @@
         case HTTP_LDAP:
         case CLIENT_SSL_CERT_LDAP:
         case LDAP:
-          if (accountResolver.find(nameOrEmail) == null) {
+          if (accountResolver.find(db.get(), nameOrEmailOrId) == null) {
             // account does not exist, try to create it
-            Account a = createAccountByLdap(nameOrEmail);
+            Account a = createAccountByLdap(nameOrEmailOrId);
             if (a != null) {
               return a;
             }
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 9976d4c..0fd4728 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
@@ -21,8 +21,10 @@
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -51,6 +53,8 @@
 import org.eclipse.jgit.lib.PersonIdent;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
@@ -96,9 +100,19 @@
     this.name = name;
   }
 
+  public CreateGroup addOption(ListGroupsOption o) {
+    json.addOption(o);
+    return this;
+  }
+
+  public CreateGroup addOptions(Collection<ListGroupsOption> o) {
+    json.addOptions(o);
+    return this;
+  }
+
   @Override
   public GroupInfo apply(TopLevelResource resource, GroupInput input)
-      throws BadRequestException, UnprocessableEntityException,
+      throws AuthException, BadRequestException, UnprocessableEntityException,
       ResourceConflictException, OrmException, IOException {
     if (input == null) {
       input = new GroupInput();
@@ -114,9 +128,22 @@
     args.visibleToAll = MoreObjects.firstNonNull(input.visibleToAll,
         defaultVisibleToAll);
     args.ownerGroupId = ownerId;
-    args.initialMembers = ownerId == null
-        ? Collections.singleton(self.get().getAccountId())
-        : Collections.<Account.Id> emptySet();
+    if (input.members != null && !input.members.isEmpty()) {
+      List<Account.Id> members = new ArrayList<>();
+      for (String nameOrEmailOrId : input.members) {
+        Account a = addMembers.findAccount(nameOrEmailOrId);
+        if (!a.isActive()) {
+          throw new UnprocessableEntityException(String.format(
+              "Account Inactive: %s", nameOrEmailOrId));
+        }
+        members.add(a.getId());
+      }
+      args.initialMembers = members;
+    } else {
+      args.initialMembers = ownerId == null
+          ? Collections.singleton(self.get().getAccountId())
+          : Collections.<Account.Id> emptySet();
+    }
 
     for (GroupCreationValidationListener l : groupCreationValidationListeners) {
       try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java
index 669e253..ca0fab2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java
@@ -14,10 +14,8 @@
 
 package com.google.gerrit.server.index;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.inject.Inject;
 
 import java.util.Collection;
 import java.util.Collections;
@@ -30,9 +28,7 @@
   private final CopyOnWriteArrayList<I> writeIndexes;
   private final AtomicReference<I> searchIndex;
 
-  @Inject
-  @VisibleForTesting
-  public IndexCollection() {
+  protected IndexCollection() {
     this.writeIndexes = Lists.newCopyOnWriteArrayList();
     this.searchIndex = new AtomicReference<>();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index 26ed600..d5d90d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.index.account.AccountIndexDefinition;
 import com.google.gerrit.server.index.account.AccountIndexRewriter;
 import com.google.gerrit.server.index.account.AccountIndexer;
+import com.google.gerrit.server.index.account.AccountIndexerImpl;
 import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexDefinition;
@@ -92,7 +93,7 @@
     bind(AccountIndexRewriter.class);
     bind(AccountIndexCollection.class);
     listener().to(AccountIndexCollection.class);
-    factory(AccountIndexer.Factory.class);
+    factory(AccountIndexerImpl.Factory.class);
 
     bind(ChangeIndexRewriter.class);
     bind(ChangeIndexCollection.class);
@@ -132,7 +133,7 @@
 
   @Provides
   @Singleton
-  AccountIndexer getAccountIndexer(AccountIndexer.Factory factory,
+  AccountIndexer getAccountIndexer(AccountIndexerImpl.Factory factory,
       AccountIndexCollection indexes) {
     return factory.create(indexes);
   }
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 0de17d3..824739e 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
@@ -21,6 +21,7 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.FieldType;
 import com.google.gerrit.server.index.SchemaUtil;
@@ -83,6 +84,15 @@
         }
       };
 
+  public static final FieldDef<AccountState, String> FULL_NAME =
+      new FieldDef.Single<AccountState, String>("full_name", FieldType.EXACT,
+          false) {
+        @Override
+        public String get(AccountState input, FillArgs args) {
+          return input.getAccount().getFullName();
+        }
+      };
+
   public static final FieldDef<AccountState, String> ACTIVE =
       new FieldDef.Single<AccountState, String>(
           "inactive", FieldType.EXACT, false) {
@@ -137,6 +147,21 @@
         }
       };
 
+  public static final FieldDef<AccountState, Iterable<String>> WATCHED_PROJECT =
+      new FieldDef.Repeatable<AccountState, String>(
+          "watchedproject", FieldType.EXACT, false) {
+        @Override
+        public Iterable<String> get(AccountState input, FillArgs args) {
+          return FluentIterable.from(input.getProjectWatches().keySet())
+              .transform(new Function<ProjectWatchKey, String>() {
+            @Override
+            public String apply(ProjectWatchKey in) {
+              return in.project().get();
+            }
+          }).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 9f4cca8..6aa516c 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
@@ -14,12 +14,18 @@
 
 package com.google.gerrit.server.index.account;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.IndexCollection;
+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/AccountIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexer.java
index dafc7e9..3203563 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexer.java
@@ -15,38 +15,15 @@
 package com.google.gerrit.server.index.account;
 
 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.index.Index;
-import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
 
 import java.io.IOException;
 
-public class AccountIndexer {
-
-  public interface Factory {
-    AccountIndexer create(AccountIndexCollection indexes);
-  }
-
-  private final AccountIndexCollection indexes;
-  private final AccountCache byIdCache;
-
-  @AssistedInject
-  AccountIndexer(AccountCache byIdCache,
-      @Assisted AccountIndexCollection indexes) {
-    this.byIdCache = byIdCache;
-    this.indexes = indexes;
-  }
+public interface AccountIndexer {
 
   /**
    * Synchronously index an account.
    *
    * @param id account id to index.
    */
-  public void index(Account.Id id) throws IOException {
-    for (Index<?, AccountState> i : indexes.getWriteIndexes()) {
-      i.replace(byIdCache.get(id));
-    }
-  }
+  void index(Account.Id id) throws IOException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
new file mode 100644
index 0000000..d3bfeb8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2016 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 com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
+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.index.Index;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+
+public class AccountIndexerImpl implements AccountIndexer {
+  public interface Factory {
+    AccountIndexerImpl create(AccountIndexCollection indexes);
+    AccountIndexerImpl create(@Nullable AccountIndex index);
+  }
+
+  private final AccountCache byIdCache;
+  private final AccountIndexCollection indexes;
+  private final AccountIndex index;
+
+  @AssistedInject
+  AccountIndexerImpl(AccountCache byIdCache,
+      @Assisted AccountIndexCollection indexes) {
+    this.byIdCache = byIdCache;
+    this.indexes = indexes;
+    this.index = null;
+  }
+
+  @AssistedInject
+  AccountIndexerImpl(AccountCache byIdCache,
+      @Assisted AccountIndex index) {
+    this.byIdCache = byIdCache;
+    this.indexes = null;
+    this.index = index;
+  }
+
+  @Override
+  public void index(Account.Id id) throws IOException {
+    for (Index<?, AccountState> i : getWriteIndexes()) {
+      i.replace(byIdCache.get(id));
+    }
+  }
+
+  private Collection<AccountIndex> getWriteIndexes() {
+    if (indexes != null) {
+      return indexes.getWriteIndexes();
+    }
+
+    return index != null
+        ? Collections.singleton(index)
+        : ImmutableSet.<AccountIndex> of();
+  }
+}
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 0c5af2c..bebe668 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
@@ -30,6 +30,12 @@
       AccountField.REGISTERED,
       AccountField.USERNAME);
 
+  static final Schema<AccountState> V2 =
+      schema(V1, AccountField.WATCHED_PROJECT);
+
+  static final Schema<AccountState> V3 =
+      schema(V2, AccountField.FULL_NAME);
+
   public static final AccountSchemaDefinitions INSTANCE =
       new AccountSchemaDefinitions();
 
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 247aa62..dc1c4a5 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
@@ -14,12 +14,18 @@
 
 package com.google.gerrit.server.index.change;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.IndexCollection;
 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/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 4a331d9..fa4f2fa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -203,19 +203,7 @@
    */
   public void index(ReviewDb db, Change change)
       throws IOException, OrmException {
-    ChangeData cd;
-    if (notesMigration.commitChangeWrites()) {
-      // Auto-rebuilding when NoteDb reads are disabled just increases
-      // contention on the meta ref from a background indexing thread with
-      // little benefit. The next actual write to the entity may still incur a
-      // less-contentious rebuild.
-      ChangeNotes notes =
-          changeNotesFactory.createWithAutoRebuildingDisabled(change, null);
-      cd = changeDataFactory.create(db, notes);
-    } else {
-      cd = changeDataFactory.create(db, change);
-    }
-    index(cd);
+    index(newChangeData(db, change));
   }
 
   /**
@@ -226,8 +214,8 @@
    * @param changeId ID of the change to index.
    */
   public void index(ReviewDb db, Project.NameKey project, Change.Id changeId)
-      throws IOException {
-    index(changeDataFactory.create(db, project, changeId));
+      throws IOException, OrmException {
+    index(newChangeData(db, project, changeId));
   }
 
   /**
@@ -258,7 +246,8 @@
   }
 
   private CheckedFuture<?, IOException> submit(Callable<?> task) {
-    return Futures.makeChecked(executor.submit(task), MAPPER);
+    return Futures.makeChecked(
+        Futures.nonCancellationPropagating(executor.submit(task)), MAPPER);
   }
 
   private class IndexTask implements Callable<Void> {
@@ -300,8 +289,8 @@
         };
         RequestContext oldCtx = context.setContext(newCtx);
         try {
-          ChangeData cd = changeDataFactory
-              .create(newCtx.getReviewDbProvider().get(), project, id);
+          ChangeData cd = newChangeData(
+              newCtx.getReviewDbProvider().get(), project, id);
           index(cd);
           return null;
         } finally  {
@@ -342,4 +331,29 @@
       return null;
     }
   }
+
+  // Avoid auto-rebuilding when reindexing if reading is disabled. This just
+  // increases contention on the meta ref from a background indexing thread
+  // with little benefit. The next actual write to the entity may still incur a
+  // less-contentious rebuild.
+  private ChangeData newChangeData(ReviewDb db, Change change)
+      throws OrmException {
+    if (!notesMigration.readChanges()) {
+      ChangeNotes notes = changeNotesFactory.createWithAutoRebuildingDisabled(
+          change, null);
+      return changeDataFactory.create(db, notes);
+    }
+    return changeDataFactory.create(db, change);
+  }
+
+  private ChangeData newChangeData(ReviewDb db, Project.NameKey project,
+      Change.Id changeId) throws OrmException {
+    if (!notesMigration.readChanges()) {
+      ChangeNotes notes = changeNotesFactory.createWithAutoRebuildingDisabled(
+          db, project, changeId);
+      return changeDataFactory.create(db, notes);
+    }
+    return changeDataFactory.create(db, project, changeId);
+  }
+
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index 0684d8f..badc706 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -18,7 +18,7 @@
 
 import com.google.common.collect.Multimap;
 import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
@@ -50,6 +50,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.sql.Timestamp;
 import java.text.MessageFormat;
 import java.util.Collection;
 import java.util.Collections;
@@ -72,7 +73,8 @@
   protected final ChangeData changeData;
   protected PatchSet patchSet;
   protected PatchSetInfo patchSetInfo;
-  protected ChangeMessage changeMessage;
+  protected String changeMessage;
+  protected Timestamp timestamp;
 
   protected ProjectState projectState;
   protected Set<Account.Id> authors;
@@ -104,8 +106,14 @@
     patchSetInfo = psi;
   }
 
+  @Deprecated
   public void setChangeMessage(final ChangeMessage cm) {
+    setChangeMessage(cm.getMessage(), cm.getWrittenOn());
+  }
+
+  public void setChangeMessage(String cm, Timestamp t) {
     changeMessage = cm;
+    timestamp = t;
   }
 
   /** Format the message body by calling {@link #appendText(String)}. */
@@ -166,9 +174,8 @@
     authors = getAuthors();
 
     super.init();
-
-    if (changeMessage != null && changeMessage.getWrittenOn() != null) {
-      setHeader("Date", new Date(changeMessage.getWrittenOn().getTime()));
+    if (timestamp != null) {
+      setHeader("Date", new Date(timestamp.getTime()));
     }
     setChangeSubjectHeader();
     setHeader("X-Gerrit-Change-Id", "" + change.getKey().get());
@@ -220,13 +227,10 @@
     }
   }
 
-  /** Get the text of the "cover letter", from {@link ChangeMessage}. */
+  /** Get the text of the "cover letter". */
   public String getCoverLetter() {
     if (changeMessage != null) {
-      final String txt = changeMessage.getMessage();
-      if (txt != null) {
-        return txt.trim();
-      }
+      return changeMessage.trim();
     }
     return "";
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index a39e43a..b56b737 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -20,7 +20,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.CommentRange;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index 88c9199..3136aec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2016 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.
@@ -31,16 +31,19 @@
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.template.soy.tofu.SoyTofu;
 
 import org.apache.velocity.runtime.RuntimeInstance;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -72,9 +75,12 @@
   final Provider<ReviewDb> db;
   final ChangeData.Factory changeDataFactory;
   final RuntimeInstance velocityRuntime;
+  final SoyTofu soyTofu;
   final EmailSettings settings;
   final DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners;
   final StarredChangesUtil starredChangesUtil;
+  final AccountIndexCollection accountIndexes;
+  final Provider<InternalAccountQuery> accountQueryProvider;
 
   @Inject
   EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
@@ -96,10 +102,13 @@
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       RuntimeInstance velocityRuntime,
+      @MailTemplates SoyTofu soyTofu,
       EmailSettings settings,
       @SshAdvertisedAddresses List<String> sshAddresses,
       DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners,
-      StarredChangesUtil starredChangesUtil) {
+      StarredChangesUtil starredChangesUtil,
+      AccountIndexCollection accountIndexes,
+      Provider<InternalAccountQuery> accountQueryProvider) {
     this.server = server;
     this.projectCache = projectCache;
     this.groupBackend = groupBackend;
@@ -122,9 +131,12 @@
     this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.velocityRuntime = velocityRuntime;
+    this.soyTofu = soyTofu;
     this.settings = settings;
     this.sshAddresses = sshAddresses;
     this.outgoingEmailValidationListeners = outgoingEmailValidationListeners;
     this.starredChangesUtil = starredChangesUtil;
+    this.accountIndexes = accountIndexes;
+    this.accountQueryProvider = accountQueryProvider;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
index 51f7ad1..0bc65bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
@@ -32,6 +32,7 @@
 
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.regex.Pattern;
 
 /** Creates a {@link FromAddressGenerator} from the {@link GerritServerConfig} */
 @Singleton
@@ -53,13 +54,15 @@
       generator =
           new PatternGen(srvAddr, accountCache, anonymousCowardName, name,
               srvAddr.email);
-
     } else if ("USER".equalsIgnoreCase(from)) {
-      generator = new UserGen(accountCache, srvAddr);
-
+      String[] domains = cfg.getStringList("sendemail", null, "allowedDomain");
+      Pattern domainPattern = MailUtil.glob(domains);
+      ParameterizedString namePattern =
+          new ParameterizedString("${user} (Code Review)");
+      generator = new UserGen(accountCache, domainPattern, anonymousCowardName,
+          namePattern, srvAddr);
     } else if ("SERVER".equalsIgnoreCase(from)) {
       generator = new ServerGen(srvAddr);
-
     } else {
       final Address a = Address.parse(from);
       final ParameterizedString name = a.name != null ? new ParameterizedString(a.name) : null;
@@ -84,11 +87,31 @@
 
   static final class UserGen implements FromAddressGenerator {
     private final AccountCache accountCache;
-    private final Address srvAddr;
+    private final Pattern domainPattern;
+    private final String anonymousCowardName;
+    private final ParameterizedString nameRewriteTmpl;
+    private final Address serverAddress;
 
-    UserGen(AccountCache accountCache, Address srvAddr) {
+    /**
+     * From address generator for USER mode
+     *
+     * @param accountCache get user account from id
+     * @param domainPattern allowed user domain pattern that Gerrit can send as
+     *        the user
+     * @param anonymousCowardName name used when user's full name is missing
+     * @param nameRewriteTmpl name template used for rewriting the sender's name
+     *        when Gerrit can not send as the user
+     * @param serverAddress serverAddress.name is used when fromId is null and
+     *        serverAddress.email is used when Gerrit can not send as the user
+     */
+    UserGen(AccountCache accountCache, Pattern domainPattern,
+        String anonymousCowardName, ParameterizedString nameRewriteTmpl,
+        Address serverAddress) {
       this.accountCache = accountCache;
-      this.srvAddr = srvAddr;
+      this.domainPattern = domainPattern;
+      this.anonymousCowardName = anonymousCowardName;
+      this.nameRewriteTmpl = nameRewriteTmpl;
+      this.serverAddress = serverAddress;
     }
 
     @Override
@@ -98,14 +121,44 @@
 
     @Override
     public Address from(final Account.Id fromId) {
+      String senderName;
       if (fromId != null) {
         Account a = accountCache.get(fromId).getAccount();
+        String fullName = a.getFullName();
         String userEmail = a.getPreferredEmail();
-        return new Address(
-            a.getFullName(),
-            userEmail != null ? userEmail : srvAddr.getEmail());
+        if (canRelay(userEmail)) {
+          return new Address(fullName, userEmail);
+        }
+
+        if (fullName == null || "".equals(fullName.trim())) {
+          fullName = anonymousCowardName;
+        }
+        senderName = nameRewriteTmpl.replace("user", fullName).toString();
+      } else {
+        senderName = serverAddress.name;
       }
-      return srvAddr;
+
+      String senderEmail;
+      ParameterizedString senderEmailPattern =
+          new ParameterizedString(serverAddress.email);
+      if (senderEmailPattern.getParameterNames().isEmpty()) {
+        senderEmail = senderEmailPattern.getRawPattern();
+      } else {
+        senderEmail = senderEmailPattern.replace("userHash", hashOf(senderName))
+            .toString();
+      }
+      return new Address(senderName, senderEmail);
+    }
+
+    /** check if Gerrit is allowed to send from {@code userEmail}. */
+    private boolean canRelay(String userEmail) {
+      if (userEmail != null) {
+        int index = userEmail.indexOf('@');
+        if (index > 0 && index < userEmail.length() - 1) {
+          return domainPattern.matcher(userEmail.substring(index + 1)).matches();
+        }
+      }
+      return false;
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java
new file mode 100644
index 0000000..320c09b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2016 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.mail;
+
+import com.google.common.io.CharStreams;
+import com.google.common.io.Resources;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+
+import com.google.template.soy.SoyFileSet;
+import com.google.template.soy.tofu.SoyTofu;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.ClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.charset.StandardCharsets;
+
+/** Configures Soy Tofu object for rendering email templates. */
+@Singleton
+public class MailSoyTofuProvider implements Provider<SoyTofu> {
+
+  // Note: will fail to construct the tofu object if this array is empty.
+  private static final String[] TEMPLATES = {
+    "footer.soy",
+  };
+
+  private final SitePaths site;
+
+  @Inject
+  MailSoyTofuProvider(SitePaths site) {
+    this.site = site;
+  }
+
+  @Override
+  public SoyTofu get() throws ProvisionException {
+    SoyFileSet.Builder builder = new SoyFileSet.Builder();
+    for (String name : TEMPLATES) {
+      addTemplate(builder, name);
+    }
+    return builder.build().compileToTofu();
+  }
+
+  private void addTemplate(SoyFileSet.Builder builder, String name)
+      throws ProvisionException {
+    // Load as a file in the mail templates directory if present.
+    Path tmpl = site.mail_dir.resolve(name);
+    if (Files.isRegularFile(tmpl)) {
+      String content;
+      try (Reader r = Files.newBufferedReader(tmpl, StandardCharsets.UTF_8)) {
+        content = CharStreams.toString(r);
+      } catch (IOException err) {
+        throw new ProvisionException("Failed to read template file " +
+            tmpl.toAbsolutePath().toString(), err);
+      }
+      builder.add(content, tmpl.toAbsolutePath().toString());
+      return;
+    }
+
+    // Otherwise load the template as a resource.
+    String resourcePath = "com/google/gerrit/server/mail/" + name;
+    builder.add(Resources.getResource(resourcePath));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailTemplates.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailTemplates.java
new file mode 100644
index 0000000..72fdaae
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailTemplates.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 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.mail;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface MailTemplates {}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
index a1274c3..8a132cd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gwtorm.server.OrmException;
@@ -31,21 +32,21 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 public class MailUtil {
-
   public static MailRecipients getRecipientsFromFooters(
-      AccountResolver accountResolver, boolean draftPatchSet,
+      ReviewDb db, AccountResolver accountResolver, boolean draftPatchSet,
       List<FooterLine> footerLines) throws OrmException {
     MailRecipients recipients = new MailRecipients();
     if (!draftPatchSet) {
       for (FooterLine footerLine : footerLines) {
         try {
           if (isReviewer(footerLine)) {
-            recipients.reviewers.add(toAccountId(accountResolver, footerLine
+            recipients.reviewers.add(toAccountId(db, accountResolver, footerLine
                 .getValue().trim()));
           } else if (footerLine.matches(FooterKey.CC)) {
-            recipients.cc.add(toAccountId(accountResolver, footerLine
+            recipients.cc.add(toAccountId(db, accountResolver, footerLine
                 .getValue().trim()));
           }
         } catch (NoSuchAccountException e) {
@@ -64,9 +65,10 @@
     return recipients;
   }
 
-  private static Account.Id toAccountId(final AccountResolver accountResolver,
-      final String nameOrEmail) throws OrmException, NoSuchAccountException {
-    final Account a = accountResolver.findByNameOrEmail(nameOrEmail);
+  private static Account.Id toAccountId(ReviewDb db,
+      AccountResolver accountResolver, String nameOrEmail)
+      throws OrmException, NoSuchAccountException {
+    Account a = accountResolver.findByNameOrEmail(db, nameOrEmail);
     if (a == null) {
       throw new NoSuchAccountException("\"" + nameOrEmail
           + "\" is not registered");
@@ -123,4 +125,19 @@
       return Collections.unmodifiableSet(all);
     }
   }
+
+  /** allow wildcard matching for {@code domains} */
+  public static Pattern glob(String[] domains) {
+    // if domains is not set, match anything
+    if (domains == null || domains.length == 0) {
+      return Pattern.compile(".*");
+    }
+
+    StringBuilder sb = new StringBuilder("");
+    for (String domain : domains) {
+      String quoted = "\\Q" + domain.replace("\\E", "\\E\\\\E\\Q") + "\\E|";
+      sb.append(quoted.replace("*", "\\E.*\\Q"));
+    }
+    return Pattern.compile(sb.substring(0, sb.length() - 1));
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index 04085b6..bc234ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2016 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.
@@ -19,7 +19,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.UserIdentity;
@@ -28,9 +28,9 @@
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gwtorm.server.OrmException;
+import com.google.template.soy.tofu.SoyTofu;
 
 import org.apache.commons.lang.StringUtils;
-import org.apache.commons.validator.routines.EmailValidator;
 import org.apache.velocity.Template;
 import org.apache.velocity.VelocityContext;
 import org.apache.velocity.context.InternalContextAdapterImpl;
@@ -66,7 +66,7 @@
   private Address smtpFromAddress;
   private StringBuilder body;
   protected VelocityContext velocityContext;
-
+  protected Map<String, Object> soyContext;
   protected final EmailArguments args;
   protected Account.Id fromId;
   protected NotifyHandling notify = NotifyHandling.ALL;
@@ -165,6 +165,7 @@
    */
   protected void init() throws EmailException {
     setupVelocityContext();
+    setupSoyContext();
 
     smtpFromAddress = args.fromAddressGenerator.from(fromId);
     setHeader("Date", new Date());
@@ -393,7 +394,7 @@
   /** Schedule delivery of this message to the given account. */
   protected void add(final RecipientType rt, final Address addr) {
     if (addr != null && addr.email != null && addr.email.length() > 0) {
-      if (!EmailValidator.getInstance().isValid(addr.email)) {
+      if (!OutgoingEmailValidator.isValid(addr.email)) {
         log.warn("Not emailing " + addr.email + " (invalid email address)");
       } else if (!args.emailSender.canEmail(addr.email)) {
         log.warn("Not emailing " + addr.email + " (prohibited by allowrcpt)");
@@ -429,6 +430,11 @@
     velocityContext.put("StringUtils", StringUtils.class);
   }
 
+  protected void setupSoyContext() {
+    soyContext = new LinkedHashMap<String, Object>();
+    // TODO(wyatta): set data here.
+  }
+
   protected String velocify(String template) throws EmailException {
     try {
       RuntimeInstance runtime = args.velocityRuntime;
@@ -464,6 +470,13 @@
     }
   }
 
+  protected String soyFile(String name) throws EmailException {
+    return args.soyTofu
+        .newRenderer("com.google.gerrit.server.mail.template." + name)
+        .setData(soyContext)
+        .render();
+  }
+
   public String joinStrings(Iterable<Object> in, String joiner) {
     return joinStrings(in.iterator(), joiner);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmailValidator.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmailValidator.java
new file mode 100644
index 0000000..5ab5f4e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmailValidator.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2016 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.mail;
+
+import static org.apache.commons.validator.routines.DomainValidator.ArrayType.GENERIC_PLUS;
+
+import org.apache.commons.validator.routines.DomainValidator;
+import org.apache.commons.validator.routines.EmailValidator;
+
+public class OutgoingEmailValidator {
+  static {
+    DomainValidator.updateTLDOverride(GENERIC_PLUS, new String[]{"local"});
+  }
+
+  public static boolean isValid(String addr) {
+    return EmailValidator.getInstance(true, true).isValid(addr);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
index 6e4e1d4..c1ead9f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
@@ -27,6 +27,8 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.Predicate;
@@ -42,6 +44,7 @@
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public class ProjectWatch {
@@ -62,6 +65,62 @@
 
   /** Returns all watchers that are relevant */
   public final Watchers getWatchers(NotifyType type) throws OrmException {
+    Watchers matching;
+    if (args.accountIndexes.getSearchIndex() != null) {
+      matching = getWatchersFromIndex(type);
+    } else {
+      matching = getWatchersFromDb(type);
+    }
+
+    for (ProjectState state : projectState.tree()) {
+      for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
+        if (nc.isNotify(type)) {
+          try {
+            add(matching, nc);
+          } catch (QueryParseException e) {
+            log.warn("Project {} has invalid notify {} filter \"{}\": {}",
+                state.getProject().getName(), nc.getName(),
+                nc.getFilter(), e.getMessage());
+          }
+        }
+      }
+    }
+
+    return matching;
+  }
+
+  private Watchers getWatchersFromIndex(NotifyType type)
+      throws OrmException {
+    Watchers matching = new Watchers();
+    Set<Account.Id> projectWatchers = new HashSet<>();
+
+    for (AccountState a : args.accountQueryProvider.get()
+        .byWatchedProject(project)) {
+      Account.Id accountId = a.getAccount().getId();
+      for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e :
+          a.getProjectWatches().entrySet()) {
+        if (add(matching, accountId, e.getKey(), e.getValue(), type)) {
+          // We only want to prevent matching All-Projects if this filter hits
+          projectWatchers.add(accountId);
+        }
+      }
+    }
+
+    for (AccountState a : args.accountQueryProvider.get()
+        .byWatchedProject(args.allProjectsName)) {
+      for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e :
+        a.getProjectWatches().entrySet()) {
+        Account.Id accountId = a.getAccount().getId();
+        if (!projectWatchers.contains(accountId)) {
+          add(matching, accountId, e.getKey(), e.getValue(), type);
+        }
+      }
+    }
+    return matching;
+  }
+
+  private Watchers getWatchersFromDb(NotifyType type)
+      throws OrmException {
     Watchers matching = new Watchers();
     Set<Account.Id> projectWatchers = new HashSet<>();
 
@@ -79,21 +138,6 @@
         add(matching, w, type);
       }
     }
-
-    for (ProjectState state : projectState.tree()) {
-      for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
-        if (nc.isNotify(type)) {
-          try {
-            add(matching, nc);
-          } catch (QueryParseException e) {
-            log.warn("Project {} has invalid notify {} filter \"{}\": {}",
-                state.getProject().getName(), nc.getName(),
-                nc.getFilter(), e.getMessage());
-          }
-        }
-      }
-    }
-
     return matching;
   }
 
@@ -172,6 +216,26 @@
     }
   }
 
+  private boolean add(Watchers matching, Account.Id accountId,
+      ProjectWatchKey key, Set<NotifyType> watchedTypes, NotifyType type)
+      throws OrmException {
+    IdentifiedUser user = args.identifiedUserFactory.create(accountId);
+
+    try {
+      if (filterMatch(user, key.filter())) {
+        // If we are set to notify on this type, add the user.
+        // Otherwise, still return true to stop notifications for this user.
+        if (watchedTypes.contains(type)) {
+          matching.bcc.accounts.add(accountId);
+        }
+        return true;
+      }
+    } catch (QueryParseException e) {
+      // Ignore broken filter expressions.
+    }
+    return false;
+  }
+
   private boolean add(Watchers matching, AccountProjectWatch w, NotifyType type)
       throws OrmException {
     IdentifiedUser user = args.identifiedUserFactory.create(w.getAccountId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
index 101aaac..3fdc550 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
@@ -18,6 +18,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
 
 import org.apache.velocity.runtime.RuntimeConstants;
 import org.apache.velocity.runtime.RuntimeInstance;
@@ -30,6 +31,7 @@
 import java.util.Properties;
 
 /** Configures Velocity template engine for sending email. */
+@Singleton
 public class VelocityRuntimeProvider implements Provider<RuntimeInstance> {
   private final SitePaths site;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 8b620c0..679a9de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -83,8 +83,13 @@
   @AutoValue
   public abstract static class LoadHandle implements AutoCloseable {
     public static LoadHandle create(ChangeNotesRevWalk walk, ObjectId id) {
+      if (ObjectId.zeroId().equals(id)) {
+        id = null;
+      } else if (id != null) {
+        id = id.copy();
+      }
       return new AutoValue_AbstractChangeNotes_LoadHandle(
-          checkNotNull(walk), id != null ? id.copy() : null);
+          checkNotNull(walk), id);
     }
 
     public static LoadHandle missing() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index f9bad6f..4c1a734 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -82,6 +82,7 @@
   private static final String FILE = "File";
   private static final String LENGTH = "Bytes";
   private static final String PARENT = "Parent";
+  private static final String PARENT_NUMBER = "Parent-number";
   private static final String PATCH_SET = "Patch-set";
   private static final String REVISION = "Revision";
   private static final String UUID = "UUID";
@@ -151,11 +152,13 @@
     int sizeOfNote = note.length;
     byte[] psb = PATCH_SET.getBytes(UTF_8);
     byte[] bpsb = BASE_PATCH_SET.getBytes(UTF_8);
+    byte[] bpn = PARENT_NUMBER.getBytes(UTF_8);
 
     RevId revId = new RevId(parseStringField(note, p, changeId, REVISION));
     String fileName = null;
     PatchSet.Id psId = null;
     boolean isForBase = false;
+    Integer parentNumber = null;
 
     while (p.value < sizeOfNote) {
       boolean matchPs = match(note, p, psb);
@@ -168,13 +171,16 @@
         fileName = null;
         psId = parsePsId(note, p, changeId, BASE_PATCH_SET);
         isForBase = true;
+        if (match(note, p, bpn)) {
+          parentNumber = parseParentNumber(note, p, changeId);
+        }
       } else if (psId == null) {
         throw parseException(changeId, "missing %s or %s header",
             PATCH_SET, BASE_PATCH_SET);
       }
 
-      PatchLineComment c =
-          parseComment(note, p, fileName, psId, revId, isForBase, status);
+      PatchLineComment c = parseComment(
+          note, p, fileName, psId, revId, isForBase, parentNumber, status);
       fileName = c.getKey().getParentKey().getFileName();
       if (!seen.add(c.getKey())) {
         throw parseException(
@@ -187,7 +193,7 @@
 
   private PatchLineComment parseComment(byte[] note, MutableInteger curr,
       String currentFileName, PatchSet.Id psId, RevId revId, boolean isForBase,
-      Status status) throws ConfigInvalidException {
+      Integer parentNumber, Status status) throws ConfigInvalidException {
     Change.Id changeId = psId.getParentKey();
 
     // Check if there is a new file.
@@ -235,7 +241,13 @@
         range.getEndLine(), aId, parentUUID, commentTime);
     plc.setMessage(message);
     plc.setTag(tag);
-    plc.setSide((short) (isForBase ? 0 : 1));
+
+    if (isForBase) {
+      plc.setSide((short) (parentNumber == null ? 0 : -parentNumber));
+    } else {
+      plc.setSide((short) 1);
+    }
+
     if (range.getStartCharacter() != -1) {
       plc.setRange(range);
     }
@@ -333,6 +345,23 @@
     return new PatchSet.Id(changeId, patchSetId);
   }
 
+  private static Integer parseParentNumber(byte[] note, MutableInteger curr,
+      Change.Id changeId) throws ConfigInvalidException {
+    checkHeaderLineFormat(note, curr, PARENT_NUMBER, changeId);
+
+    int start = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
+    MutableInteger i = new MutableInteger();
+    int parentNumber = RawParseUtils.parseBase10(note, start, i);
+    int endOfLine = RawParseUtils.nextLF(note, curr.value);
+    if (i.value != endOfLine - 1) {
+      throw parseException(changeId, "could not parse %s", PARENT_NUMBER);
+    }
+    checkResult(parentNumber, "parent number", changeId);
+    curr.value = endOfLine;
+    return Integer.valueOf(parentNumber);
+
+  }
+
   private static String parseFilename(byte[] note, MutableInteger curr,
       Change.Id changeId) throws ConfigInvalidException {
     checkHeaderLineFormat(note, curr, FILE, changeId);
@@ -461,10 +490,13 @@
         PatchLineComment first = psComments.get(0);
 
         short side = first.getSide();
-        appendHeaderField(writer, side == 0
+        appendHeaderField(writer, side <= 0
             ? BASE_PATCH_SET
             : PATCH_SET,
             Integer.toString(psId.get()));
+        if (side < 0) {
+          appendHeaderField(writer, PARENT_NUMBER, Integer.toString(-side));
+        }
 
         String currentFilename = null;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 82ed02a..6327682 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -49,6 +49,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.git.RefCache;
 import com.google.gerrit.server.git.RepoRefCache;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -75,6 +76,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /** View of a single {@link Change} based on the log of its notes branch. */
 public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
@@ -149,7 +151,7 @@
       return changes.get(0).notes();
     }
 
-    public ChangeNotes create(ReviewDb db, Project.NameKey project,
+    private Change loadChangeFromDb(ReviewDb db, Project.NameKey project,
         Change.Id changeId) throws OrmException {
       Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId);
       checkNotNull(change,
@@ -160,7 +162,19 @@
           project, changeId, change.getProject());
       // TODO: Throw NoSuchChangeException when the change is not found in the
       // database
-      return new ChangeNotes(args, change).load();
+      return change;
+    }
+
+    public ChangeNotes create(ReviewDb db, Project.NameKey project,
+        Change.Id changeId) throws OrmException {
+      return new ChangeNotes(args, loadChangeFromDb(db, project, changeId))
+          .load();
+    }
+
+    public ChangeNotes createWithAutoRebuildingDisabled(ReviewDb db,
+        Project.NameKey project, Change.Id changeId) throws OrmException {
+      return new ChangeNotes(
+          args, loadChangeFromDb(db, project, changeId), false, null).load();
     }
 
     /**
@@ -380,6 +394,10 @@
     return state.reviewers();
   }
 
+  public ImmutableList<ReviewerStatusUpdate> getReviewerUpdates() {
+    return state.reviewerUpdates();
+  }
+
   /**
    *
    * @return a ImmutableSet of all hashtags for this change sorted in alphabetical order.
@@ -547,37 +565,41 @@
 
   private LoadHandle rebuildAndOpen(Repository repo, ObjectId oldId)
       throws IOException {
-    try (Timer1.Context timer =
-        args.metrics.autoRebuildLatency.start(CHANGES)) {
+    Timer1.Context timer = args.metrics.autoRebuildLatency.start(CHANGES);
+    try {
       Change.Id cid = getChangeId();
       ReviewDb db = args.db.get();
       ChangeRebuilder rebuilder = args.rebuilder.get();
-      NoteDbUpdateManager manager = rebuilder.stage(db, cid);
-      if (manager == null) {
-        return super.openHandle(repo, oldId); // May be null in tests.
-      }
-      NoteDbUpdateManager.Result r = manager.stageAndApplyDelta(change);
-      try {
-        rebuilder.execute(db, cid, manager);
-        repo.scanForRepoChanges();
-      } catch (OrmException | IOException e) {
-        // Rebuilding failed. Most likely cause is contention on one or more
-        // change refs; there are other types of errors that can happen during
-        // rebuilding, but generally speaking they should happen during stage(),
-        // not execute(). Assume that some other worker is going to successfully
-        // store the rebuilt state, which is deterministic given an input
-        // ChangeBundle.
-        //
-        // Parse notes from the staged result so we can return something useful
-        // to the caller instead of throwing.
-        args.metrics.autoRebuildFailureCount.increment(CHANGES);
-        rebuildResult = checkNotNull(r);
-        checkNotNull(r.newState());
-        checkNotNull(r.staged());
-        return LoadHandle.create(
-            ChangeNotesCommit.newStagedRevWalk(
-                repo, r.staged().changeObjects()),
-            r.newState().getChangeMetaId());
+      NoteDbUpdateManager.Result r;
+      try (NoteDbUpdateManager manager = rebuilder.stage(db, cid)) {
+        if (manager == null) {
+          return super.openHandle(repo, oldId); // May be null in tests.
+        }
+        r = manager.stageAndApplyDelta(change);
+        try {
+          rebuilder.execute(db, cid, manager);
+          repo.scanForRepoChanges();
+        } catch (OrmException | IOException e) {
+          // Rebuilding failed. Most likely cause is contention on one or more
+          // change refs; there are other types of errors that can happen during
+          // rebuilding, but generally speaking they should happen during stage(),
+          // not execute(). Assume that some other worker is going to successfully
+          // store the rebuilt state, which is deterministic given an input
+          // ChangeBundle.
+          //
+          // Parse notes from the staged result so we can return something useful
+          // to the caller instead of throwing.
+          log.debug("Rebuilding change {} failed: {}",
+              getChangeId(), e.getMessage());
+          args.metrics.autoRebuildFailureCount.increment(CHANGES);
+          rebuildResult = checkNotNull(r);
+          checkNotNull(r.newState());
+          checkNotNull(r.staged());
+          return LoadHandle.create(
+              ChangeNotesCommit.newStagedRevWalk(
+                  repo, r.staged().changeObjects()),
+              r.newState().getChangeMetaId());
+        }
       }
       return LoadHandle.create(
           ChangeNotesCommit.newRevWalk(repo), r.newState().getChangeMetaId());
@@ -585,6 +607,10 @@
       return super.openHandle(repo, oldId);
     } catch (OrmException e) {
       throw new IOException(e);
+    } finally {
+      log.debug("Rebuilt change {} in project {} in {} ms",
+          getChangeId(), getProjectName(),
+          TimeUnit.MILLISECONDS.convert(timer.stop(), TimeUnit.NANOSECONDS));
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 3f93f1e..8272aaf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -60,6 +60,7 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.util.LabelVote;
 
@@ -79,12 +80,14 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.NavigableSet;
+import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
 
@@ -105,9 +108,11 @@
   // in during the parsing process.
   private final Table<Account.Id, ReviewerStateInternal, Timestamp> reviewers;
   private final List<Account.Id> allPastReviewers;
+  private final List<ReviewerStatusUpdate> reviewerUpdates;
   private final List<SubmitRecord> submitRecords;
   private final Multimap<RevId, PatchLineComment> comments;
   private final TreeMap<PatchSet.Id, PatchSet> patchSets;
+  private final Set<PatchSet.Id> deletedPatchSets;
   private final Map<PatchSet.Id, PatchSetState> patchSetStates;
   private final Map<PatchSet.Id,
       Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>>> approvals;
@@ -140,11 +145,13 @@
     approvals = new HashMap<>();
     reviewers = HashBasedTable.create();
     allPastReviewers = new ArrayList<>();
+    reviewerUpdates = new ArrayList<>();
     submitRecords = Lists.newArrayListWithExpectedSize(1);
     allChangeMessages = new ArrayList<>();
     changeMessagesByPatchSet = LinkedListMultimap.create();
     comments = ArrayListMultimap.create();
     patchSets = Maps.newTreeMap(ReviewDbUtil.intKeyOrdering());
+    deletedPatchSets = new HashSet<>();
     patchSetStates = new HashMap<>();
   }
 
@@ -195,6 +202,7 @@
         buildApprovals(),
         ReviewerSet.fromTable(Tables.transpose(reviewers)),
         allPastReviewers,
+        buildReviewerUpdates(),
         submitRecords,
         buildAllMessages(),
         buildMessagesByPatchSet(),
@@ -217,6 +225,19 @@
     return result;
   }
 
+  private List<ReviewerStatusUpdate> buildReviewerUpdates() {
+    List<ReviewerStatusUpdate> result = new ArrayList<>();
+    HashMap<Account.Id, ReviewerStateInternal> lastState = new HashMap<>();
+    for (ReviewerStatusUpdate u : Lists.reverse(reviewerUpdates)) {
+      if (!Objects.equals(ownerId, u.reviewer()) &&
+          lastState.get(u.reviewer()) != u.state()) {
+        result.add(u);
+        lastState.put(u.reviewer(), u.state());
+      }
+    }
+    return result;
+  }
+
   private List<ChangeMessage> buildAllMessages() {
     return Lists.reverse(allChangeMessages);
   }
@@ -249,8 +270,13 @@
     }
 
     PatchSetState psState = parsePatchSetState(commit);
-    if (psState != null && !patchSetStates.containsKey(psId)) {
-      patchSetStates.put(psId, psState);
+    if (psState != null) {
+      if (!patchSetStates.containsKey(psId)) {
+        patchSetStates.put(psId, psState);
+      }
+      if (psState == PatchSetState.DELETED) {
+        deletedPatchSets.add(psId);
+      }
     }
 
     Account.Id accountId = parseIdent(commit);
@@ -381,7 +407,12 @@
     if (ps == null) {
       ps = new PatchSet(psId);
       patchSets.put(psId, ps);
-    } else if (ps.getRevision() != PARTIAL_PATCH_SET) {
+    } else if (!ps.getRevision().equals(PARTIAL_PATCH_SET)) {
+      if (deletedPatchSets.contains(psId)) {
+        // Do not update PS details as PS was deleted and this meta data is of
+        // no relevance
+        return;
+      }
       throw new ConfigInvalidException(
           String.format(
               "Multiple revisions parsed for patch set %s: %s and %s",
@@ -742,6 +773,8 @@
       throw invalidFooter(state.getFooterKey(), line);
     }
     Account.Id accountId = noteUtil.parseIdent(ident, id);
+    reviewerUpdates.add(
+        ReviewerStatusUpdate.create(ts, ownerId, accountId, state));
     if (!reviewers.containsRow(accountId)) {
       reviewers.put(accountId, state, ts);
     }
@@ -763,7 +796,7 @@
 
   private void updatePatchSetStates() throws ConfigInvalidException {
     for (PatchSet ps : patchSets.values()) {
-      if (ps.getRevision() == PARTIAL_PATCH_SET) {
+      if (ps.getRevision().equals(PARTIAL_PATCH_SET)) {
         throw parseException("No %s found for patch set %s",
             FOOTER_COMMIT, ps.getPatchSetId());
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index f84f9a1..988184f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.ReviewerStatusUpdate;
 
 import java.sql.Timestamp;
 import java.util.List;
@@ -63,6 +64,7 @@
         ImmutableListMultimap.<PatchSet.Id, PatchSetApproval>of(),
         ReviewerSet.empty(),
         ImmutableList.<Account.Id>of(),
+        ImmutableList.<ReviewerStatusUpdate>of(),
         ImmutableList.<SubmitRecord>of(),
         ImmutableList.<ChangeMessage>of(),
         ImmutableListMultimap.<PatchSet.Id, ChangeMessage>of(),
@@ -87,6 +89,7 @@
       Multimap<PatchSet.Id, PatchSetApproval> approvals,
       ReviewerSet reviewers,
       List<Account.Id> allPastReviewers,
+      List<ReviewerStatusUpdate> reviewerUpdates,
       List<SubmitRecord> submitRecords,
       List<ChangeMessage> allChangeMessages,
       Multimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet,
@@ -113,6 +116,7 @@
         ImmutableListMultimap.copyOf(approvals),
         reviewers,
         ImmutableList.copyOf(allPastReviewers),
+        ImmutableList.copyOf(reviewerUpdates),
         ImmutableList.copyOf(submitRecords),
         ImmutableList.copyOf(allChangeMessages),
         ImmutableListMultimap.copyOf(changeMessagesByPatchSet),
@@ -152,8 +156,11 @@
   abstract ImmutableSet<String> hashtags();
   abstract ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets();
   abstract ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals();
+
   abstract ReviewerSet reviewers();
   abstract ImmutableList<Account.Id> allPastReviewers();
+  abstract ImmutableList<ReviewerStatusUpdate> reviewerUpdates();
+
   abstract ImmutableList<SubmitRecord> submitRecords();
   abstract ImmutableList<ChangeMessage> allChangeMessages();
   abstract ImmutableListMultimap<PatchSet.Id, ChangeMessage>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
index ee64376..679b5e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
@@ -32,6 +32,15 @@
 import java.util.concurrent.Callable;
 
 public abstract class ChangeRebuilder {
+  public static class NoPatchSetsException extends OrmException {
+    private static final long serialVersionUID = 1L;
+
+    NoPatchSetsException(Change.Id changeId) {
+      super("Change " + changeId
+          + " cannot be rebuilt because it has no patch sets");
+    }
+  }
+
   private final SchemaFactory<ReviewDb> schemaFactory;
 
   protected ChangeRebuilder(SchemaFactory<ReviewDb> schemaFactory) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
index a181431..08acbad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
@@ -161,10 +161,11 @@
     if (change == null) {
       throw new NoSuchChangeException(changeId);
     }
-    NoteDbUpdateManager manager =
-        updateManagerFactory.create(change.getProject());
-    buildUpdates(manager, ChangeBundle.fromReviewDb(db, changeId));
-    return execute(db, changeId, manager);
+    try (NoteDbUpdateManager manager =
+        updateManagerFactory.create(change.getProject())) {
+      buildUpdates(manager, ChangeBundle.fromReviewDb(db, changeId));
+      return execute(db, changeId, manager);
+    }
   }
 
   private static class AbortUpdateException extends OrmRuntimeException {
@@ -175,6 +176,16 @@
     }
   }
 
+  private static class ConflictingUpdateException extends OrmRuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    ConflictingUpdateException(Change change, String expectedNoteDbState) {
+      super(String.format(
+          "Expected change %s to have noteDbState %s but was %s",
+          change.getId(), expectedNoteDbState, change.getNoteDbState()));
+    }
+  }
+
   @Override
   public Result rebuild(NoteDbUpdateManager manager,
       ChangeBundle bundle) throws NoSuchChangeException, IOException,
@@ -216,24 +227,44 @@
       db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
         @Override
         public Change update(Change change) {
-          if (!Objects.equals(oldNoteDbState, change.getNoteDbState())) {
+          String currNoteDbState = change.getNoteDbState();
+          if (Objects.equals(currNoteDbState, newNoteDbState)) {
+            // Another thread completed the same rebuild we were about to.
             throw new AbortUpdateException();
+          } else if (!Objects.equals(oldNoteDbState, currNoteDbState)) {
+            // Another thread updated the state to something else.
+            throw new ConflictingUpdateException(change, oldNoteDbState);
           }
           change.setNoteDbState(newNoteDbState);
           return change;
         }
       });
-      if (!migration.failChangeWrites()) {
-        manager.execute();
-      } else {
-        // Don't even attempt to execute if read-only, it would fail anyway. But
-        // do throw an exception to the caller so they know to use the staged
-        // results instead of reading from the repo.
-        throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY);
-      }
+    } catch (ConflictingUpdateException e) {
+      // Rethrow as an OrmException so the caller knows to use staged results.
+      // Strictly speaking they are not completely up to date, but result we
+      // send to the caller is the same as if this rebuild had executed before
+      // the other thread.
+      throw new OrmException(e.getMessage());
     } catch (AbortUpdateException e) {
-      // Drop this rebuild; another thread completed it.
+      if (NoteDbChangeState.parse(changeId, newNoteDbState).isUpToDate(
+          manager.getChangeRepo().cmds.getRepoRefCache(),
+          manager.getAllUsersRepo().cmds.getRepoRefCache())) {
+        // If the state in ReviewDb matches NoteDb at this point, it means
+        // another thread successfully completed this rebuild. It's ok to not
+        // execute the update in this case, since the object referenced in the
+        // Result was flushed to the repo by whatever thread won the race.
+        return r;
+      }
+      // If the state doesn't match, that means another thread attempted this
+      // rebuild, but failed. Fall through and try to update the ref again.
     }
+    if (migration.failChangeWrites()) {
+      // Don't even attempt to execute if read-only, it would fail anyway. But
+      // do throw an exception to the caller so they know to use the staged
+      // results instead of reading from the repo.
+      throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY);
+    }
+    manager.execute();
     return r;
   }
 
@@ -246,16 +277,18 @@
     checkArgument(allChanges.containsKey(project));
     boolean ok = true;
     ProgressMonitor pm = new TextProgressMonitor(new PrintWriter(System.out));
-    NoteDbUpdateManager manager = updateManagerFactory.create(project);
     pm.beginTask(
         FormatUtil.elide(project.get(), 50), allChanges.get(project).size());
-    try (ObjectInserter allUsersInserter = allUsersRepo.newObjectInserter();
+    try (NoteDbUpdateManager manager = updateManagerFactory.create(project);
+        ObjectInserter allUsersInserter = allUsersRepo.newObjectInserter();
         RevWalk allUsersRw = new RevWalk(allUsersInserter.newReader())) {
       manager.setAllUsersRepo(allUsersRepo, allUsersRw, allUsersInserter,
           new ChainedReceiveCommands(allUsersRepo));
       for (Change.Id changeId : allChanges.get(project)) {
         try {
           buildUpdates(manager, ChangeBundle.fromReviewDb(db, changeId));
+        } catch (NoPatchSetsException e) {
+          log.warn(e.getMessage());
         } catch (Throwable t) {
           log.error("Failed to rebuild change " + changeId, t);
           ok = false;
@@ -273,6 +306,10 @@
       throws IOException, OrmException {
     manager.setCheckExpectedState(false);
     Change change = new Change(bundle.getChange());
+    if (bundle.getPatchSets().isEmpty()) {
+      throw new NoPatchSetsException(change.getId());
+    }
+
     PatchSet.Id currPsId = change.currentPatchSetId();
     // We will rebuild all events, except for draft comments, in buckets based
     // on author and timestamp.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index d9aa532..77b8dc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -55,6 +55,7 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.LabelVote;
+import com.google.gerrit.server.util.RequestId;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -218,11 +219,12 @@
   }
 
   public ObjectId commit() throws IOException, OrmException {
-    NoteDbUpdateManager updateManager =
-        updateManagerFactory.create(getProjectName());
-    updateManager.add(this);
-    updateManager.stageAndApplyDelta(getChange());
-    updateManager.execute();
+    try (NoteDbUpdateManager updateManager =
+        updateManagerFactory.create(getProjectName())) {
+      updateManager.add(this);
+      updateManager.stageAndApplyDelta(getChange());
+      updateManager.execute();
+    }
     return getResult();
   }
 
@@ -264,9 +266,10 @@
     approvals.put(label, reviewer, Optional.<Short> absent());
   }
 
-  public void merge(String submissionId, Iterable<SubmitRecord> submitRecords) {
+  public void merge(RequestId submissionId,
+      Iterable<SubmitRecord> submitRecords) {
     this.status = Change.Status.MERGED;
-    this.submissionId = submissionId;
+    this.submissionId = submissionId.toStringForStorage();
     this.submitRecords = ImmutableList.copyOf(submitRecords);
     checkArgument(!this.submitRecords.isEmpty(),
         "no submit records specified at submit time");
@@ -376,6 +379,10 @@
     this.hashtags = hashtags;
   }
 
+  public Map<Account.Id, ReviewerStateInternal> getReviewers() {
+    return reviewers;
+  }
+
   public void putReviewer(Account.Id reviewer, ReviewerStateInternal type) {
     checkArgument(type != ReviewerStateInternal.REMOVED, "invalid ReviewerType");
     reviewers.put(reviewer, type);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
index c13ccb2..802359c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.server.notedb.NoteDbTable.ACCOUNTS;
 import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
@@ -50,18 +51,19 @@
   private static final String NOTE_DB = "noteDb";
   private static final String READ = "read";
   private static final String WRITE = "write";
+  private static final String SEQUENCE = "sequence";
 
   private static void checkConfig(Config cfg) {
     Set<String> keys = new HashSet<>();
     for (NoteDbTable t : NoteDbTable.values()) {
       keys.add(t.key());
     }
+    Set<String> allowed = ImmutableSet.of(READ, WRITE, SEQUENCE);
     for (String t : cfg.getSubsections(NOTE_DB)) {
       checkArgument(keys.contains(t.toLowerCase()),
           "invalid NoteDb table: %s", t);
       for (String key : cfg.getNames(NOTE_DB, t)) {
-        String lk = key.toLowerCase();
-        checkArgument(lk.equals(WRITE) || lk.equals(READ),
+        checkArgument(allowed.contains(key.toLowerCase()),
             "invalid NoteDb key: %s.%s", t, key);
       }
     }
@@ -78,14 +80,24 @@
 
   private final boolean writeChanges;
   private final boolean readChanges;
+  private final boolean readChangeSequence;
+
   private final boolean writeAccounts;
   private final boolean readAccounts;
 
   @Inject
   ConfigNotesMigration(@GerritServerConfig Config cfg) {
     checkConfig(cfg);
+
     writeChanges = cfg.getBoolean(NOTE_DB, CHANGES.key(), WRITE, false);
     readChanges = cfg.getBoolean(NOTE_DB, CHANGES.key(), READ, false);
+
+    // Reading change sequence numbers from NoteDb is not the default even if
+    // reading changes themselves is. Once this is enabled, it's not easy to
+    // undo: ReviewDb might hand out numbers that have already been assigned by
+    // NoteDb. This decision for the default may be reevaluated later.
+    readChangeSequence = cfg.getBoolean(NOTE_DB, CHANGES.key(), SEQUENCE, false);
+
     writeAccounts = cfg.getBoolean(NOTE_DB, ACCOUNTS.key(), WRITE, false);
     readAccounts = cfg.getBoolean(NOTE_DB, ACCOUNTS.key(), READ, false);
   }
@@ -101,6 +113,11 @@
   }
 
   @Override
+  public boolean readChangeSequence() {
+    return readChangeSequence;
+  }
+
+  @Override
   public boolean writeAccounts() {
     return writeAccounts;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 13a36d5..08195e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -43,14 +43,20 @@
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.ReceiveCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.concurrent.TimeUnit;
 
 /**
  * View of the draft comments for a single {@link Change} based on the log of
  * its drafts branch.
  */
 public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
+  private static final Logger log =
+      LoggerFactory.getLogger(DraftCommentNotes.class);
+
   public interface Factory {
     DraftCommentNotes create(Change change, Account.Id accountId);
     DraftCommentNotes createWithAutoRebuildingDisabled(
@@ -184,33 +190,44 @@
   }
 
   private LoadHandle rebuildAndOpen(Repository repo) throws IOException {
-    try (Timer1.Context timer =
-        args.metrics.autoRebuildLatency.start(CHANGES)) {
+    Timer1.Context timer = args.metrics.autoRebuildLatency.start(CHANGES);
+    try {
       Change.Id cid = getChangeId();
       ReviewDb db = args.db.get();
       ChangeRebuilder rebuilder = args.rebuilder.get();
-      NoteDbUpdateManager manager = rebuilder.stage(db, cid);
-      if (manager == null) {
-        return super.openHandle(repo); // May be null in tests.
-      }
-      NoteDbUpdateManager.Result r = manager.stageAndApplyDelta(change);
-      try {
-        rebuilder.execute(db, cid, manager);
-        repo.scanForRepoChanges();
-      } catch (OrmException | IOException e) {
-        // See ChangeNotes#rebuildAndOpen.
-        args.metrics.autoRebuildFailureCount.increment(CHANGES);
-        checkNotNull(r.staged());
-        return LoadHandle.create(
-            ChangeNotesCommit.newStagedRevWalk(
-                repo, r.staged().allUsersObjects()),
-            draftsId(r));
+      NoteDbUpdateManager.Result r;
+      try (NoteDbUpdateManager manager = rebuilder.stage(db, cid)) {
+        if (manager == null) {
+          return super.openHandle(repo); // May be null in tests.
+        }
+        r = manager.stageAndApplyDelta(change);
+        try {
+          rebuilder.execute(db, cid, manager);
+          repo.scanForRepoChanges();
+        } catch (OrmException | IOException e) {
+          // See ChangeNotes#rebuildAndOpen.
+          log.debug("Rebuilding change {} via drafts failed: {}",
+              getChangeId(), e.getMessage());
+          args.metrics.autoRebuildFailureCount.increment(CHANGES);
+          checkNotNull(r.staged());
+          return LoadHandle.create(
+              ChangeNotesCommit.newStagedRevWalk(
+                  repo, r.staged().allUsersObjects()),
+              draftsId(r));
+        }
       }
       return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), draftsId(r));
     } catch (NoSuchChangeException e) {
       return super.openHandle(repo);
     } catch (OrmException e) {
       throw new IOException(e);
+    } finally {
+      log.debug("Rebuilt change {} in {} in {} ms via drafts",
+          getChangeId(),
+          change != null
+              ? "project " + change.getProject()
+              : "unknown project",
+          TimeUnit.MILLISECONDS.convert(timer.stop(), TimeUnit.NANOSECONDS));
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
index d960bda..4a7a781 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
@@ -204,6 +204,19 @@
     return id.get().equals(draftIds.get(accountId));
   }
 
+  boolean isUpToDate(RefCache changeRepoRefs, RefCache draftsRepoRefs)
+      throws IOException {
+    if (!isChangeUpToDate(changeRepoRefs)) {
+      return false;
+    }
+    for (Account.Id accountId : draftIds.keySet()) {
+      if (!areDraftsUpToDate(draftsRepoRefs, accountId)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   @VisibleForTesting
   Change.Id getChangeId() {
     return changeId;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index c54c17d..cad531f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -69,7 +69,7 @@
  * To see the state that would be applied prior to executing the full sequence
  * of updates, use {@link #stage()}.
  */
-public class NoteDbUpdateManager {
+public class NoteDbUpdateManager implements AutoCloseable {
   public static String CHANGES_READ_ONLY = "NoteDb changes are read-only";
 
   public interface Factory {
@@ -202,6 +202,23 @@
     toDelete = new HashSet<>();
   }
 
+  @Override
+  public void close() {
+    try {
+      if (allUsersRepo != null) {
+        OpenRepo r = allUsersRepo;
+        allUsersRepo = null;
+        r.close();
+      }
+    } finally {
+      if (changeRepo != null) {
+        OpenRepo r = changeRepo;
+        changeRepo = null;
+        r.close();
+      }
+    }
+  }
+
   public NoteDbUpdateManager setChangeRepo(Repository repo, RevWalk rw,
       @Nullable ObjectInserter ins, ChainedReceiveCommands cmds) {
     checkState(changeRepo == null, "change repo already initialized");
@@ -404,12 +421,7 @@
       execute(changeRepo);
       execute(allUsersRepo);
     } finally {
-      if (allUsersRepo != null) {
-        allUsersRepo.close();
-      }
-      if (changeRepo != null) {
-        changeRepo.close();
-      }
+      close();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
index 4bc1407..56b41d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -62,6 +62,15 @@
    */
   protected abstract boolean writeChanges();
 
+  /**
+   * Read sequential change ID numbers from NoteDb.
+   * <p>
+   * If true, change IDs are read from {@code refs/sequences/changes} in
+   * All-Projects. If false, change IDs are read from ReviewDb's native
+   * sequences.
+   */
+  public abstract boolean readChangeSequence();
+
   public abstract boolean readAccounts();
 
   public abstract boolean writeAccounts();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
index 3d17131..c47fd4f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -23,8 +23,10 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Predicates;
 import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.Runnables;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -46,6 +48,8 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -64,6 +68,10 @@
  * non-monotonic numbers.
  */
 public class RepoSequence {
+  public interface Seed {
+    int get() throws OrmException;
+  }
+
   @VisibleForTesting
   static RetryerBuilder<RefUpdate.Result> retryerBuilder() {
     return RetryerBuilder.<RefUpdate.Result> newBuilder()
@@ -80,7 +88,7 @@
   private final GitRepositoryManager repoManager;
   private final Project.NameKey projectName;
   private final String refName;
-  private final int start;
+  private final Seed seed;
   private final int batchSize;
   private final Runnable afterReadRef;
   private final Retryer<RefUpdate.Result> retryer;
@@ -94,20 +102,30 @@
   @VisibleForTesting
   int acquireCount;
 
-  public RepoSequence(GitRepositoryManager repoManager,
-      Project.NameKey projectName, String name, int start, int batchSize) {
-    this(repoManager, projectName, name, start, batchSize,
-        Runnables.doNothing(), RETRYER);
+  public RepoSequence(
+      GitRepositoryManager repoManager,
+      Project.NameKey projectName,
+      String name,
+      Seed seed,
+      int batchSize) {
+    this(repoManager, projectName, name, seed, batchSize, Runnables.doNothing(),
+        RETRYER);
   }
 
   @VisibleForTesting
-  RepoSequence(GitRepositoryManager repoManager, Project.NameKey projectName,
-      String name, int start, int batchSize, Runnable afterReadRef,
+  RepoSequence(
+      GitRepositoryManager repoManager,
+      Project.NameKey projectName,
+      String name,
+      Seed seed,
+      int batchSize,
+      Runnable afterReadRef,
       Retryer<RefUpdate.Result> retryer) {
     this.repoManager = checkNotNull(repoManager, "repoManager");
     this.projectName = checkNotNull(projectName, "projectName");
     this.refName = RefNames.REFS_SEQUENCES + checkNotNull(name, "name");
-    this.start = start;
+    this.seed = checkNotNull(seed, "seed");
+
     checkArgument(batchSize > 0, "expected batchSize > 0, got: %s", batchSize);
     this.batchSize = batchSize;
     this.afterReadRef = checkNotNull(afterReadRef, "afterReadRef");
@@ -120,7 +138,7 @@
     counterLock.lock();
     try {
       if (counter >= limit) {
-        acquire();
+        acquire(batchSize);
       }
       return counter++;
     } finally {
@@ -128,34 +146,81 @@
     }
   }
 
-  private void acquire() throws OrmException {
+  public ImmutableList<Integer> next(int count) throws OrmException {
+    if (count == 0) {
+      return ImmutableList.of();
+    }
+    checkArgument(count > 0, "count is negative: %s", count);
+    counterLock.lock();
+    try {
+      List<Integer> ids = new ArrayList<>(count);
+      while (counter < limit) {
+        ids.add(counter++);
+        if (ids.size() == count) {
+          return ImmutableList.copyOf(ids);
+        }
+      }
+      acquire(Math.max(count - ids.size(), batchSize));
+      while (ids.size() < count) {
+        ids.add(counter++);
+      }
+      return ImmutableList.copyOf(ids);
+    } finally {
+      counterLock.unlock();
+    }
+  }
+
+  @VisibleForTesting
+  public void set(int val) throws OrmException {
+    // Don't bother spinning. This is only for tests, and a test that calls set
+    // concurrently with other writes is doing it wrong.
+    counterLock.lock();
+    try {
+      try (Repository repo = repoManager.openRepository(projectName);
+          RevWalk rw = new RevWalk(repo)) {
+        checkResult(store(repo, rw, null, val));
+        counter = limit;
+      } catch (IOException e) {
+        throw new OrmException(e);
+      }
+    } finally {
+      counterLock.unlock();
+    }
+  }
+
+  private void acquire(int count) throws OrmException {
     try (Repository repo = repoManager.openRepository(projectName);
         RevWalk rw = new RevWalk(repo)) {
-      TryAcquire attempt = new TryAcquire(repo, rw);
-      RefUpdate.Result result = retryer.call(attempt);
-      if (result != RefUpdate.Result.NEW && result != RefUpdate.Result.FORCED) {
-        throw new OrmException("failed to update " + refName + ": " + result);
-      }
+      TryAcquire attempt = new TryAcquire(repo, rw, count);
+      checkResult(retryer.call(attempt));
       counter = attempt.next;
-      limit = counter + batchSize;
+      limit = counter + count;
       acquireCount++;
     } catch (ExecutionException | RetryException e) {
-      Throwables.propagateIfInstanceOf(e.getCause(), OrmException.class);
+      Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
       throw new OrmException(e);
     } catch (IOException e) {
       throw new OrmException(e);
     }
   }
 
+  private void checkResult(RefUpdate.Result result) throws OrmException {
+    if (result != RefUpdate.Result.NEW && result != RefUpdate.Result.FORCED) {
+      throw new OrmException("failed to update " + refName + ": " + result);
+    }
+  }
+
   private class TryAcquire implements Callable<RefUpdate.Result> {
     private final Repository repo;
     private final RevWalk rw;
+    private final int count;
 
     private int next;
 
-    private TryAcquire(Repository repo, RevWalk rw) {
+    private TryAcquire(Repository repo, RevWalk rw, int count) {
       this.repo = repo;
       this.rw = rw;
+      this.count = count;
     }
 
     @Override
@@ -165,12 +230,12 @@
       ObjectId oldId;
       if (ref == null) {
         oldId = ObjectId.zeroId();
-        next = start;
+        next = seed.get();
       } else {
         oldId = ref.getObjectId();
         next = parse(oldId);
       }
-      return store(oldId, next + batchSize);
+      return store(repo, rw, oldId, next + count);
     }
 
     private int parse(ObjectId id) throws IOException, OrmException {
@@ -189,18 +254,21 @@
       }
       return val;
     }
+  }
 
-    private RefUpdate.Result store(ObjectId oldId, int val) throws IOException {
-      ObjectId newId;
-      try (ObjectInserter ins = repo.newObjectInserter()) {
-        newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
-        ins.flush();
-      }
-      RefUpdate ru = repo.updateRef(refName);
-      ru.setExpectedOldObjectId(oldId);
-      ru.setNewObjectId(newId);
-      ru.setForceUpdate(true); // Required for non-commitish updates.
-      return ru.update(rw);
+  private RefUpdate.Result store(Repository repo, RevWalk rw,
+      @Nullable ObjectId oldId, int val) throws IOException {
+    ObjectId newId;
+    try (ObjectInserter ins = repo.newObjectInserter()) {
+      newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
+      ins.flush();
     }
+    RefUpdate ru = repo.updateRef(refName);
+    if (oldId != null) {
+      ru.setExpectedOldObjectId(oldId);
+    }
+    ru.setNewObjectId(newId);
+    ru.setForceUpdate(true); // Required for non-commitish updates.
+    return ru.update(rw);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java
index 7cbf741..c4af9fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -57,6 +57,10 @@
 public class AutoMerger {
   private static final Logger log = LoggerFactory.getLogger(AutoMerger.class);
 
+  static boolean cacheAutomerge(Config cfg) {
+    return cfg.getBoolean("change", null, "cacheAutomerge", true);
+  }
+
   private final PersonIdent gerritIdent;
   private final boolean save;
 
@@ -64,7 +68,7 @@
   AutoMerger(
       @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent gerritIdent) {
-    save = cfg.getBoolean("change", null, "cacheAutomerge", true);
+    save = cacheAutomerge(cfg);
     this.gerritIdent = gerritIdent;
   }
 
@@ -79,7 +83,11 @@
       throws IOException {
     checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins);
     InMemoryInserter tmpIns = null;
-    if (!save) {
+    if (ins instanceof InMemoryInserter) {
+      // Caller gave us an in-memory inserter, so ensure anything we write from
+      // this method is visible to them.
+      tmpIns = (InMemoryInserter) ins;
+    } else if (!save) {
       // If we don't plan on saving results, use a fully in-memory inserter.
       // Using just a non-flushing wrapper is not sufficient, since in
       // particular DfsInserter might try to write to storage after exceeding an
@@ -105,7 +113,7 @@
     ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(repo, true);
     DirCache dc = DirCache.newInCore();
     m.setDirCache(dc);
-    m.setObjectInserter(save ? new NonFlushingWrapper(ins) : tmpIns);
+    m.setObjectInserter(tmpIns == null ? new NonFlushingWrapper(ins) : tmpIns);
 
     boolean couldMerge;
     try {
@@ -236,6 +244,7 @@
     }
 
     checkArgument(tmpIns == null);
+    checkArgument(!(ins instanceof InMemoryInserter));
     ObjectId commitId = ins.insert(cb);
     ins.flush();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
index 038ad51..cdde12a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
@@ -14,84 +14,25 @@
 
 package com.google.gerrit.server.patch;
 
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 
 import org.eclipse.jgit.lib.ObjectId;
 
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
 import java.io.Serializable;
 
-public class IntraLineDiffKey implements Serializable {
-  static final long serialVersionUID = 4L;
+@AutoValue
+public abstract class IntraLineDiffKey implements Serializable {
+  public static final long serialVersionUID = 5L;
 
-  private transient boolean ignoreWhitespace;
-  private transient ObjectId aId;
-  private transient ObjectId bId;
-
-  public IntraLineDiffKey(ObjectId aId, ObjectId bId,
-      boolean ignoreWhitespace) {
-    this.aId = aId;
-    this.bId = bId;
-    this.ignoreWhitespace = ignoreWhitespace;
+  public static IntraLineDiffKey create(ObjectId aId, ObjectId bId,
+      Whitespace whitespace) {
+    return new AutoValue_IntraLineDiffKey(aId, bId, whitespace);
   }
 
-  public ObjectId getBlobA() {
-    return aId;
-  }
+  public abstract ObjectId getBlobA();
 
-  public ObjectId getBlobB() {
-    return bId;
-  }
+  public abstract ObjectId getBlobB();
 
-  public boolean isIgnoreWhitespace() {
-    return ignoreWhitespace;
-  }
-
-  @Override
-  public int hashCode() {
-    int h = 0;
-
-    h = h * 31 + aId.hashCode();
-    h = h * 31 + bId.hashCode();
-    h = h * 31 + (ignoreWhitespace ? 1 : 0);
-
-    return h;
-  }
-
-  @Override
-  public boolean equals(final Object o) {
-    if (o instanceof IntraLineDiffKey) {
-      final IntraLineDiffKey k = (IntraLineDiffKey) o;
-      return aId.equals(k.aId) //
-          && bId.equals(k.bId) //
-          && ignoreWhitespace == k.ignoreWhitespace;
-    }
-    return false;
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder n = new StringBuilder();
-    n.append("IntraLineDiffKey[");
-    n.append(aId.name());
-    n.append("..");
-    n.append(bId.name());
-    n.append("]");
-    return n.toString();
-  }
-
-  private void writeObject(final ObjectOutputStream out) throws IOException {
-    writeNotNull(out, aId);
-    writeNotNull(out, bId);
-    out.writeBoolean(ignoreWhitespace);
-  }
-
-  private void readObject(final ObjectInputStream in) throws IOException {
-    aId = readNotNull(in);
-    bId = readNotNull(in);
-    ignoreWhitespace = in.readBoolean();
-  }
+  public abstract Whitespace getWhitespace();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index dd15cfc..ae37c01 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -93,7 +93,7 @@
     } catch (ExecutionException e) {
       // If there was an error computing the result, carry it
       // up to the caller so the cache knows this key is invalid.
-      Throwables.propagateIfInstanceOf(e.getCause(), Exception.class);
+      Throwables.throwIfInstanceOf(e.getCause(), Exception.class);
       throw new Exception(e.getMessage(), e.getCause());
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
index 2099376..8a2403f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -28,7 +28,7 @@
   PatchList get(Change change, PatchSet patchSet)
       throws PatchListNotAvailableException;
 
-  ObjectId getOldId(Change change, PatchSet patchSet)
+  ObjectId getOldId(Change change, PatchSet patchSet, Integer parentNum)
       throws PatchListNotAvailableException;
 
   IntraLineDiff getIntraLineDiff(IntraLineDiffKey key,
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 7c8f19f..abafad7 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
@@ -103,6 +103,17 @@
   @Override
   public PatchList get(Change change, PatchSet patchSet)
       throws PatchListNotAvailableException {
+    return get(change, patchSet, null);
+  }
+
+  @Override
+  public ObjectId getOldId(Change change, PatchSet patchSet, Integer parentNum)
+      throws PatchListNotAvailableException {
+    return get(change, patchSet, parentNum).getOldId();
+  }
+
+  private PatchList get(Change change, PatchSet patchSet, Integer parentNum)
+      throws PatchListNotAvailableException {
     Project.NameKey project = change.getProject();
     if (patchSet.getRevision() == null) {
       throw new PatchListNotAvailableException(
@@ -110,13 +121,10 @@
     }
     ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
     Whitespace ws = Whitespace.IGNORE_NONE;
-    return get(new PatchListKey(null, b, ws), project);
-  }
-
-  @Override
-  public ObjectId getOldId(Change change, PatchSet patchSet)
-      throws PatchListNotAvailableException {
-    return get(change, patchSet).getOldId();
+    if (parentNum != null) {
+      return get(PatchListKey.againstParentNum(parentNum, b, ws), project);
+    }
+    return get(PatchListKey.againstDefaultBase(b, ws), project);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index b04558d..961ed5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -32,9 +32,10 @@
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
+import java.util.Objects;
 
 public class PatchListKey implements Serializable {
-  static final long serialVersionUID = 20L;
+  public static final long serialVersionUID = 21L;
 
   public static final BiMap<Whitespace, Character> WHITESPACE_TYPES = ImmutableBiMap.of(
       Whitespace.IGNORE_NONE, 'N',
@@ -46,7 +47,36 @@
     checkState(WHITESPACE_TYPES.size() == Whitespace.values().length);
   }
 
+  public static PatchListKey againstDefaultBase(AnyObjectId newId,
+      Whitespace ws) {
+    return new PatchListKey(null, newId, ws);
+  }
+
+  public static PatchListKey againstParentNum(int parentNum, AnyObjectId newId,
+      Whitespace ws) {
+    return new PatchListKey(parentNum, newId, ws);
+  }
+
+  /**
+   * Old patch-set ID
+   * <p>
+   * When null, it represents the Base of the newId for a non-merge commit.
+   * <p>
+   * When newId is a merge commit, null value of the oldId represents either
+   * the auto-merge commit of the newId or a parent commit of the newId.
+   * These two cases are distinguished by the parentNum.
+   */
   private transient ObjectId oldId;
+
+  /**
+   * 1-based parent number when newId is a merge commit
+   * <p>
+   * For the auto-merge case this field is null.
+   * <p>
+   * Used only when oldId is null and newId is a merge commit
+   */
+  private transient Integer parentNum;
+
   private transient ObjectId newId;
   private transient Whitespace whitespace;
 
@@ -56,12 +86,24 @@
     whitespace = ws;
   }
 
+  private PatchListKey(int parentNum, AnyObjectId b, Whitespace ws) {
+    this.parentNum = Integer.valueOf(parentNum);
+    newId = b.copy();
+    whitespace = ws;
+  }
+
   /** Old side commit, or null to assume ancestor or combined merge. */
   @Nullable
   public ObjectId getOldId() {
     return oldId;
   }
 
+  /** Parent number (old side) of the new side (merge) commit */
+  @Nullable
+  public Integer getParentNum() {
+    return parentNum;
+  }
+
   /** New side commit name. */
   public ObjectId getNewId() {
     return newId;
@@ -73,24 +115,16 @@
 
   @Override
   public int hashCode() {
-    int h = 0;
-
-    if (oldId != null) {
-      h = h * 31 + oldId.hashCode();
-    }
-
-    h = h * 31 + newId.hashCode();
-    h = h * 31 + whitespace.name().hashCode();
-
-    return h;
+    return Objects.hash(oldId, parentNum, newId, whitespace);
   }
 
   @Override
   public boolean equals(final Object o) {
     if (o instanceof PatchListKey) {
-      final PatchListKey k = (PatchListKey) o;
-      return eq(oldId, k.oldId) //
-          && eq(newId, k.newId) //
+      PatchListKey k = (PatchListKey) o;
+      return Objects.equals(oldId, k.oldId)
+          && Objects.equals(parentNum, k.parentNum)
+          && Objects.equals(newId, k.newId)
           && whitespace == k.whitespace;
     }
     return false;
@@ -109,15 +143,9 @@
     return n.toString();
   }
 
-  private static boolean eq(final ObjectId a, final ObjectId b) {
-    if (a == null && b == null) {
-      return true;
-    }
-    return a != null && b != null && AnyObjectId.equals(a, b);
-  }
-
   private void writeObject(final ObjectOutputStream out) throws IOException {
     writeCanBeNull(out, oldId);
+    out.writeInt(parentNum == null ? 0 : parentNum);
     writeNotNull(out, newId);
     Character c = WHITESPACE_TYPES.get(whitespace);
     if (c == null) {
@@ -128,6 +156,8 @@
 
   private void readObject(final ObjectInputStream in) throws IOException {
     oldId = readCanBeNull(in);
+    int n = in.readInt();
+    parentNum = n == 0 ? null : Integer.valueOf(n);
     newId = readNotNull(in);
     char t = in.readChar();
     whitespace = WHITESPACE_TYPES.inverse().get(t);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 90bebfc..1156b91 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -15,6 +15,7 @@
 
 package com.google.gerrit.server.patch;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
@@ -27,6 +28,7 @@
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.InMemoryInserter;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -84,6 +86,7 @@
   private final PatchListKey key;
   private final Project.NameKey project;
   private final long timeoutMillis;
+  private final boolean save;
 
   @AssistedInject
   PatchListLoader(GitRepositoryManager mgr,
@@ -104,13 +107,17 @@
         ConfigUtil.getTimeUnit(cfg, "cache", PatchListCacheImpl.FILE_NAME,
             "timeout", TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS),
             TimeUnit.MILLISECONDS);
+    save = AutoMerger.cacheAutomerge(cfg);
   }
 
   @Override
   public PatchList call() throws IOException,
       PatchListNotAvailableException {
-    try (Repository repo = repoManager.openRepository(project)) {
-      return readPatchList(key, repo);
+    try (Repository repo = repoManager.openRepository(project);
+        ObjectInserter ins = newInserter(repo);
+        ObjectReader reader = ins.newReader();
+        RevWalk rw = new RevWalk(reader)) {
+      return readPatchList(repo, rw, ins);
     }
   }
 
@@ -131,43 +138,48 @@
     }
   }
 
-  private PatchList readPatchList(final PatchListKey key, final Repository repo)
-      throws IOException, PatchListNotAvailableException {
-    final RawTextComparator cmp = comparatorFor(key.getWhitespace());
-    try (ObjectInserter ins = repo.newObjectInserter();
-        ObjectReader reader = ins.newReader();
-        RevWalk rw = new RevWalk(reader);
-        DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
-      final RevCommit b = rw.parseCommit(key.getNewId());
-      final RevObject a = aFor(key, repo, rw, ins, b);
+  private ObjectInserter newInserter(Repository repo) {
+    return save
+        ? repo.newObjectInserter()
+        : new InMemoryInserter(repo);
+  }
+
+  public PatchList readPatchList(Repository repo, RevWalk rw,
+      ObjectInserter ins) throws IOException, PatchListNotAvailableException {
+    ObjectReader reader = rw.getObjectReader();
+    checkArgument(reader.getCreatedFromInserter() == ins);
+    RawTextComparator cmp = comparatorFor(key.getWhitespace());
+    try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
+      RevCommit b = rw.parseCommit(key.getNewId());
+      RevObject a = aFor(key, repo, rw, ins, b);
 
       if (a == null) {
         // TODO(sop) Remove this case.
         // This is a merge commit, compared to its ancestor.
         //
-        final PatchListEntry[] entries = new PatchListEntry[1];
+        PatchListEntry[] entries = new PatchListEntry[1];
         entries[0] = newCommitMessage(cmp, reader, null, b);
         return new PatchList(a, b, true, entries);
       }
 
-      final boolean againstParent =
-          b.getParentCount() > 0 && b.getParent(0) == a;
+      boolean againstParent =
+          b.getParentCount() > 0 && b.getParent(0).equals(a);
 
       RevCommit aCommit = a instanceof RevCommit ? (RevCommit) a : null;
       RevTree aTree = rw.parseTree(a);
       RevTree bTree = b.getTree();
 
-      df.setRepository(repo);
+      df.setReader(reader, repo.getConfig());
       df.setDiffComparator(cmp);
       df.setDetectRenames(true);
       List<DiffEntry> diffEntries = df.scan(aTree, bTree);
 
       Set<String> paths = null;
-      if (key.getOldId() != null) {
-        PatchListKey newKey =
-            new PatchListKey(null, key.getNewId(), key.getWhitespace());
-        PatchListKey oldKey =
-            new PatchListKey(null, key.getOldId(), key.getWhitespace());
+      if (key.getOldId() != null && b.getParentCount() == 1) {
+        PatchListKey newKey = PatchListKey.againstDefaultBase(
+            key.getNewId(), key.getWhitespace());
+        PatchListKey oldKey = PatchListKey.againstDefaultBase(
+            key.getOldId(), key.getWhitespace());
         paths = FluentIterable
             .from(patchListCache.get(newKey, project).getPatches())
             .append(patchListCache.get(oldKey, project).getPatches())
@@ -191,9 +203,9 @@
 
           FileHeader fh = toFileHeader(key, df, e);
           long oldSize =
-              getFileSize(repo, reader, e.getOldMode(), e.getOldPath(), aTree);
+              getFileSize(reader, e.getOldMode(), e.getOldPath(), aTree);
           long newSize =
-              getFileSize(repo, reader, e.getNewMode(), e.getNewPath(), bTree);
+              getFileSize(reader, e.getNewMode(), e.getNewPath(), bTree);
           entries.add(newEntry(aTree, fh, newSize, newSize - oldSize));
         }
       }
@@ -202,14 +214,14 @@
     }
   }
 
-  private static long getFileSize(Repository repo, ObjectReader reader,
+  private static long getFileSize(ObjectReader reader,
       FileMode mode, String path, RevTree t) throws IOException {
     if (!isBlob(mode)) {
       return 0;
     }
     try (TreeWalk tw = TreeWalk.forPath(reader, path, t)) {
       return tw != null
-          ? repo.open(tw.getObjectId(0), OBJ_BLOB).getSize()
+          ? reader.open(tw.getObjectId(0), OBJ_BLOB).getSize()
           : 0;
     }
   }
@@ -248,7 +260,7 @@
     } catch (ExecutionException e) {
       // If there was an error computing the result, carry it
       // up to the caller so the cache knows this key is invalid.
-      Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
+      Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
       throw new IOException(e.getMessage(), e.getCause());
     }
   }
@@ -324,13 +336,18 @@
 
     switch (b.getParentCount()) {
       case 0:
-        return rw.parseAny(emptyTree(repo));
+        return rw.parseAny(emptyTree(ins));
       case 1: {
         RevCommit r = b.getParent(0);
         rw.parseBody(r);
         return r;
       }
       case 2:
+        if (key.getParentNum() != null) {
+          RevCommit r = b.getParent(key.getParentNum() - 1);
+          rw.parseBody(r);
+          return r;
+        }
         return autoMerger.merge(repo, rw, ins, b, mergeStrategy);
       default:
         // TODO(sop) handle an octopus merge.
@@ -338,11 +355,9 @@
     }
   }
 
-  private static ObjectId emptyTree(final Repository repo) throws IOException {
-    try (ObjectInserter oi = repo.newObjectInserter()) {
-      ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
-      oi.flush();
-      return id;
-    }
+  private static ObjectId emptyTree(ObjectInserter ins) throws IOException {
+    ObjectId id = ins.insert(Constants.OBJ_TREE, new byte[] {});
+    ins.flush();
+    return id;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 51c70f5..e09d26f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -143,9 +143,9 @@
     } else if (diffPrefs.intralineDifference) {
       IntraLineDiff d =
           patchListCache.getIntraLineDiff(
-              new IntraLineDiffKey(
+              IntraLineDiffKey.create(
                 a.id, b.id,
-                diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE),
+                diffPrefs.ignoreWhitespace),
               IntraLineDiffArgs.create(
                 a.src, b.src, edits, projectKey, bId, b.path));
       if (d != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index b7ca69d4..a7d2523 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.patch;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.util.GitUtil.getParent;
 
 import com.google.common.base.Optional;
 import com.google.gerrit.common.Nullable;
@@ -44,9 +45,9 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -70,6 +71,13 @@
         @Assisted("patchSetA") PatchSet.Id patchSetA,
         @Assisted("patchSetB") PatchSet.Id patchSetB,
         DiffPreferencesInfo diffPrefs);
+
+    PatchScriptFactory create(
+        ChangeControl control,
+        String fileName,
+        int parentNum,
+        PatchSet.Id patchSetB,
+        DiffPreferencesInfo diffPrefs);
   }
 
   private static final Logger log =
@@ -86,6 +94,7 @@
   private final String fileName;
   @Nullable
   private final PatchSet.Id psa;
+  private final int parentNum;
   private final PatchSet.Id psb;
   private final DiffPreferencesInfo diffPrefs;
   private final ChangeEditUtil editReader;
@@ -103,7 +112,7 @@
   private List<Patch> history;
   private CommentDetail comments;
 
-  @Inject
+  @AssistedInject
   PatchScriptFactory(GitRepositoryManager grm,
       PatchSetUtil psUtil,
       Provider<PatchScriptBuilder> builderFactory,
@@ -129,14 +138,45 @@
 
     this.fileName = fileName;
     this.psa = patchSetA;
+    this.parentNum = -1;
     this.psb = patchSetB;
     this.diffPrefs = diffPrefs;
 
     changeId = patchSetB.getParentKey();
-    checkArgument(
-        patchSetA == null || patchSetA.getParentKey().equals(changeId),
-        "cannot compare PatchSets from different changes: %s and %s",
-        patchSetA, patchSetB);
+  }
+
+  @AssistedInject
+  PatchScriptFactory(GitRepositoryManager grm,
+      PatchSetUtil psUtil,
+      Provider<PatchScriptBuilder> builderFactory,
+      PatchListCache patchListCache,
+      ReviewDb db,
+      AccountInfoCacheFactory.Factory aicFactory,
+      PatchLineCommentsUtil plcUtil,
+      ChangeEditUtil editReader,
+      @Assisted ChangeControl control,
+      @Assisted String fileName,
+      @Assisted int parentNum,
+      @Assisted PatchSet.Id patchSetB,
+      @Assisted DiffPreferencesInfo diffPrefs) {
+    this.repoManager = grm;
+    this.psUtil = psUtil;
+    this.builderFactory = builderFactory;
+    this.patchListCache = patchListCache;
+    this.db = db;
+    this.control = control;
+    this.aicFactory = aicFactory;
+    this.plcUtil = plcUtil;
+    this.editReader = editReader;
+
+    this.fileName = fileName;
+    this.psa = null;
+    this.parentNum = parentNum;
+    this.psb = patchSetB;
+    this.diffPrefs = diffPrefs;
+
+    changeId = patchSetB.getParentKey();
+    checkArgument(parentNum >= 0, "parentNum must be >= 0");
   }
 
   public void setLoadHistory(boolean load) {
@@ -151,7 +191,9 @@
   public PatchScript call() throws OrmException, NoSuchChangeException,
       LargeObjectException, AuthException,
       InvalidChangeOperationException, IOException {
-    validatePatchSetId(psa);
+    if (parentNum < 0) {
+      validatePatchSetId(psa);
+    }
     validatePatchSetId(psb);
 
     change = control.getChange();
@@ -163,15 +205,19 @@
         ? new PatchSet(psb)
         : psUtil.get(db, control.getNotes(), psb);
 
-    aId = psEntityA != null ? toObjectId(psEntityA) : null;
-    bId = toObjectId(psEntityB);
-
     if ((psEntityA != null && !control.isPatchVisible(psEntityA, db)) ||
         (psEntityB != null && !control.isPatchVisible(psEntityB, db))) {
       throw new NoSuchChangeException(changeId);
     }
 
     try (Repository git = repoManager.openRepository(project)) {
+      bId = toObjectId(psEntityB);
+      if (parentNum < 0) {
+        aId = psEntityA != null ? toObjectId(psEntityA) : null;
+      } else {
+        aId = getParent(git, bId, parentNum);
+      }
+
       try {
         final PatchList list = listFor(keyFor(diffPrefs.ignoreWhitespace));
         final PatchScriptBuilder b = newBuilder(list, git);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
index bc35d27..8e85fd0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
@@ -16,8 +16,12 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.config.AdministrateServerGroups;
+import com.google.gerrit.server.config.AdministrateServerGroupsProvider;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitReceivePackGroupsProvider;
 import com.google.gerrit.server.config.GitUploadPackGroups;
@@ -29,13 +33,20 @@
 public class AccessControlModule extends FactoryModule {
   @Override
   protected void configure() {
-    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {}) //
-        .annotatedWith(GitUploadPackGroups.class) //
-        .toProvider(GitUploadPackGroupsProvider.class).in(SINGLETON);
+    bind(new TypeLiteral<ImmutableSet<GroupReference>>() {})
+      .annotatedWith(AdministrateServerGroups.class)
+      .toProvider(AdministrateServerGroupsProvider.class)
+      .in(SINGLETON);
 
-    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {}) //
-        .annotatedWith(GitReceivePackGroups.class) //
-        .toProvider(GitReceivePackGroupsProvider.class).in(SINGLETON);
+    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
+        .annotatedWith(GitUploadPackGroups.class)
+        .toProvider(GitUploadPackGroupsProvider.class)
+        .in(SINGLETON);
+
+    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
+        .annotatedWith(GitReceivePackGroups.class)
+        .toProvider(GitReceivePackGroupsProvider.class)
+        .in(SINGLETON);
 
     bind(ChangeControl.Factory.class);
     factory(ProjectControl.AssistedFactory.class);
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 2f1208b..9086b6a 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
@@ -314,11 +314,16 @@
   }
 
   /** Can this user add a patch set to this change? */
-  public boolean canAddPatchSet(ReviewDb db)
-      throws OrmException {
-    return getRefControl().canUpload()
-        && !isPatchSetLocked(db)
-        && isPatchVisible(patchSetUtil.current(db, notes), db);
+  public boolean canAddPatchSet(ReviewDb db) throws OrmException {
+    if (!getRefControl().canUpload()
+        || isPatchSetLocked(db)
+        || !isPatchVisible(patchSetUtil.current(db, notes), db)) {
+      return false;
+    }
+    if (isOwner()) {
+      return true;
+    }
+    return getRefControl().canAddPatchSet();
   }
 
   /** Is the current patch set locked against state changes? */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java
new file mode 100644
index 0000000..37d5295
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java
@@ -0,0 +1,133 @@
+// Copyright (C) 2016 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.client.SubmitType;
+import com.google.gerrit.extensions.common.MergeableInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.InMemoryInserter;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.Merger;
+import org.eclipse.jgit.merge.ResolveMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+
+/**
+ * Check the mergeability at current branch for a git object references expression.
+ */
+public class CheckMergeability implements RestReadView<BranchResource> {
+
+  private String source;
+  private String strategy;
+  private SubmitType submitType;
+  private final Provider<ReviewDb> db;
+
+  @Option(name = "--source", metaVar = "COMMIT",
+      usage = "the source reference to merge, which could be any git object "
+          + "references expression, refer to "
+          + "org.eclipse.jgit.lib.Repository#resolve(String)",
+      required = true)
+  public void setSource(String source) {
+    this.source = source;
+  }
+
+  @Option(name = "--strategy", metaVar = "STRATEGY",
+      usage = "name of the merge strategy, refer to "
+          + "org.eclipse.jgit.merge.MergeStrategy")
+  public void setStrategy(String strategy) {
+    this.strategy = strategy;
+  }
+
+  private final GitRepositoryManager gitManager;
+
+  @Inject
+  CheckMergeability(GitRepositoryManager gitManager,
+      @GerritServerConfig Config cfg,
+      Provider<ReviewDb> db) {
+    this.gitManager = gitManager;
+    this.strategy = MergeUtil.getMergeStrategy(cfg).getName();
+    this.submitType = cfg.getEnum("project", null, "submitType",
+        SubmitType.MERGE_IF_NECESSARY);
+    this.db = db;
+  }
+
+  @Override
+  public MergeableInfo apply(BranchResource resource)
+      throws IOException, BadRequestException, ResourceNotFoundException {
+    if (!(submitType.equals(SubmitType.MERGE_ALWAYS) ||
+          submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
+      throw new BadRequestException(
+          "Submit type: " + submitType + " is not supported");
+    }
+
+    MergeableInfo result = new MergeableInfo();
+    result.submitType = submitType;
+    result.strategy = strategy;
+    try (Repository git = gitManager.openRepository(resource.getNameKey());
+         RevWalk rw = new RevWalk(git);
+         ObjectInserter inserter = new InMemoryInserter(git)) {
+      Merger m = MergeUtil.newMerger(git, inserter, strategy);
+
+      Ref destRef = git.getRefDatabase().exactRef(resource.getRef());
+      if (destRef == null) {
+        throw new ResourceNotFoundException(resource.getRef());
+      }
+
+      RevCommit targetCommit = rw.parseCommit(destRef.getObjectId());
+      RevCommit sourceCommit = MergeUtil.resolveCommit(git, rw, source);
+
+      if (!resource.getControl().canReadCommit(db.get(), git, sourceCommit)) {
+        throw new BadRequestException(
+            "do not have read permission for: " + source);
+      }
+
+      if (rw.isMergedInto(sourceCommit, targetCommit)) {
+        result.mergeable = true;
+        result.commitMerged = true;
+        result.contentMerged = true;
+        return result;
+      }
+
+      if (m.merge(false, targetCommit, sourceCommit)) {
+        result.mergeable = true;
+        result.commitMerged = false;
+        result.contentMerged = m.getResultTreeId().equals(targetCommit.getTree());
+      } else {
+        result.mergeable = false;
+        if (m instanceof ResolveMerger) {
+          result.conflicts = ((ResolveMerger) m).getUnmergedPaths();
+        }
+      }
+    } catch (IllegalArgumentException e) {
+      throw new BadRequestException(e.getMessage());
+    }
+    return result;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
index 2a29995..f151b59 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
@@ -21,14 +21,18 @@
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
 
 import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.List;
 import java.util.Set;
 
 public class CommentLinkProvider implements Provider<List<CommentLinkInfo>> {
+  private static final Logger log =
+      LoggerFactory.getLogger(CommentLinkProvider.class);
+
   private final Config cfg;
 
   @Inject
@@ -42,12 +46,16 @@
     List<CommentLinkInfo> cls =
         Lists.newArrayListWithCapacity(subsections.size());
     for (String name : subsections) {
-      CommentLinkInfoImpl cl = ProjectConfig.buildCommentLink(cfg, name, true);
-      if (cl.isOverrideOnly()) {
-        throw new ProvisionException(
-            "commentlink " + name + " empty except for \"enabled\"");
+      try {
+        CommentLinkInfoImpl cl = ProjectConfig.buildCommentLink(cfg, name, true);
+        if (cl.isOverrideOnly()) {
+          log.warn("commentlink " + name + " empty except for \"enabled\"");
+          continue;
+        }
+        cls.add(cl);
+      } catch (IllegalArgumentException e) {
+        log.warn("invalid commentlink: " + e.getMessage());
       }
-      cls.add(cl);
     }
     return ImmutableList.copyOf(cls);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
index 4879bb7..3deb7d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
@@ -69,7 +69,7 @@
         RevWalk rw = new RevWalk(repo)) {
       RevCommit commit = rw.parseCommit(objectId);
       rw.parseBody(commit);
-      if (!parent.getControl().canReadCommit(db.get(), rw, commit)) {
+      if (!parent.getControl().canReadCommit(db.get(), repo, commit)) {
         throw new ResourceNotFoundException(id);
       }
       for (int i = 0; i < commit.getParentCount(); i++) {
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 e49ba7d..c7b2922 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
@@ -55,6 +55,7 @@
   private final GitRepositoryManager repoManager;
   private final Provider<ReviewDb> db;
   private final GitReferenceUpdated referenceUpdated;
+  private final RefValidationHelper refCreationValidator;
   private String ref;
 
   @Inject
@@ -62,11 +63,14 @@
       GitRepositoryManager repoManager,
       Provider<ReviewDb> db,
       GitReferenceUpdated referenceUpdated,
+      RefValidationHelper.Factory refHelperFactory,
       @Assisted String ref) {
     this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
     this.db = db;
     this.referenceUpdated = referenceUpdated;
+    this.refCreationValidator =
+        refHelperFactory.create(ReceiveCommand.Type.CREATE);
     this.ref = ref;
   }
 
@@ -113,8 +117,7 @@
         }
       }
 
-      rw.reset();
-      if (!refControl.canCreate(db.get(), rw, object)) {
+      if (!refControl.canCreate(db.get(), repo, object)) {
         throw new AuthException("Cannot create \"" + ref + "\"");
       }
 
@@ -124,6 +127,8 @@
         u.setNewObjectId(object.copy());
         u.setRefLogIdent(identifiedUser.get().newRefLogIdent());
         u.setRefLogMessage("created via REST from " + input.revision, false);
+        refCreationValidator.validateRefOperation(
+            rsrc.getName(), identifiedUser.get(), u);
         final RefUpdate.Result result = u.update(rw);
         switch (result) {
           case FAST_FORWARD:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 2a447e4..fa385f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
 import com.google.gerrit.server.config.RepositoryConfig;
+import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
@@ -94,7 +95,7 @@
   private final ProjectJson json;
   private final ProjectControl.GenericFactory projectControlFactory;
   private final GitRepositoryManager repoManager;
-  private final DynamicSet<NewProjectCreatedListener> createdListener;
+  private final DynamicSet<NewProjectCreatedListener> createdListeners;
   private final ProjectCache projectCache;
   private final GroupBackend groupBackend;
   private final ProjectOwnerGroupsProvider.Factory projectOwnerGroups;
@@ -113,7 +114,7 @@
       DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners,
       ProjectControl.GenericFactory projectControlFactory,
       GitRepositoryManager repoManager,
-      DynamicSet<NewProjectCreatedListener> createdListener,
+      DynamicSet<NewProjectCreatedListener> createdListeners,
       ProjectCache projectCache,
       GroupBackend groupBackend,
       ProjectOwnerGroupsProvider.Factory projectOwnerGroups,
@@ -131,7 +132,7 @@
     this.json = json;
     this.projectControlFactory = projectControlFactory;
     this.repoManager = repoManager;
-    this.createdListener = createdListener;
+    this.createdListeners = createdListeners;
     this.projectCache = projectCache;
     this.groupBackend = groupBackend;
     this.projectOwnerGroups = projectOwnerGroups;
@@ -226,7 +227,7 @@
     return Response.created(json.format(p));
   }
 
-  public Project createProject(CreateProjectArgs args)
+  private Project createProject(CreateProjectArgs args)
       throws BadRequestException, ResourceConflictException, IOException,
       ConfigInvalidException {
     final Project.NameKey nameKey = args.getProject();
@@ -253,24 +254,7 @@
           createEmptyCommits(repo, nameKey, args.branch);
         }
 
-        NewProjectCreatedListener.Event event = new NewProjectCreatedListener.Event() {
-          @Override
-          public String getProjectName() {
-            return nameKey.get();
-          }
-
-          @Override
-          public String getHeadName() {
-            return head;
-          }
-        };
-        for (NewProjectCreatedListener l : createdListener) {
-          try {
-            l.onNewProjectCreated(event);
-          } catch (RuntimeException e) {
-            log.warn("Failure in NewProjectCreatedListener", e);
-          }
-        }
+        fire(nameKey, head);
 
         return projectCache.get(nameKey).getProject();
       }
@@ -395,4 +379,40 @@
       throw e;
     }
   }
+
+  private void fire(Project.NameKey name, String head) {
+    if (!createdListeners.iterator().hasNext()) {
+      return;
+    }
+
+    Event event = new Event(name, head);
+    for (NewProjectCreatedListener l : createdListeners) {
+      try {
+        l.onNewProjectCreated(event);
+      } catch (RuntimeException e) {
+        log.warn("Failure in NewProjectCreatedListener", e);
+      }
+    }
+  }
+
+  static class Event extends AbstractNoNotifyEvent
+      implements NewProjectCreatedListener.Event {
+    private final Project.NameKey name;
+    private final String head;
+
+    Event(Project.NameKey name, String head) {
+      this.name = name;
+      this.head = head;
+    }
+
+    @Override
+    public String getProjectName() {
+      return name.get();
+    }
+
+    @Override
+    public String getHeadName() {
+      return head;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
index 9db12b8..091cba3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -50,16 +50,20 @@
   private final GitRepositoryManager repoManager;
   private final Provider<InternalChangeQuery> queryProvider;
   private final GitReferenceUpdated referenceUpdated;
+  private final RefValidationHelper refDeletionValidator;
 
   @Inject
   DeleteBranch(Provider<IdentifiedUser> identifiedUser,
       GitRepositoryManager repoManager,
       Provider<InternalChangeQuery> queryProvider,
-      GitReferenceUpdated referenceUpdated) {
+      GitReferenceUpdated referenceUpdated,
+      RefValidationHelper.Factory refHelperFactory) {
     this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
     this.queryProvider = queryProvider;
     this.referenceUpdated = referenceUpdated;
+    this.refDeletionValidator =
+        refHelperFactory.create(ReceiveCommand.Type.DELETE);
   }
 
   @Override
@@ -78,6 +82,8 @@
       RefUpdate.Result result;
       RefUpdate u = r.updateRef(rsrc.getRef());
       u.setForceUpdate(true);
+      refDeletionValidator.validateRefOperation(
+          rsrc.getName(), identifiedUser.get(), u);
       int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
       for (;;) {
         try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
index e0a84eb..f4fa446 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
@@ -35,6 +35,7 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
@@ -53,16 +54,20 @@
   private final GitRepositoryManager repoManager;
   private final Provider<InternalChangeQuery> queryProvider;
   private final GitReferenceUpdated referenceUpdated;
+  private final RefValidationHelper refDeletionValidator;
 
   @Inject
   DeleteBranches(Provider<IdentifiedUser> identifiedUser,
       GitRepositoryManager repoManager,
       Provider<InternalChangeQuery> queryProvider,
-      GitReferenceUpdated referenceUpdated) {
+      GitReferenceUpdated referenceUpdated,
+      RefValidationHelper.Factory refHelperFactory) {
     this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
     this.queryProvider = queryProvider;
     this.referenceUpdated = referenceUpdated;
+    this.refDeletionValidator =
+        refHelperFactory.create(ReceiveCommand.Type.DELETE);
   }
 
   @Override
@@ -100,7 +105,8 @@
   }
 
   private ReceiveCommand createDeleteCommand(ProjectResource project,
-      Repository r, String branch) throws OrmException, IOException {
+      Repository r, String branch)
+          throws OrmException, IOException, ResourceConflictException {
     Ref ref = r.getRefDatabase().getRef(branch);
     ReceiveCommand command;
     if (ref == null) {
@@ -120,6 +126,10 @@
     if (!queryProvider.get().setLimit(1).byBranchOpen(branchKey).isEmpty()) {
       command.setResult(Result.REJECTED_OTHER_REASON, "it has open changes");
     }
+    RefUpdate u = r.updateRef(branch);
+    u.setForceUpdate(true);
+    refDeletionValidator.validateRefOperation(
+        project.getName(), identifiedUser.get(), u);
     return command;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
index 47942be..82ea155 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
@@ -14,16 +14,39 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.TypeLiteral;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+import java.io.IOException;
 
 public class FileResource implements RestResource {
   public static final TypeLiteral<RestView<FileResource>> FILE_KIND =
       new TypeLiteral<RestView<FileResource>>() {};
 
+  public static FileResource create(GitRepositoryManager repoManager,
+      ProjectControl project, ObjectId rev, String path)
+          throws ResourceNotFoundException, IOException {
+    try (Repository repo =
+            repoManager.openRepository(project.getProject().getNameKey());
+        RevWalk rw = new RevWalk(repo)) {
+      RevTree tree = rw.parseTree(rev);
+      if (TreeWalk.forPath(repo, path, tree) != null) {
+        return new FileResource(project, rev, path);
+      }
+    }
+    throw new ResourceNotFoundException(IdString.fromDecoded(path));
+  }
+
   private final ProjectControl project;
   private final ObjectId rev;
   private final String path;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
index d0460d5..dcb8747 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
@@ -19,19 +19,25 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.ObjectId;
 
+import java.io.IOException;
+
 @Singleton
 public class FilesCollection implements
     ChildCollection<BranchResource, FileResource> {
   private final DynamicMap<RestView<FileResource>> views;
+  private final GitRepositoryManager repoManager;
 
   @Inject
-  FilesCollection(DynamicMap<RestView<FileResource>> views) {
+  FilesCollection(DynamicMap<RestView<FileResource>> views,
+      GitRepositoryManager repoManager) {
     this.views = views;
+    this.repoManager = repoManager;
   }
 
   @Override
@@ -40,11 +46,10 @@
   }
 
   @Override
-  public FileResource parse(BranchResource parent, IdString id) {
-    return new FileResource(
-        parent.getControl(),
-        ObjectId.fromString(parent.getRevision()),
-        id.get());
+  public FileResource parse(BranchResource parent, IdString id)
+      throws ResourceNotFoundException, IOException {
+    return FileResource.create(repoManager, parent.getControl(),
+        ObjectId.fromString(parent.getRevision()), id.get());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
index 8e0aab8..64a5fb2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
@@ -19,17 +19,24 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import java.io.IOException;
+
 @Singleton
 public class FilesInCommitCollection implements
     ChildCollection<CommitResource, FileResource> {
   private final DynamicMap<RestView<FileResource>> views;
+  private final GitRepositoryManager repoManager;
 
   @Inject
-  FilesInCommitCollection(DynamicMap<RestView<FileResource>> views) {
+  FilesInCommitCollection(DynamicMap<RestView<FileResource>> views,
+      GitRepositoryManager repoManager) {
     this.views = views;
+    this.repoManager = repoManager;
   }
 
   @Override
@@ -39,8 +46,13 @@
 
   @Override
   public FileResource parse(CommitResource parent, IdString id)
-      throws ResourceNotFoundException {
-    return new FileResource(parent.getProject(), parent.getCommit(), id.get());
+      throws ResourceNotFoundException, IOException {
+    if (Patch.COMMIT_MSG.equals(id.get())) {
+      return new FileResource(parent.getProject(), parent.getCommit(),
+          id.get());
+    }
+    return FileResource.create(repoManager, parent.getProject(),
+        parent.getCommit(), id.get());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
index f2b5fb8..12ca2eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
@@ -62,7 +62,7 @@
       } else if (head.getObjectId() != null) {
         try (RevWalk rw = new RevWalk(repo)) {
           RevCommit commit = rw.parseCommit(head.getObjectId());
-          if (rsrc.getControl().canReadCommit(db.get(), rw, commit)) {
+          if (rsrc.getControl().canReadCommit(db.get(), repo, commit)) {
             return head.getObjectId().name();
           }
           throw new AuthException("not allowed to see HEAD");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index 341d741..8a6145a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -68,6 +68,8 @@
     delete(BRANCH_KIND).to(DeleteBranch.class);
     post(PROJECT_KIND, "branches:delete").to(DeleteBranches.class);
     factory(CreateBranch.Factory.class);
+    get(BRANCH_KIND, "mergeable").to(CheckMergeability.class);
+    factory(RefValidationHelper.Factory.class);
     get(BRANCH_KIND, "reflog").to(GetReflog.class);
     child(BRANCH_KIND, "files").to(FilesCollection.class);
     get(FILE_KIND, "content").to(GetContent.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index d27d4f9..2097ebd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -148,7 +148,7 @@
     } catch (ExecutionException e) {
       if (!(e.getCause() instanceof RepositoryNotFoundException)) {
         log.warn(String.format("Cannot read project %s", projectName.get()), e);
-        Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
+        Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
         throw new IOException(e);
       }
       return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 4099e15..25f80ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -37,7 +37,6 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroups;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.VisibleRefFilter;
@@ -148,7 +147,6 @@
   private final String canonicalWebUrl;
   private final CurrentUser user;
   private final ProjectState state;
-  private final GitRepositoryManager repoManager;
   private final ChangeNotes.Factory changeNotesFactory;
   private final ChangeControl.Factory changeControlFactory;
   private final PermissionCollection.Factory permissionFilter;
@@ -167,7 +165,6 @@
       @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
       ProjectCache pc,
       PermissionCollection.Factory permissionFilter,
-      GitRepositoryManager repoManager,
       ChangeNotes.Factory changeNotesFactory,
       ChangeControl.Factory changeControlFactory,
       TagCache tagCache,
@@ -175,7 +172,6 @@
       @CanonicalWebUrl @Nullable String canonicalWebUrl,
       @Assisted CurrentUser who,
       @Assisted ProjectState ps) {
-    this.repoManager = repoManager;
     this.changeNotesFactory = changeNotesFactory;
     this.changeControlFactory = changeControlFactory;
     this.tagCache = tagCache;
@@ -516,8 +512,8 @@
     return false;
   }
 
-  public boolean canReadCommit(ReviewDb db, RevWalk rw, RevCommit commit) {
-    try (Repository repo = openRepository()) {
+  public boolean canReadCommit(ReviewDb db, Repository repo, RevCommit commit) {
+    try (RevWalk rw = new RevWalk(repo)) {
       return isMergedIntoVisibleRef(repo, db, rw, commit,
           repo.getAllRefs().values());
     } catch (IOException e) {
@@ -541,8 +537,4 @@
     return !refs.isEmpty()
         && IncludedInResolver.includedInOne(repo, rw, commit, refs.values());
   }
-
-  Repository openRepository() throws IOException {
-    return repoManager.openRepository(getProject().getNameKey());
-  }
 }
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 b391da7..9e5c35f 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
@@ -123,6 +123,7 @@
       final GitRepositoryManager gitMgr,
       final RulesCache rulesCache,
       final List<CommentLinkInfo> commentLinks,
+      final CapabilityCollection.Factory capabilityFactory,
       @Assisted final ProjectConfig config) {
     this.sitePaths = sitePaths;
     this.projectCache = projectCache;
@@ -137,7 +138,7 @@
     this.config = config;
     this.configs = new HashMap<>();
     this.capabilities = isAllProjects
-      ? new CapabilityCollection(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
+      ? capabilityFactory.create(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
       : null;
 
     if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) {
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 0559731..ad41522 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
@@ -150,6 +150,13 @@
         && canWrite();
   }
 
+  /** @return true if this user can add a new patch set to this ref */
+  public boolean canAddPatchSet() {
+    return projectControl.controlForRef("refs/for/" + getRefName())
+        .canPerform(Permission.ADD_PATCH_SET)
+        && canWrite();
+  }
+
   /** @return true if this user can submit merge patch sets to this ref */
   public boolean canUploadMerges() {
     return projectControl.controlForRef("refs/for/" + getRefName())
@@ -236,12 +243,11 @@
    * Determines whether the user can create a new Git ref.
    *
    * @param db db for checking change visibility.
-   * @param rw revision pool {@code object} was parsed in; must be reset before
-   *     calling this method.
+   * @param repo repository on which user want to create
    * @param object the object the user will start the reference with.
    * @return {@code true} if the user specified can create a new Git ref
    */
-  public boolean canCreate(ReviewDb db, RevWalk rw, RevObject object) {
+  public boolean canCreate(ReviewDb db, Repository repo, RevObject object) {
     if (!canWrite()) {
       return false;
     }
@@ -274,7 +280,7 @@
         // 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 true;
-      } else if (isMergedIntoBranchOrTag(db, rw, (RevCommit) object)) {
+      } else if (isMergedIntoBranchOrTag(db, repo, (RevCommit) object)) {
         // 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
@@ -284,7 +290,7 @@
       return false;
     } else if (object instanceof RevTag) {
       final RevTag tag = (RevTag) object;
-      try {
+      try (RevWalk rw = new RevWalk(repo)) {
         rw.parseBody(tag);
       } catch (IOException e) {
         return false;
@@ -318,9 +324,9 @@
     }
   }
 
-  private boolean isMergedIntoBranchOrTag(ReviewDb db, RevWalk rw,
+  private boolean isMergedIntoBranchOrTag(ReviewDb db, Repository repo,
       RevCommit commit) {
-    try (Repository repo = projectControl.openRepository()) {
+    try (RevWalk rw = new RevWalk(repo)) {
       List<Ref> refs = new ArrayList<>(
           repo.getRefDatabase().getRefs(Constants.R_HEADS).values());
       refs.addAll(repo.getRefDatabase().getRefs(Constants.R_TAGS).values());
@@ -611,19 +617,6 @@
 
     rules = relevant.getPermission(permissionName);
 
-    if (rules.isEmpty()) {
-      effective.put(permissionName, rules);
-      return rules;
-    }
-
-    if (rules.size() == 1) {
-      if (!projectControl.match(rules.get(0), isChangeOwner)) {
-        rules = Collections.emptyList();
-      }
-      effective.put(permissionName, rules);
-      return rules;
-    }
-
     List<PermissionRule> mine = new ArrayList<>(rules.size());
     for (PermissionRule rule : rules) {
       if (projectControl.match(rule, isChangeOwner)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPattern.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPattern.java
index ed50a54..8c850fb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPattern.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPattern.java
@@ -49,7 +49,7 @@
       try {
         return exampleCache.get(refPattern);
       } catch (ExecutionException e) {
-        Throwables.propagateIfPossible(e.getCause());
+        Throwables.throwIfUnchecked(e.getCause());
         throw new RuntimeException(e);
       }
     } else if (refPattern.endsWith("/*")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefValidationHelper.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefValidationHelper.java
new file mode 100644
index 0000000..6e2fd5d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefValidationHelper.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.validators.RefOperationValidators;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.transport.ReceiveCommand.Type;
+
+public class RefValidationHelper {
+  public interface Factory {
+    RefValidationHelper create(Type operationType);
+  }
+
+  private final RefOperationValidators.Factory refValidatorsFactory;
+  private final Type operationType;
+
+  @Inject
+  RefValidationHelper(RefOperationValidators.Factory refValidatorsFactory,
+      @Assisted Type operationType) {
+    this.refValidatorsFactory = refValidatorsFactory;
+    this.operationType = operationType;
+  }
+
+  public void validateRefOperation(String projectName, IdentifiedUser user,
+      RefUpdate update) throws ResourceConflictException {
+    RefOperationValidators refValidators =
+        refValidatorsFactory.create(
+            new Project(new Project.NameKey(projectName)),
+            user,
+            RefOperationValidators.getCommand(update, operationType));
+    try {
+      refValidators.validateForRefOperation();
+    } catch (ValidationException e) {
+      throw new ResourceConflictException(e.getMessage());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
index aa06024..442447f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
@@ -23,8 +23,10 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.SetHead.Input;
 import com.google.inject.Inject;
@@ -53,15 +55,15 @@
 
   private final GitRepositoryManager repoManager;
   private final Provider<IdentifiedUser> identifiedUser;
-  private final DynamicSet<HeadUpdatedListener> headUpdatedListener;
+  private final DynamicSet<HeadUpdatedListener> headUpdatedListeners;
 
   @Inject
   SetHead(GitRepositoryManager repoManager,
       Provider<IdentifiedUser> identifiedUser,
-      DynamicSet<HeadUpdatedListener> headUpdatedListener) {
+      DynamicSet<HeadUpdatedListener> headUpdatedListeners) {
     this.repoManager = repoManager;
     this.identifiedUser = identifiedUser;
-    this.headUpdatedListener = headUpdatedListener;
+    this.headUpdatedListeners = headUpdatedListeners;
   }
 
   @Override
@@ -106,33 +108,53 @@
             throw new IOException("Setting HEAD failed with " + res);
         }
 
-        HeadUpdatedListener.Event event = new HeadUpdatedListener.Event() {
-          @Override
-          public String getProjectName() {
-            return rsrc.getNameKey().get();
-          }
-
-          @Override
-          public String getOldHeadName() {
-            return oldHead;
-          }
-
-          @Override
-          public String getNewHeadName() {
-            return newHead;
-          }
-        };
-        for (HeadUpdatedListener l : headUpdatedListener) {
-          try {
-            l.onHeadUpdated(event);
-          } catch (RuntimeException e) {
-            log.warn("Failure in HeadUpdatedListener", e);
-          }
-        }
+        fire(rsrc.getNameKey(), oldHead, newHead);
       }
       return ref;
     } catch (RepositoryNotFoundException e) {
       throw new ResourceNotFoundException(rsrc.getName());
     }
   }
+
+  private void fire(Project.NameKey nameKey, String oldHead, String newHead) {
+    if (!headUpdatedListeners.iterator().hasNext()) {
+      return;
+    }
+    Event event = new Event(nameKey, oldHead, newHead);
+    for (HeadUpdatedListener l : headUpdatedListeners) {
+      try {
+        l.onHeadUpdated(event);
+      } catch (RuntimeException e) {
+        log.warn("Failure in HeadUpdatedListener", e);
+      }
+    }
+  }
+
+  static class Event extends AbstractNoNotifyEvent
+      implements HeadUpdatedListener.Event {
+    private final Project.NameKey nameKey;
+    private final String oldHead;
+    private final String newHead;
+
+    Event(Project.NameKey nameKey, String oldHead, String newHead) {
+      this.nameKey = nameKey;
+      this.oldHead = oldHead;
+      this.newHead = newHead;
+    }
+
+    @Override
+    public String getProjectName() {
+      return nameKey.get();
+    }
+
+    @Override
+    public String getOldHeadName() {
+      return oldHead;
+    }
+
+    @Override
+    public String getNewHeadName() {
+      return newHead;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java
index 168be5d..c2b8b03 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java
@@ -84,7 +84,7 @@
     try {
       return readImpl();
     } catch (OrmRuntimeException err) {
-      Throwables.propagateIfInstanceOf(err.getCause(), OrmException.class);
+      Throwables.throwIfInstanceOf(err.getCause(), OrmException.class);
       throw new OrmException(err);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/InternalQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/InternalQuery.java
new file mode 100644
index 0000000..36e5792
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/InternalQuery.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexConfig;
+import com.google.gerrit.server.index.Schema;
+import com.google.gwtorm.server.OrmException;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Execute a single query over a secondary index, for use by Gerrit internals.
+ * <p>
+ * By default, visibility of returned entities is not enforced (unlike in {@link
+ * QueryProcessor}). The methods in this class are not typically used by
+ * user-facing paths, but rather by internal callers that need to process all
+ * matching results.
+ */
+public class InternalQuery<T> {
+  private final QueryProcessor<T> queryProcessor;
+  private final IndexCollection<?, T, ? extends Index<?, T>> indexes;
+
+  protected final IndexConfig indexConfig;
+
+  protected InternalQuery(QueryProcessor<T> queryProcessor,
+      IndexCollection<?, T, ? extends Index<?, T>> indexes,
+          IndexConfig indexConfig) {
+    this.queryProcessor = queryProcessor.enforceVisibility(false);
+    this.indexes = indexes;
+    this.indexConfig = indexConfig;
+  }
+
+  public InternalQuery<T> setLimit(int n) {
+    queryProcessor.setLimit(n);
+    return this;
+  }
+
+  public InternalQuery<T> enforceVisibility(boolean enforce) {
+    queryProcessor.enforceVisibility(enforce);
+    return this;
+  }
+
+  public InternalQuery<T> setRequestedFields(Set<String> fields) {
+    queryProcessor.setRequestedFields(fields);
+    return this;
+  }
+
+  public InternalQuery<T> noFields() {
+    queryProcessor.setRequestedFields(ImmutableSet.<String> of());
+    return this;
+  }
+
+  public List<T> query(Predicate<T> p) throws OrmException {
+    try {
+      return queryProcessor.query(p).entities();
+    } catch (QueryParseException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  protected Schema<T> schema() {
+    Index<?, T> index = indexes != null ? indexes.getSearchIndex() : null;
+    return index != null ? index.getSchema() : null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
index d971d86..70bdffb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.index.QueryOptions;
 import com.google.gerrit.server.index.SchemaDefinitions;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -136,8 +137,10 @@
       throws OrmException, QueryParseException {
     try {
       return query(null, queries);
+    } catch (OrmRuntimeException e) {
+      throw new OrmException(e.getMessage(), e);
     } catch (OrmException e) {
-      Throwables.propagateIfInstanceOf(e.getCause(), QueryParseException.class);
+      Throwables.throwIfInstanceOf(e.getCause(), QueryParseException.class);
       throw e;
     }
   }
@@ -170,7 +173,7 @@
       // max for this user. The only way to see if there are more entities is to
       // ask for one more result from the query.
       QueryOptions opts =
-          createOptions(indexConfig, page, limit, getRequestedFields());
+          createOptions(indexConfig, start, limit + 1, getRequestedFields());
       Predicate<T> pred = rewriter.rewrite(q, opts);
       if (enforceVisibility) {
         pred = enforceVisibility(pred);
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 7a9f5bd..b3f92ff 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
@@ -14,7 +14,10 @@
 
 package com.google.gerrit.server.query.account;
 
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Ints;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.IndexPredicate;
@@ -22,12 +25,28 @@
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryBuilder;
 
+import java.util.List;
+
 public class AccountPredicates {
   public static boolean hasActive(Predicate<AccountState> p) {
     return QueryBuilder.find(p, AccountPredicate.class,
         AccountField.ACTIVE.getName()) != null;
   }
 
+  static Predicate<AccountState> defaultPredicate(String query) {
+    // Adapt the capacity of this list when adding more default predicates.
+    List<Predicate<AccountState>> preds = Lists.newArrayListWithCapacity(3);
+    Integer id = Ints.tryParse(query);
+    if (id != null) {
+      preds.add(id(new Account.Id(id)));
+    }
+    preds.add(equalsName(query));
+    preds.add(username(query));
+    // Adapt the capacity of the "predicates" list when adding more default
+    // predicates.
+    return Predicate.or(preds);
+  }
+
   static Predicate<AccountState> id(Account.Id accountId) {
     return new AccountPredicate(AccountField.ID,
         AccountQueryBuilder.FIELD_ACCOUNT, accountId.toString());
@@ -43,6 +62,14 @@
         AccountQueryBuilder.FIELD_NAME, name.toLowerCase());
   }
 
+  static Predicate<AccountState> externalId(String externalId) {
+    return new AccountPredicate(AccountField.EXTERNAL_ID, externalId);
+  }
+
+  static Predicate<AccountState> fullName(String fullName) {
+    return new AccountPredicate(AccountField.FULL_NAME, fullName);
+  }
+
   public static Predicate<AccountState> isActive() {
     return new AccountPredicate(AccountField.ACTIVE, "1");
   }
@@ -56,6 +83,10 @@
         AccountQueryBuilder.FIELD_USERNAME, username.toLowerCase());
   }
 
+  static Predicate<AccountState> watchedProject(Project.NameKey project) {
+    return new AccountPredicate(AccountField.WATCHED_PROJECT, project.get());
+  }
+
   static class AccountPredicate extends IndexPredicate<AccountState> {
     AccountPredicate(FieldDef<AccountState, ?> def, String value) {
       super(def, value);
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 b10d4c5..0288cb2 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
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.query.account;
 
+import com.google.common.base.Function;
+import com.google.common.base.Splitter;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.errors.NotSignedInException;
@@ -29,8 +31,6 @@
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 
-import java.util.List;
-
 /**
  * Parses a query string meant to be applied to account objects.
  */
@@ -122,21 +122,29 @@
     return AccountPredicates.username(username);
   }
 
-  @Override
-  public Predicate<AccountState> defaultField(String query)
-      throws QueryParseException {
-    if ("self".equalsIgnoreCase(query)) {
-      return AccountPredicates.id(self());
-    }
+  public Predicate<AccountState> defaultQuery(String query) {
+    return Predicate.and(
+        Lists.transform(Splitter.on(' ').omitEmptyStrings().splitToList(query),
+            new Function<String, Predicate<AccountState>>() {
+              @Override
+              public Predicate<AccountState> apply(String s) {
+                return defaultField(s);
+              }
+            }));
+  }
 
-    List<Predicate<AccountState>> preds = Lists.newArrayListWithCapacity(3);
-    Integer id = Ints.tryParse(query);
-    if (id != null) {
-      preds.add(AccountPredicates.id(new Account.Id(id)));
+  @Override
+  protected Predicate<AccountState> defaultField(String query) {
+    Predicate<AccountState> defaultPredicate =
+        AccountPredicates.defaultPredicate(query);
+    if ("self".equalsIgnoreCase(query)) {
+      try {
+        return Predicate.or(defaultPredicate, AccountPredicates.id(self()));
+      } catch (QueryParseException e) {
+        // Skip.
+      }
     }
-    preds.add(name(query));
-    preds.add(username(query));
-    return Predicate.or(preds);
+    return defaultPredicate;
   }
 
   private Account.Id self() throws QueryParseException {
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
new file mode 100644
index 0000000..7bc3144
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.account;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.IndexConfig;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
+import com.google.gerrit.server.query.InternalQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Set;
+
+public class InternalAccountQuery extends InternalQuery<AccountState> {
+  private static final Logger log =
+      LoggerFactory.getLogger(InternalAccountQuery.class);
+
+  @Inject
+  InternalAccountQuery(AccountQueryProcessor queryProcessor,
+      AccountIndexCollection indexes,
+      IndexConfig indexConfig) {
+    super(queryProcessor, indexes, indexConfig);
+  }
+
+  @Override
+  public InternalAccountQuery setLimit(int n) {
+    super.setLimit(n);
+    return this;
+  }
+
+  @Override
+  public InternalAccountQuery enforceVisibility(boolean enforce) {
+    super.enforceVisibility(enforce);
+    return this;
+  }
+
+  @Override
+  public InternalAccountQuery setRequestedFields(Set<String> fields) {
+    super.setRequestedFields(fields);
+    return this;
+  }
+
+  @Override
+  public InternalAccountQuery noFields() {
+    super.noFields();
+    return this;
+  }
+
+  public List<AccountState> byDefault(String query)
+      throws OrmException {
+    return query(AccountPredicates.defaultPredicate(query));
+  }
+
+  public List<AccountState> byExternalId(String externalId)
+      throws OrmException {
+    return query(AccountPredicates.externalId(externalId));
+  }
+
+  public AccountState oneByExternalId(String externalId) throws OrmException {
+    List<AccountState> accountStates = byExternalId(externalId);
+    if (accountStates.size() == 1) {
+      return accountStates.get(0);
+    } else if (accountStates.size() > 0) {
+      StringBuilder msg = new StringBuilder();
+      msg.append("Ambiguous external ID ")
+          .append(externalId)
+          .append("for accounts: ");
+      Joiner.on(", ").appendTo(msg,
+          Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION));
+      log.warn(msg.toString());
+    }
+    return null;
+  }
+
+  public List<AccountState> byFullName(String fullName)
+      throws OrmException {
+    return query(AccountPredicates.fullName(fullName));
+  }
+
+  public List<AccountState> byWatchedProject(Project.NameKey project)
+      throws OrmException {
+    return query(AccountPredicates.watchedProject(project));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 1929833..6526d3c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -50,6 +50,7 @@
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -349,6 +350,7 @@
   private Set<Account.Id> starredByUser;
   private ImmutableMultimap<Account.Id, String> stars;
   private ReviewerSet reviewers;
+  private List<ReviewerStatusUpdate> reviewerUpdates;
   private PersonIdent author;
   private PersonIdent committer;
 
@@ -782,8 +784,16 @@
       if (c == null) {
         currentApprovals = Collections.emptyList();
       } else {
-        currentApprovals = ImmutableList.copyOf(approvalsUtil.byPatchSet(
-            db, changeControl(), c.currentPatchSetId()));
+        try {
+          currentApprovals = ImmutableList.copyOf(approvalsUtil.byPatchSet(
+              db, changeControl(), c.currentPatchSetId()));
+        } catch (OrmException e) {
+          if (e.getCause() instanceof NoSuchChangeException) {
+            currentApprovals = Collections.emptyList();
+          } else {
+            throw e;
+          }
+        }
       }
     }
     return currentApprovals;
@@ -877,7 +887,7 @@
     return FluentIterable.from(patchSets()).filter(predicate).toList();
   }
 
-public void setPatchSets(Collection<PatchSet> patchSets) {
+  public void setPatchSets(Collection<PatchSet> patchSets) {
     this.currentPatchSet = null;
     this.patchSets = patchSets;
   }
@@ -940,6 +950,21 @@
     return reviewers;
   }
 
+  public List<ReviewerStatusUpdate> reviewerUpdates() throws OrmException {
+    if (reviewerUpdates == null) {
+      reviewerUpdates = approvalsUtil.getReviewerUpdates(notes());
+    }
+    return reviewerUpdates;
+  }
+
+  public void setReviewerUpdates(List<ReviewerStatusUpdate> reviewerUpdates) {
+    this.reviewerUpdates = reviewerUpdates;
+  }
+
+  public List<ReviewerStatusUpdate> getReviewerUpdates() {
+    return reviewerUpdates;
+  }
+
   public Collection<PatchLineComment> publishedComments()
       throws OrmException {
     if (publishedComments == null) {
@@ -985,9 +1010,18 @@
         mergeable = true;
       } else {
         PatchSet ps = currentPatchSet();
-        if (ps == null || !changeControl().isPatchVisible(ps, db)) {
-          return null;
+        try {
+          if (ps == null || !changeControl().isPatchVisible(ps, db)) {
+            return null;
+          }
+        } catch (OrmException e) {
+          if (e.getCause() instanceof NoSuchChangeException) {
+            return null;
+          } else {
+            throw e;
+          }
         }
+
         try (Repository repo = repoManager.openRepository(project())) {
           Ref ref = repo.getRefDatabase().exactRef(c.getDest().get());
           SubmitTypeRecord str = submitTypeRecord();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index a150c93..d7c7730 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupBackend;
@@ -184,6 +185,7 @@
     final IndexConfig indexConfig;
     final Provider<ListMembers> listMembers;
     final StarredChangesUtil starredChangesUtil;
+    final AccountCache accountCache;
     final boolean allowsDrafts;
 
     private final Provider<CurrentUser> self;
@@ -217,6 +219,7 @@
         IndexConfig indexConfig,
         Provider<ListMembers> listMembers,
         StarredChangesUtil starredChangesUtil,
+        AccountCache accountCache,
         @GerritServerConfig Config cfg) {
       this(db, queryProvider, rewriter, opFactories, userFactory, self,
           capabilityControlFactory, changeControlGenericFactory, notesFactory,
@@ -224,7 +227,7 @@
           allProjectsName, allUsersName, patchListCache, repoManager,
           projectCache, listChildProjects, submitDryRun, conflictsCache,
           trackingFooters, indexes != null ? indexes.getSearchIndex() : null,
-          indexConfig, listMembers, starredChangesUtil,
+          indexConfig, listMembers, starredChangesUtil, accountCache,
           cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true));
     }
 
@@ -256,6 +259,7 @@
         IndexConfig indexConfig,
         Provider<ListMembers> listMembers,
         StarredChangesUtil starredChangesUtil,
+        AccountCache accountCache,
         boolean allowsDrafts) {
      this.db = db;
      this.queryProvider = queryProvider;
@@ -284,6 +288,7 @@
      this.indexConfig = indexConfig;
      this.listMembers = listMembers;
      this.starredChangesUtil = starredChangesUtil;
+     this.accountCache = accountCache;
      this.allowsDrafts = allowsDrafts;
     }
 
@@ -295,7 +300,7 @@
           allProjectsName, allUsersName, patchListCache, repoManager,
           projectCache, listChildProjects, submitDryRun,
           conflictsCache, trackingFooters, index, indexConfig, listMembers,
-          starredChangesUtil, allowsDrafts);
+          starredChangesUtil, accountCache, allowsDrafts);
     }
 
     Arguments asUser(Account.Id otherId) {
@@ -744,7 +749,7 @@
     if ("self".equals(who)) {
       return is_visible();
     }
-    Set<Account.Id> m = args.accountResolver.findAll(who);
+    Set<Account.Id> m = args.accountResolver.findAll(args.db.get(), who);
     if (!m.isEmpty()) {
       List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
       for (Account.Id id : m) {
@@ -964,7 +969,8 @@
       }
     }
 
-    List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(9);
+    // Adapt the capacity of this list when adding more default predicates.
+    List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(11);
     try {
       predicates.add(commit(query));
     } catch (IllegalArgumentException e) {
@@ -992,6 +998,8 @@
     predicates.add(ref(query));
     predicates.add(branch(query));
     predicates.add(topic(query));
+    // Adapt the capacity of the "predicates" list when adding more default
+    // predicates.
     return Predicate.or(predicates);
   }
 
@@ -1000,7 +1008,7 @@
     if ("self".equals(who)) {
       return Collections.singleton(self());
     }
-    Set<Account.Id> matches = args.accountResolver.findAll(who);
+    Set<Account.Id> matches = args.accountResolver.findAll(args.db.get(), who);
     if (matches.isEmpty()) {
       throw error("User " + who + " not found");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index 44e5e7e..0ff5ac7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -72,7 +72,7 @@
   @Override
   protected QueryOptions createOptions(IndexConfig indexConfig, int start,
       int limit, Set<String> requestedFields) {
-    return IndexedChangeQuery.createOptions(indexConfig, start, limit + 1,
+    return IndexedChangeQuery.createOptions(indexConfig, start, limit,
         requestedFields);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 680e796..27a7ec7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -24,10 +24,8 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
 import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -35,12 +33,10 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.index.IndexConfig;
-import com.google.gerrit.server.index.Schema;
-import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.query.InternalQuery;
 import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
@@ -55,15 +51,7 @@
 import java.util.List;
 import java.util.Set;
 
-/**
- * Execute a single query over changes, for use by Gerrit internals.
- * <p>
- * By default, visibility of returned changes is not enforced (unlike in {@link
- * ChangeQueryProcessor}). The methods in this class are not typically used by
- * user-facing paths, but rather by internal callers that need to process all
- * matching results.
- */
-public class InternalChangeQuery {
+public class InternalChangeQuery extends InternalQuery<ChangeData> {
   private static Predicate<ChangeData> ref(Branch.NameKey branch) {
     return new RefPredicate(branch.get());
   }
@@ -84,42 +72,41 @@
     return new CommitPredicate(id);
   }
 
-  private final IndexConfig indexConfig;
-  private final ChangeQueryProcessor qp;
-  private final ChangeIndexCollection indexes;
   private final ChangeData.Factory changeDataFactory;
   private final ChangeNotes.Factory notesFactory;
 
   @Inject
-  InternalChangeQuery(IndexConfig indexConfig,
-      ChangeQueryProcessor queryProcessor,
+  InternalChangeQuery(ChangeQueryProcessor queryProcessor,
       ChangeIndexCollection indexes,
+      IndexConfig indexConfig,
       ChangeData.Factory changeDataFactory,
       ChangeNotes.Factory notesFactory) {
-    this.indexConfig = indexConfig;
-    qp = queryProcessor.enforceVisibility(false);
-    this.indexes = indexes;
+    super(queryProcessor, indexes, indexConfig);
     this.changeDataFactory = changeDataFactory;
     this.notesFactory = notesFactory;
   }
 
+  @Override
   public InternalChangeQuery setLimit(int n) {
-    qp.setLimit(n);
+    super.setLimit(n);
     return this;
   }
 
+  @Override
   public InternalChangeQuery enforceVisibility(boolean enforce) {
-    qp.enforceVisibility(enforce);
+    super.enforceVisibility(enforce);
     return this;
   }
 
+  @Override
   public InternalChangeQuery setRequestedFields(Set<String> fields) {
-    qp.setRequestedFields(fields);
+    super.setRequestedFields(fields);
     return this;
   }
 
+  @Override
   public InternalChangeQuery noFields() {
-    qp.setRequestedFields(ImmutableSet.<String> of());
+    super.noFields();
     return this;
   }
 
@@ -281,7 +268,7 @@
   }
 
   public List<ChangeData> bySubmissionId(String cs) throws OrmException {
-    if (Strings.isNullOrEmpty(cs) || !schema(indexes).hasField(SUBMISSIONID)) {
+    if (Strings.isNullOrEmpty(cs) || !schema().hasField(SUBMISSIONID)) {
       return Collections.emptyList();
     }
     return query(new SubmissionIdPredicate(cs));
@@ -300,18 +287,4 @@
   public List<ChangeData> byIsStarred(Account.Id id) throws OrmException {
     return query(new IsStarredByPredicate(id));
   }
-
-  public List<ChangeData> query(Predicate<ChangeData> p) throws OrmException {
-    try {
-      return qp.query(p).entities();
-    } catch (QueryParseException e) {
-      throw new OrmException(e);
-    }
-  }
-
-  private static Schema<ChangeData> schema(
-      @Nullable ChangeIndexCollection indexes) {
-    ChangeIndex index = indexes != null ? indexes.getSearchIndex() : null;
-    return index != null ? index.getSchema() : null;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index c8512bf..0b7a2f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -14,30 +14,20 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryBuilder;
 import com.google.gerrit.server.query.QueryParseException;
-import com.google.gwtorm.server.OrmException;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
 class IsWatchedByPredicate extends AndPredicate<ChangeData> {
-  private static final Logger log =
-      LoggerFactory.getLogger(IsWatchedByPredicate.class);
-
-  private static final CurrentUser.PropertyKey<List<AccountProjectWatch>> PROJECT_WATCHES =
-      CurrentUser.PropertyKey.create();
-
   private static String describe(CurrentUser user) {
     if (user.isIdentifiedUser()) {
       return user.getAccountId().toString();
@@ -58,11 +48,11 @@
       boolean checkIsVisible) throws QueryParseException {
     List<Predicate<ChangeData>> r = new ArrayList<>();
     ChangeQueryBuilder builder = new ChangeQueryBuilder(args);
-    for (AccountProjectWatch w : getWatches(args)) {
+    for (ProjectWatchKey w : getWatches(args)) {
       Predicate<ChangeData> f = null;
-      if (w.getFilter() != null) {
+      if (w.filter() != null) {
         try {
-          f = builder.parse(w.getFilter());
+          f = builder.parse(w.filter());
           if (QueryBuilder.find(f, IsWatchedByPredicate.class) != null) {
             // If the query is going to infinite loop, assume it
             // will never match and return null. Yes this test
@@ -76,10 +66,10 @@
       }
 
       Predicate<ChangeData> p;
-      if (w.getProjectNameKey().equals(args.allProjectsName)) {
+      if (w.project().equals(args.allProjectsName)) {
         p = null;
       } else {
-        p = builder.project(w.getProjectNameKey().get());
+        p = builder.project(w.project().get());
       }
 
       if (p != null && f != null) {
@@ -101,22 +91,14 @@
     }
   }
 
-  private static List<AccountProjectWatch> getWatches(
+  private static Collection<ProjectWatchKey> getWatches(
       ChangeQueryBuilder.Arguments args) throws QueryParseException {
     CurrentUser user = args.getUser();
-    List<AccountProjectWatch> watches = user.get(PROJECT_WATCHES);
-    if (watches == null && user.isIdentifiedUser()) {
-      try {
-        watches = args.db.get().accountProjectWatches()
-            .byAccount(user.asIdentifiedUser().getAccountId()).toList();
-        user.put(PROJECT_WATCHES, watches);
-      } catch (OrmException e) {
-        log.warn("Cannot load accountProjectWatches", e);
-      }
+    if (user.isIdentifiedUser()) {
+      return args.accountCache.get(args.getUser().getAccountId())
+          .getProjectWatches().keySet();
     }
-    return MoreObjects.firstNonNull(
-        watches,
-        Collections.<AccountProjectWatch> emptyList());
+    return Collections.<ProjectWatchKey> emptySet();
   }
 
   private static List<Predicate<ChangeData>> none() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index e6d9385..7c7417a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -138,10 +138,12 @@
       AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
       AccessSection tags = config.getAccessSection("refs/tags/*", true);
       AccessSection meta = config.getAccessSection(RefNames.REFS_CONFIG, true);
+      AccessSection refsFor = config.getAccessSection("refs/for/*", true);
       AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
 
       grant(config, cap, GlobalCapability.ADMINISTRATE_SERVER, admin);
       grant(config, all, Permission.READ, admin, anonymous);
+      grant(config, refsFor, Permission.ADD_PATCH_SET, registered);
 
       if (batch != null) {
         Permission priority = cap.getPermission(GlobalCapability.PRIORITY, true);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java
index 3bec395..7d64437 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java
@@ -40,7 +40,7 @@
     if (database == null || database.isEmpty()) {
       database = "db/ReviewDB";
     }
-    return createUrl(site.resolve(database));
+    return appendUrlOptions(cfg, createUrl(site.resolve(database)));
   }
 
   public static String createUrl(Path path) {
@@ -50,16 +50,20 @@
         .toString();
   }
 
-  public static String appendCacheSize(Config cfg, String url) {
-    long h2CacheSize = cfg.getLong("cache", null, "h2CacheSize", -1);
+  public static String appendUrlOptions(Config cfg, String url) {
+    long h2CacheSize = cfg.getLong("database", "h2", "cacheSize", -1);
+    boolean h2AutoServer = cfg.getBoolean("database", "h2", "autoServer", false);
+
+    StringBuilder urlBuilder = new StringBuilder().append(url);
+
     if (h2CacheSize >= 0) {
       // H2 CACHE_SIZE is always given in KB
-      return new StringBuilder()
-          .append(url)
-          .append(";CACHE_SIZE=")
-          .append(h2CacheSize / 1024)
-          .toString();
+      urlBuilder.append(";CACHE_SIZE=")
+          .append(h2CacheSize / 1024);
     }
-    return url;
+    if (h2AutoServer) {
+      urlBuilder.append(";AUTO_SERVER=TRUE");
+    }
+    return urlBuilder.toString();
   }
 }
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 8e74f6c..9573f99 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
@@ -75,7 +75,7 @@
   @Inject
   H2AccountPatchReviewStore(@GerritServerConfig Config cfg,
       SitePaths sitePaths) {
-    this.url = H2.appendCacheSize(cfg, getUrl(sitePaths));
+    this.url = H2.appendUrlOptions(cfg, getUrl(sitePaths));
   }
 
   public static String getUrl(SitePaths sitePaths) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 6ad2fb8..7217fd0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -33,7 +33,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_127> C = Schema_127.class;
+  public static final Class<Schema_129> C = Schema_129.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java
index e95353a..fc1b0cd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java
@@ -79,10 +79,10 @@
         }
         RefSpec newRefSpec = new RefSpec(subbranch.get() + ":" + superBranch.get());
 
-        if (!s.getRefSpecs().contains(newRefSpec)) {
+        if (!s.getMatchingRefSpecs().contains(newRefSpec)) {
           // For the migration we use only exact RefSpecs, we're not trying to
           // generalize it.
-          s.addRefSpec(newRefSpec);
+          s.addMatchingRefSpec(newRefSpec);
         }
 
         pc.commit(md);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java
index 8791c96..16f0bcf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java
@@ -104,13 +104,12 @@
       for (Map.Entry<Account.Id, Collection<AccountSshKey>> e : imports.asMap()
           .entrySet()) {
         try (MetaDataUpdate md = new MetaDataUpdate(
-                 GitReferenceUpdated.DISABLED, allUsersName, git, bru);
-             VersionedAuthorizedKeys authorizedKeys =
-                 new VersionedAuthorizedKeys(
-                     new SimpleSshKeyCreator(), e.getKey())) {
+                 GitReferenceUpdated.DISABLED, allUsersName, git, bru)) {
           md.getCommitBuilder().setAuthor(serverUser);
           md.getCommitBuilder().setCommitter(serverUser);
 
+          VersionedAuthorizedKeys authorizedKeys = new VersionedAuthorizedKeys(
+              new SimpleSshKeyCreator(), e.getKey());
           authorizedKeys.load(md);
           authorizedKeys.setKeys(fixInvalidSequenceNumbers(e.getValue()));
           authorizedKeys.commit(md);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_128.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_128.java
new file mode 100644
index 0000000..9552fa4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_128.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.schema.AclUtil.grant;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+public class Schema_128 extends SchemaVersion {
+  private static final String COMMIT_MSG =
+      "Add addPatchSet permission to all projects";
+
+  private final GitRepositoryManager repoManager;
+  private final AllProjectsName allProjectsName;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_128(Provider<Schema_126> prior,
+      GitRepositoryManager repoManager,
+      AllProjectsName allProjectsName,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.allProjectsName = allProjectsName;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    try (Repository git = repoManager.openRepository(allProjectsName);
+        MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
+            allProjectsName, git)) {
+      ProjectConfig config = ProjectConfig.read(md);
+
+      GroupReference registered = SystemGroupBackend.getGroup(REGISTERED_USERS);
+      AccessSection refsFor = config.getAccessSection("refs/for/*", true);
+      grant(config, refsFor, Permission.ADD_PATCH_SET,
+          false, false, registered);
+
+      md.getCommitBuilder().setAuthor(serverUser);
+      md.getCommitBuilder().setCommitter(serverUser);
+      md.setMessage(COMMIT_MSG);
+      config.commit(md);
+    } catch (ConfigInvalidException | IOException ex) {
+      throw new OrmException(ex);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_129.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_129.java
new file mode 100644
index 0000000..de02ec7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_129.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_129 extends SchemaVersion {
+
+  @Inject
+  Schema_129(Provider<Schema_128> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void preUpdateSchema(ReviewDb db) throws OrmException {
+    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement()) {
+      stmt.execute("ALTER TABLE patch_sets MODIFY groups clob");
+    } catch (SQLException e) {
+      // Ignore.  Type may have already been modified manually.
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
index b481944..c8190a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
@@ -19,6 +19,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.file.LockFile;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -27,20 +28,26 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 @Singleton
 public class DefaultSecureStore extends SecureStore {
   private final FileBasedConfig sec;
+  private final Map<String, FileBasedConfig> pluginSec;
+  private final SitePaths site;
 
   @Inject
   DefaultSecureStore(SitePaths site) {
+    this.site = site;
     sec = new FileBasedConfig(site.secure_config.toFile(), FS.DETECTED);
     try {
       sec.load();
-    } catch (Exception e) {
+    } catch (IOException | ConfigInvalidException e) {
       throw new RuntimeException("Cannot load secure.config", e);
     }
+    this.pluginSec = new HashMap<>();
   }
 
   @Override
@@ -49,6 +56,28 @@
   }
 
   @Override
+  public synchronized String[] getListForPlugin(String pluginName, String section,
+    String subsection, String name) {
+    FileBasedConfig cfg = null;
+    if (pluginSec.containsKey(pluginName)) {
+      cfg = pluginSec.get(pluginName);
+    } else {
+      String filename = pluginName + ".secure.config";
+      File pluginConfigFile = site.etc_dir.resolve(filename).toFile();
+      if (pluginConfigFile.exists()) {
+        cfg = new FileBasedConfig(pluginConfigFile, FS.DETECTED);
+        try {
+          cfg.load();
+          pluginSec.put(pluginName, cfg);
+        } catch (IOException | ConfigInvalidException e) {
+          throw new RuntimeException("Cannot load " + filename, e);
+        }
+      }
+    }
+    return cfg != null ? cfg.getStringList(section, subsection, name) : null;
+  }
+
+  @Override
   public void setList(String section, String subsection, String name,
       List<String> values) {
     if (values != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStore.java
index 2a0086e..122e26b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStore.java
@@ -75,6 +75,38 @@
   }
 
   /**
+   * Extract decrypted value of stored plugin config property from SecureStore
+   * or {@code null} when property was not found.
+   *
+   * @param pluginName
+   * @param section
+   * @param subsection
+   * @param name
+   * @return decrypted String value or {@code null} if not found
+   */
+  public final String getForPlugin(String pluginName, String section,
+      String subsection, String name) {
+    String[] values = getListForPlugin(pluginName, section, subsection, name);
+    if (values != null && values.length > 0) {
+      return values[0];
+    }
+    return null;
+  }
+
+  /**
+   * Extract list of plugin config values from SecureStore and decrypt every
+   * value in that list, or {@code null} when property was not found.
+   *
+   * @param pluginName
+   * @param section
+   * @param subsection
+   * @param name
+   * @return decrypted list of string values or {@code null}
+   */
+  public abstract String[] getListForPlugin(String pluginName, String section,
+      String subsection, String name);
+
+  /**
    * Extract list of values from SecureStore and decrypt every value in that
    * list or {@code null} when property was not found.
    *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java b/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java
index 37383b7..9e48aad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java
@@ -16,6 +16,8 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.Version;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -60,7 +62,11 @@
    * @param name path of the item, relative to the root of the catalog.
    * @return the entry; null if the item is not part of the catalog.
    */
-  public Entry get(String name) {
+  @Nullable
+  public Entry get(@Nullable String name) {
+    if (Strings.isNullOrEmpty(name)) {
+      return null;
+    }
     if (name.startsWith("/")) {
       name = name.substring(1);
     }
@@ -109,6 +115,7 @@
     return Collections.unmodifiableSortedMap(toc);
   }
 
+  @Nullable
   private static byte[] read(String path) {
     String name = "root/" + path;
     try (InputStream in = ToolsCatalog.class.getResourceAsStream(name)) {
@@ -128,6 +135,7 @@
     }
   }
 
+  @Nullable
   private static String dirOf(String path) {
     final int s = path.lastIndexOf('/');
     return s < 0 ? null : path.substring(0, s);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/GitUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GitUtil.java
new file mode 100644
index 0000000..2d1e1fa
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/GitUtil.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2016 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.util;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+public class GitUtil {
+
+  /**
+   * @param git
+   * @param commitId
+   * @param parentNum
+   * @return the {@code paretNo} parent of given commit or {@code null}
+   *             when {@code parentNo} exceed number of {@code commitId} parents.
+   * @throws IncorrectObjectTypeException
+   *             the supplied id is not a commit or an annotated tag.
+   * @throws IOException
+   *             a pack file or loose object could not be read.
+   */
+  public static RevCommit getParent(Repository git,
+      ObjectId commitId, int parentNum) throws IOException {
+    try (RevWalk walk = new RevWalk(git)) {
+      RevCommit commit = walk.parseCommit(commitId);
+      if (commit.getParentCount() > parentNum) {
+        return commit.getParent(parentNum);
+      }
+    }
+    return null;
+  }
+
+  private GitUtil() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java
new file mode 100644
index 0000000..4f43c2a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2016 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.util;
+
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/** Unique identifier for an end-user request, used in logs and similar. */
+public class RequestId {
+  private static final String MACHINE_ID;
+  static {
+    String id;
+    try {
+      id = InetAddress.getLocalHost().getHostAddress();
+    } catch (UnknownHostException e) {
+      id = "unknown";
+    }
+    MACHINE_ID = id;
+  }
+
+  public static RequestId forChange(Change c) {
+    return new RequestId(c.getId().toString());
+  }
+
+  public static RequestId forProject(Project.NameKey p) {
+    return new RequestId(p.toString());
+  }
+
+  private final String str;
+
+  private RequestId(String resourceId) {
+    Hasher h = Hashing.sha1().newHasher();
+    h.putLong(Thread.currentThread().getId())
+        .putUnencodedChars(MACHINE_ID);
+    str = "[" + resourceId + "-" + TimeUtil.nowTs().getTime() +
+        "-" + h.hash().toString().substring(0, 8) + "]";
+  }
+
+  @Override
+  public String toString() {
+    return str;
+  }
+
+  public String toStringForStorage() {
+    return str.substring(1, str.length() - 1);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
index 382485e..bdbb938 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -129,7 +129,7 @@
           try {
             wrapped.call();
           } catch (Exception e) {
-            Throwables.propagateIfPossible(e);
+            Throwables.throwIfUnchecked(e);
             throw new RuntimeException(e); // Not possible.
           }
         }
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/footer.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/footer.soy
new file mode 100644
index 0000000..6800bb7
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/footer.soy
@@ -0,0 +1,24 @@
+/**
+ * Copyright (C) 2016 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.
+*/
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The footer template will determine the contents of the footer text
+ * appended to the end of all outgoing emails after the ChangeFooter and
+ * CommentFooter.
+ */
+{template .footer}
+{/template}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/account/WatchConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/account/WatchConfigTest.java
new file mode 100644
index 0000000..0619a78
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/account/WatchConfigTest.java
@@ -0,0 +1,178 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.account.WatchConfig.NotifyValue;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
+import com.google.gerrit.server.git.ValidationError;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class WatchConfigTest implements ValidationError.Sink {
+  private List<ValidationError> validationErrors = new ArrayList<>();
+
+  @Before
+  public void setup() {
+    validationErrors.clear();
+  }
+
+  @Test
+  public void parseWatchConfig() throws Exception {
+    Config cfg = new Config();
+    cfg.fromText("[project \"myProject\"]\n"
+        + "  notify = * [ALL_COMMENTS, NEW_PATCHSETS]\n"
+        + "  notify = branch:master [NEW_CHANGES]\n"
+        + "  notify = branch:master [NEW_PATCHSETS]\n"
+        + "  notify = branch:foo []\n"
+        + "[project \"otherProject\"]\n"
+        + "  notify = [NEW_PATCHSETS]\n"
+        + "  notify = * [NEW_PATCHSETS, ALL_COMMENTS]\n");
+    Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
+        WatchConfig.parse(new Account.Id(1000000), cfg, this);
+
+    assertThat(validationErrors).isEmpty();
+
+    Project.NameKey myProject = new Project.NameKey("myProject");
+    Project.NameKey otherProject = new Project.NameKey("otherProject");
+    Map<ProjectWatchKey, Set<NotifyType>> expectedProjectWatches =
+        new HashMap<>();
+    expectedProjectWatches.put(ProjectWatchKey.create(myProject, null),
+        EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS));
+    expectedProjectWatches.put(
+        ProjectWatchKey.create(myProject, "branch:master"),
+        EnumSet.of(NotifyType.NEW_CHANGES, NotifyType.NEW_PATCHSETS));
+    expectedProjectWatches.put(ProjectWatchKey.create(myProject, "branch:foo"),
+        EnumSet.noneOf(NotifyType.class));
+    expectedProjectWatches.put(ProjectWatchKey.create(otherProject, null),
+        EnumSet.of(NotifyType.NEW_PATCHSETS));
+    expectedProjectWatches.put(ProjectWatchKey.create(otherProject, null),
+        EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS));
+    assertThat(projectWatches).containsExactlyEntriesIn(expectedProjectWatches);
+  }
+
+  @Test
+  public void parseInvalidWatchConfig() throws Exception {
+    Config cfg = new Config();
+    cfg.fromText("[project \"myProject\"]\n"
+        + "  notify = * [ALL_COMMENTS, NEW_PATCHSETS]\n"
+        + "  notify = branch:master [INVALID, NEW_CHANGES]\n"
+        + "[project \"otherProject\"]\n"
+        + "  notify = [NEW_PATCHSETS]\n");
+
+    WatchConfig.parse(new Account.Id(1000000), cfg, this);
+    assertThat(validationErrors).hasSize(1);
+    assertThat(validationErrors.get(0).getMessage()).isEqualTo(
+        "watch.config: Invalid notify type INVALID in project watch of"
+            + " account 1000000 for project myProject: branch:master"
+            + " [INVALID, NEW_CHANGES]");
+  }
+
+  @Test
+  public void parseNotifyValue() throws Exception {
+    assertParseNotifyValue("* []", null, EnumSet.noneOf(NotifyType.class));
+    assertParseNotifyValue("* [ALL_COMMENTS]", null,
+        EnumSet.of(NotifyType.ALL_COMMENTS));
+    assertParseNotifyValue("[]", null, EnumSet.noneOf(NotifyType.class));
+    assertParseNotifyValue("[ALL_COMMENTS, NEW_PATCHSETS]", null,
+        EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS));
+    assertParseNotifyValue("branch:master []", "branch:master",
+        EnumSet.noneOf(NotifyType.class));
+    assertParseNotifyValue("branch:master || branch:stable []",
+        "branch:master || branch:stable", EnumSet.noneOf(NotifyType.class));
+    assertParseNotifyValue("branch:master [ALL_COMMENTS]", "branch:master",
+        EnumSet.of(NotifyType.ALL_COMMENTS));
+    assertParseNotifyValue("branch:master [ALL_COMMENTS, NEW_PATCHSETS]",
+        "branch:master",
+        EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS));
+    assertParseNotifyValue("* [ALL]", null, EnumSet.of(NotifyType.ALL));
+
+    assertThat(validationErrors).isEmpty();
+  }
+
+  @Test
+  public void parseInvalidNotifyValue() {
+    assertParseNotifyValueFails("* [] illegal-characters-at-the-end");
+    assertParseNotifyValueFails("* [INVALID]");
+    assertParseNotifyValueFails("* [ALL_COMMENTS, UNKNOWN]");
+    assertParseNotifyValueFails("* [ALL_COMMENTS NEW_CHANGES]");
+    assertParseNotifyValueFails("* [ALL_COMMENTS, NEW_CHANGES");
+    assertParseNotifyValueFails("* ALL_COMMENTS, NEW_CHANGES]");
+  }
+
+  @Test
+  public void toNotifyValue() throws Exception {
+    assertToNotifyValue(null, EnumSet.noneOf(NotifyType.class), "* []");
+    assertToNotifyValue("*", EnumSet.noneOf(NotifyType.class), "* []");
+    assertToNotifyValue(null, EnumSet.of(NotifyType.ALL_COMMENTS),
+        "* [ALL_COMMENTS]");
+    assertToNotifyValue("branch:master", EnumSet.noneOf(NotifyType.class),
+        "branch:master []");
+    assertToNotifyValue("branch:master",
+        EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS),
+        "branch:master [ALL_COMMENTS, NEW_PATCHSETS]");
+    assertToNotifyValue("branch:master",
+        EnumSet.of(NotifyType.ABANDONED_CHANGES, NotifyType.ALL_COMMENTS,
+            NotifyType.NEW_CHANGES, NotifyType.NEW_PATCHSETS,
+            NotifyType.SUBMITTED_CHANGES),
+        "branch:master [ABANDONED_CHANGES, ALL_COMMENTS, NEW_CHANGES,"
+        + " NEW_PATCHSETS, SUBMITTED_CHANGES]");
+    assertToNotifyValue("*", EnumSet.of(NotifyType.ALL), "* [ALL]");
+  }
+
+  private void assertParseNotifyValue(String notifyValue,
+      String expectedFilter, Set<NotifyType> expectedNotifyTypes) {
+    NotifyValue nv = parseNotifyValue(notifyValue);
+    assertThat(nv.filter()).isEqualTo(expectedFilter);
+    assertThat(nv.notifyTypes()).containsExactlyElementsIn(expectedNotifyTypes);
+  }
+
+  private static void assertToNotifyValue(String filter,
+      Set<NotifyType> notifyTypes, String expectedNotifyValue) {
+    NotifyValue nv = NotifyValue.create(filter, notifyTypes);
+    assertThat(nv.toString()).isEqualTo(expectedNotifyValue);
+  }
+
+  private void assertParseNotifyValueFails(String notifyValue) {
+    assertThat(validationErrors).isEmpty();
+    parseNotifyValue(notifyValue);
+    assertThat(validationErrors)
+        .named("expected validation error for notifyValue: " + notifyValue)
+        .isNotEmpty();
+    validationErrors.clear();
+  }
+
+  private NotifyValue parseNotifyValue(String notifyValue) {
+    return NotifyValue.parse(new Account.Id(1000000), "project", notifyValue, this);
+  }
+
+  @Override
+  public void error(ValidationError error) {
+    validationErrors.add(error);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/BatchUpdateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/BatchUpdateTest.java
new file mode 100644
index 0000000..87bfa00
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/BatchUpdateTest.java
@@ -0,0 +1,134 @@
+package com.google.gerrit.server.git;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
+import com.google.gerrit.server.git.BatchUpdate.RepoOnlyOp;
+import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gerrit.testutil.InMemoryDatabase;
+import com.google.gerrit.testutil.InMemoryModule;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class BatchUpdateTest {
+  @Inject
+  private AccountManager accountManager;
+
+  @Inject
+  private IdentifiedUser.GenericFactory userFactory;
+
+  @Inject
+  private InMemoryDatabase schemaFactory;
+
+  @Inject
+  private InMemoryRepositoryManager repoManager;
+
+  @Inject
+  private SchemaCreator schemaCreator;
+
+  @Inject
+  private ThreadLocalRequestContext requestContext;
+
+  @Inject
+  private BatchUpdate.Factory batchUpdateFactory;
+
+  private LifecycleManager lifecycle;
+  private ReviewDb db;
+  private TestRepository<InMemoryRepository> repo;
+  private Project.NameKey project;
+  private IdentifiedUser user;
+
+  @Before
+  public void setUp() throws Exception {
+    Injector injector = Guice.createInjector(new InMemoryModule());
+    injector.injectMembers(this);
+    lifecycle = new LifecycleManager();
+    lifecycle.add(injector);
+    lifecycle.start();
+
+    db = schemaFactory.open();
+    schemaCreator.create(db);
+    Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user"))
+        .getAccountId();
+    user = userFactory.create(userId);
+
+    project = new Project.NameKey("test");
+
+    InMemoryRepository inMemoryRepo = repoManager.createRepository(project);
+    repo = new TestRepository<>(inMemoryRepo);
+
+    requestContext.setContext(new RequestContext() {
+      @Override
+      public CurrentUser getUser() {
+        return user;
+      }
+
+      @Override
+      public Provider<ReviewDb> getReviewDbProvider() {
+        return Providers.of(db);
+      }
+    });
+  }
+
+  @After
+  public void tearDown() {
+    if (repo != null) {
+      repo.getRepository().close();
+    }
+    if (lifecycle != null) {
+      lifecycle.stop();
+    }
+    requestContext.setContext(null);
+    if (db != null) {
+      db.close();
+    }
+    InMemoryDatabase.drop(schemaFactory);
+  }
+
+  @Test
+  public void addRefUpdateFromFastForwardCommit() throws Exception {
+    final RevCommit masterCommit = repo.branch("master").commit().create();
+    final RevCommit branchCommit =
+        repo.branch("branch").commit().parent(masterCommit).create();
+
+    try (BatchUpdate bu = batchUpdateFactory
+        .create(db, project, user, TimeUtil.nowTs())) {
+      bu.addRepoOnlyOp(new RepoOnlyOp() {
+        @Override
+        public void updateRepo(RepoContext ctx) throws Exception {
+          ctx.addRefUpdate(
+              new ReceiveCommand(masterCommit.getId(), branchCommit.getId(),
+                  "refs/heads/master"));
+        }
+      });
+      bu.execute();
+    }
+
+    assertEquals(
+        repo.getRepository().exactRef("refs/heads/master").getObjectId(),
+        branchCommit.getId());
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index 0b5ed32..545fd08 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -29,7 +29,8 @@
           FakeQueryBuilder.class),
         new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
           null, null, null, null, null, null, null, null, null, null, null,
-          null, null, null, indexes, null, null, null, null, null, null, null));
+          null, null, null, indexes, null, null, null, null, null, null, null,
+          null));
   }
 
   @Operator
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
index b96b780..87dd3d9 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
@@ -25,15 +25,21 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
 
 public class FromAddressGeneratorProviderTest {
   private Config config;
@@ -56,6 +62,10 @@
     config.setString("sendemail", null, "from", newFrom);
   }
 
+  private void setDomains(List<String> domains) {
+    config.setStringList("sendemail", null, "allowedDomain", domains);
+  }
+
   @Test
   public void testDefaultIsMIXED() {
     assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class);
@@ -114,7 +124,7 @@
     replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.name).isEqualTo(name);
+    assertThat(r.name).isEqualTo(name + " (Code Review)");
     assertThat(r.email).isEqualTo(ident.getEmailAddress());
     verify(accountCache);
   }
@@ -131,6 +141,88 @@
   }
 
   @Test
+  public void testUSERAllowDomain() {
+    setFrom("USER");
+    setDomains(Arrays.asList("*.example.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.example.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name);
+    assertThat(r.email).isEqualTo(email);
+    verify(accountCache);
+  }
+
+  @Test
+  public void testUSERNoAllowDomain() {
+    setFrom("USER");
+    setDomains(Arrays.asList("example.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name + " (Code Review)");
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
+    verify(accountCache);
+  }
+
+  @Test
+  public void testUSERAllowDomainTwice() {
+    setFrom("USER");
+    setDomains(Arrays.asList("example.com"));
+    setDomains(Arrays.asList("test.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name);
+    assertThat(r.email).isEqualTo(email);
+    verify(accountCache);
+  }
+
+  @Test
+  public void testUSERAllowDomainTwiceReverse() {
+    setFrom("USER");
+    setDomains(Arrays.asList("test.com"));
+    setDomains(Arrays.asList("example.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name + " (Code Review)");
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
+    verify(accountCache);
+  }
+
+  @Test
+  public void testUSERAllowTwoDomains() {
+    setFrom("USER");
+    setDomains(Arrays.asList("example.com", "test.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name);
+    assertThat(r.email).isEqualTo(email);
+    verify(accountCache);
+  }
+
+  @Test
   public void testSelectSERVER() {
     setFrom("SERVER");
     assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class);
@@ -298,6 +390,7 @@
     account.setFullName(name);
     account.setPreferredEmail(email);
     return new AccountState(account, Collections.<AccountGroup.UUID> emptySet(),
-          Collections.<AccountExternalId> emptySet());
+        Collections.<AccountExternalId> emptySet(),
+        new HashMap<ProjectWatchKey, Set<NotifyType>>());
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java
index 6707c9f..4f2c776 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.mail;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
 
-import org.apache.commons.validator.routines.EmailValidator;
 import org.junit.Test;
 
 import java.io.BufferedReader;
@@ -27,6 +27,11 @@
   private static final String UNSUPPORTED_PREFIX = "#! ";
 
   @Test
+  public void validateLocalDomain() throws Exception {
+    assertThat(OutgoingEmailValidator.isValid("foo@bar.local")).isTrue();
+  }
+
+  @Test
   public void validateTopLevelDomains() throws Exception {
     try (InputStream in =
         this.getClass().getResourceAsStream("tlds-alpha-by-domain.txt")) {
@@ -35,7 +40,6 @@
       }
       BufferedReader r = new BufferedReader(new InputStreamReader(in));
       String tld;
-      EmailValidator validator = EmailValidator.getInstance();
       while ((tld = r.readLine()) != null) {
         if (tld.startsWith("# ") || tld.startsWith("XN--")) {
           // Ignore comments and non-latin domains
@@ -46,13 +50,13 @@
               + tld.toLowerCase().substring(UNSUPPORTED_PREFIX.length());
           assert_()
             .withFailureMessage("expected invalid TLD \"" + test + "\"")
-            .that(validator.isValid(test))
+            .that(OutgoingEmailValidator.isValid(test))
             .isFalse();
         } else {
           String test = "test@example." + tld.toLowerCase();
           assert_()
             .withFailureMessage("failed to validate TLD \"" + test + "\"")
-            .that(validator.isValid(test))
+            .that(OutgoingEmailValidator.isValid(test))
             .isTrue();
         }
       }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index bcfef5f..0173b05 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -50,6 +50,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
+import com.google.gerrit.server.util.RequestId;
 import com.google.gerrit.testutil.TestChanges;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -481,10 +482,11 @@
   @Test
   public void submitRecords() throws Exception {
     Change c = newChange();
+    RequestId submissionId = RequestId.forChange(c);
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubjectForCommit("Submit patch set 1");
 
-    update.merge("1-1453387607626-96fabc25", ImmutableList.of(
+    update.merge(submissionId, ImmutableList.of(
         submitRecord("NOT_READY", null,
           submitLabel("Verified", "OK", changeOwner.getAccountId()),
           submitLabel("Code-Review", "NEED", null)),
@@ -505,15 +507,16 @@
           submitLabel("Verified", "OK", changeOwner.getAccountId()),
           submitLabel("Alternative-Code-Review", "NEED", null)));
     assertThat(notes.getChange().getSubmissionId())
-        .isEqualTo("1-1453387607626-96fabc25");
+        .isEqualTo(submissionId.toStringForStorage());
   }
 
   @Test
   public void latestSubmitRecordsOnly() throws Exception {
     Change c = newChange();
+    RequestId submissionId = RequestId.forChange(c);
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubjectForCommit("Submit patch set 1");
-    update.merge("1-1453387607626-96fabc25", ImmutableList.of(
+    update.merge(submissionId, ImmutableList.of(
         submitRecord("OK", null,
           submitLabel("Code-Review", "OK", otherUser.getAccountId()))));
     update.commit();
@@ -521,7 +524,7 @@
     incrementPatchSet(c);
     update = newUpdate(c, changeOwner);
     update.setSubjectForCommit("Submit patch set 2");
-    update.merge("1-1453387901516-5d1e2450", ImmutableList.of(
+    update.merge(submissionId, ImmutableList.of(
         submitRecord("OK", null,
           submitLabel("Code-Review", "OK", changeOwner.getAccountId()))));
     update.commit();
@@ -531,7 +534,7 @@
         submitRecord("OK", null,
           submitLabel("Code-Review", "OK", changeOwner.getAccountId())));
     assertThat(notes.getChange().getSubmissionId())
-        .isEqualTo("1-1453387901516-5d1e2450");
+        .isEqualTo(submissionId.toStringForStorage());
   }
 
   @Test
@@ -754,7 +757,7 @@
 
     // Finish off by merging the change.
     update = newUpdate(c, changeOwner);
-    update.merge("1-1453387607626-96fabc25", ImmutableList.of(
+    update.merge(RequestId.forChange(c), ImmutableList.of(
         submitRecord("NOT_READY", null,
           submitLabel("Verified", "OK", changeOwner.getAccountId()),
           submitLabel("Alternative-Code-Review", "NEED", null))));
@@ -1008,10 +1011,12 @@
     ChangeUpdate update2 = newUpdate(c, otherUser);
     update2.putApproval("Code-Review", (short) 2);
 
-    NoteDbUpdateManager updateManager = updateManagerFactory.create(project);
-    updateManager.add(update1);
-    updateManager.add(update2);
-    updateManager.execute();
+    try (NoteDbUpdateManager updateManager =
+        updateManagerFactory.create(project)) {
+      updateManager.add(update1);
+      updateManager.add(update2);
+      updateManager.execute();
+    }
 
     ChangeNotes notes = newNotes(c);
     List<PatchSetApproval> psas =
@@ -1038,20 +1043,22 @@
     CommentRange range1 = new CommentRange(1, 1, 2, 1);
     Timestamp time1 = TimeUtil.nowTs();
     PatchSet.Id psId = c.currentPatchSetId();
-    NoteDbUpdateManager updateManager = updateManagerFactory.create(project);
-    PatchLineComment comment1 = newPublishedComment(psId, "file1",
-        uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
-        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
-    update1.setPatchSetId(psId);
-    update1.putComment(comment1);
-    updateManager.add(update1);
-
-    ChangeUpdate update2 = newUpdate(c, otherUser);
-    update2.putApproval("Code-Review", (short) 2);
-    updateManager.add(update2);
-
     RevCommit tipCommit;
-    updateManager.execute();
+    try (NoteDbUpdateManager updateManager =
+        updateManagerFactory.create(project)) {
+      PatchLineComment comment1 = newPublishedComment(psId, "file1",
+          uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
+          (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+      update1.setPatchSetId(psId);
+      update1.putComment(comment1);
+      updateManager.add(update1);
+
+      ChangeUpdate update2 = newUpdate(c, otherUser);
+      update2.putApproval("Code-Review", (short) 2);
+      updateManager.add(update2);
+
+      updateManager.execute();
+    }
 
     ChangeNotes notes = newNotes(c);
     ObjectId tip = notes.getRevision();
@@ -1094,10 +1101,12 @@
     Ref initial2 = repo.exactRef(update2.getRefName());
     assertThat(initial2).isNotNull();
 
-    NoteDbUpdateManager updateManager = updateManagerFactory.create(project);
-    updateManager.add(update1);
-    updateManager.add(update2);
-    updateManager.execute();
+    try (NoteDbUpdateManager updateManager =
+        updateManagerFactory.create(project)) {
+      updateManager.add(update1);
+      updateManager.add(update2);
+      updateManager.execute();
+    }
 
     Ref ref1 = repo.exactRef(update1.getRefName());
     assertThat(ref1.getObjectId()).isEqualTo(update1.getResult());
@@ -2222,9 +2231,11 @@
         newUpdate(c, otherUser).createDraftUpdateIfNull();
     comment2.setStatus(Status.DRAFT);
     draftUpdate.putComment(comment2);
-    NoteDbUpdateManager manager = updateManagerFactory.create(c.getProject());
-    manager.add(draftUpdate);
-    manager.execute();
+    try (NoteDbUpdateManager manager =
+        updateManagerFactory.create(c.getProject())) {
+      manager.add(draftUpdate);
+      manager.execute();
+    }
 
     // Looking at drafts directly shows the zombie comment.
     DraftCommentNotes draftNotes = draftNotesFactory.create(c, otherUserId);
@@ -2270,10 +2281,11 @@
         Status.PUBLISHED);
     update2.putComment(comment2);
 
-    NoteDbUpdateManager manager = updateManagerFactory.create(project);
-    manager.add(update1);
-    manager.add(update2);
-    manager.execute();
+    try (NoteDbUpdateManager manager = updateManagerFactory.create(project)) {
+      manager.add(update1);
+      manager.add(update2);
+      manager.execute();
+    }
 
     ChangeNotes notes = newNotes(c);
     List<PatchLineComment> comments = notes.getComments().get(new RevId(rev));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 46d8818..bf5abba 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.util.RequestId;
 import com.google.gerrit.testutil.TestChanges;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -138,7 +139,8 @@
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubjectForCommit("Submit patch set 1");
 
-    update.merge("1-1453387607626-96fabc25", ImmutableList.of(
+    RequestId submissionId = RequestId.forChange(c);
+    update.merge(submissionId, ImmutableList.of(
         submitRecord("NOT_READY", null,
           submitLabel("Verified", "OK", changeOwner.getAccountId()),
           submitLabel("Code-Review", "NEED", null)),
@@ -152,7 +154,7 @@
         + "\n"
         + "Patch-set: 1\n"
         + "Status: merged\n"
-        + "Submission-id: 1-1453387607626-96fabc25\n"
+        + "Submission-id: " + submissionId.toStringForStorage() + "\n"
         + "Submitted-with: NOT_READY\n"
         + "Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
         + "Submitted-with: NEED: Code-Review\n"
@@ -204,7 +206,8 @@
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubjectForCommit("Submit patch set 1");
 
-    update.merge("1-1453387607626-96fabc25", ImmutableList.of(
+    RequestId submissionId = RequestId.forChange(c);
+    update.merge(submissionId, ImmutableList.of(
         submitRecord("RULE_ERROR", "Problem with patch set:\n1")));
     update.commit();
 
@@ -212,7 +215,7 @@
         + "\n"
         + "Patch-set: 1\n"
         + "Status: merged\n"
-        + "Submission-id: 1-1453387607626-96fabc25\n"
+        + "Submission-id: " + submissionId.toStringForStorage() + "\n"
         + "Submitted-with: RULE_ERROR Problem with patch set: 1\n",
         update.getResult());
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java
index 9c265a8..cab6549 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -77,7 +77,7 @@
       RepoSequence s = newSequence(name, 1, batchSize);
       for (int i = 1; i <= max; i++) {
         try {
-          assertThat(s.next()).named("next for " + name).isEqualTo(i);
+          assertThat(s.next()).named("i=" + i + " for " + name).isEqualTo(i);
         } catch (OrmException e) {
           throw new AssertionError(
               "failed batchSize=" + batchSize + ", i=" + i, e);
@@ -90,6 +90,36 @@
   }
 
   @Test
+  public void oneCallerNoLoop() throws Exception {
+    RepoSequence s = newSequence("id", 1, 3);
+    assertThat(s.acquireCount).isEqualTo(0);
+
+    assertThat(s.next()).isEqualTo(1);
+    assertThat(s.acquireCount).isEqualTo(1);
+    assertThat(s.next()).isEqualTo(2);
+    assertThat(s.acquireCount).isEqualTo(1);
+    assertThat(s.next()).isEqualTo(3);
+    assertThat(s.acquireCount).isEqualTo(1);
+
+    assertThat(s.next()).isEqualTo(4);
+    assertThat(s.acquireCount).isEqualTo(2);
+    assertThat(s.next()).isEqualTo(5);
+    assertThat(s.acquireCount).isEqualTo(2);
+    assertThat(s.next()).isEqualTo(6);
+    assertThat(s.acquireCount).isEqualTo(2);
+
+    assertThat(s.next()).isEqualTo(7);
+    assertThat(s.acquireCount).isEqualTo(3);
+    assertThat(s.next()).isEqualTo(8);
+    assertThat(s.acquireCount).isEqualTo(3);
+    assertThat(s.next()).isEqualTo(9);
+    assertThat(s.acquireCount).isEqualTo(3);
+
+    assertThat(s.next()).isEqualTo(10);
+    assertThat(s.acquireCount).isEqualTo(4);
+  }
+
+  @Test
   public void twoCallers() throws Exception {
     RepoSequence s1 = newSequence("id", 1, 3);
     RepoSequence s2 = newSequence("id", 1, 3);
@@ -193,15 +223,72 @@
     s.next();
   }
 
+  @Test
+  public void nextWithCountOneCaller() throws Exception {
+    RepoSequence s = newSequence("id", 1, 3);
+    assertThat(s.next(2)).containsExactly(1, 2).inOrder();
+    assertThat(s.acquireCount).isEqualTo(1);
+    assertThat(s.next(2)).containsExactly(3, 4).inOrder();
+    assertThat(s.acquireCount).isEqualTo(2);
+    assertThat(s.next(2)).containsExactly(5, 6).inOrder();
+    assertThat(s.acquireCount).isEqualTo(2);
+
+    assertThat(s.next(3)).containsExactly(7, 8, 9).inOrder();
+    assertThat(s.acquireCount).isEqualTo(3);
+    assertThat(s.next(3)).containsExactly(10, 11, 12).inOrder();
+    assertThat(s.acquireCount).isEqualTo(4);
+    assertThat(s.next(3)).containsExactly(13, 14, 15).inOrder();
+    assertThat(s.acquireCount).isEqualTo(5);
+
+    assertThat(s.next(7)).containsExactly(16, 17, 18, 19, 20, 21, 22).inOrder();
+    assertThat(s.acquireCount).isEqualTo(6);
+    assertThat(s.next(7)).containsExactly(23, 24, 25, 26, 27, 28, 29).inOrder();
+    assertThat(s.acquireCount).isEqualTo(7);
+    assertThat(s.next(7)).containsExactly(30, 31, 32, 33, 34, 35, 36).inOrder();
+    assertThat(s.acquireCount).isEqualTo(8);
+  }
+
+  @Test
+  public void nextWithCountMultipleCallers() throws Exception {
+    RepoSequence s1 = newSequence("id", 1, 3);
+    RepoSequence s2 = newSequence("id", 1, 4);
+
+    assertThat(s1.next(2)).containsExactly(1, 2).inOrder();
+    assertThat(s1.acquireCount).isEqualTo(1);
+
+    // s1 hasn't exhausted its last batch.
+    assertThat(s2.next(2)).containsExactly(4, 5).inOrder();
+    assertThat(s2.acquireCount).isEqualTo(1);
+
+    // s1 acquires again to cover this request, plus a whole new batch.
+    assertThat(s1.next(3)).containsExactly(3, 8, 9);
+    assertThat(s1.acquireCount).isEqualTo(2);
+
+    // s2 hasn't exhausted its last batch, do so now.
+    assertThat(s2.next(2)).containsExactly(6, 7);
+    assertThat(s2.acquireCount).isEqualTo(1);
+  }
+
   private RepoSequence newSequence(String name, int start, int batchSize) {
     return newSequence(
         name, start, batchSize, Runnables.doNothing(), RETRYER);
   }
 
-  private RepoSequence newSequence(String name, int start, int batchSize,
+  private RepoSequence newSequence(String name, final int start, int batchSize,
       Runnable afterReadRef, Retryer<RefUpdate.Result> retryer) {
     return new RepoSequence(
-        repoManager, project, name, start, batchSize, afterReadRef, retryer);
+        repoManager,
+        project,
+        name,
+        new RepoSequence.Seed() {
+          @Override
+          public int get() {
+            return start;
+          }
+        },
+        batchSize,
+        afterReadRef,
+        retryer);
   }
 
   private ObjectId writeBlob(String sequenceName, String value) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
index e3c382a..79983f9 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
@@ -16,19 +16,23 @@
 
 import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.allow;
-import static com.google.gerrit.server.project.Util.deny;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
@@ -45,6 +49,7 @@
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.After;
@@ -60,12 +65,17 @@
   @Inject private ProjectControl.GenericFactory projectControlFactory;
   @Inject private SchemaCreator schemaCreator;
   @Inject private ThreadLocalRequestContext requestContext;
+  @Inject protected ProjectCache projectCache;
+  @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
+  @Inject protected AllProjectsName allProjects;
+  @Inject protected GroupCache groupCache;
 
   private LifecycleManager lifecycle;
   private ReviewDb db;
   private TestRepository<InMemoryRepository> repo;
   private ProjectConfig project;
   private IdentifiedUser user;
+  private AccountGroup.UUID admins;
 
   @Before
   public void setUp() throws Exception {
@@ -77,6 +87,13 @@
 
     db = schemaFactory.open();
     schemaCreator.create(db);
+    // Need to create at least one user to be admin before creating a "normal"
+    // registered user.
+    // See AccountManager#create().
+    accountManager.authenticate(AuthRequest.forUser("admin")).getAccountId();
+    admins = groupCache.get(new AccountGroup.NameKey("Administrators")).getGroupUUID();
+    setUpPermissions();
+
     Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user"))
         .getAccountId();
     user = userFactory.create(userId);
@@ -121,7 +138,25 @@
     ObjectId id = repo.branch("master").commit().create();
     ProjectControl pc = newProjectControl();
     RevWalk rw = repo.getRevWalk();
-    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(id)));
+    Repository r = repo.getRepository();
+
+    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(id)));
+  }
+
+  @Test
+  public void canReadCommitIfTwoRefsVisible() throws Exception {
+    allow(project, READ, REGISTERED_USERS, "refs/heads/branch1");
+    allow(project, READ, REGISTERED_USERS, "refs/heads/branch2");
+
+    ObjectId id1 = repo.branch("branch1").commit().create();
+    ObjectId id2 = repo.branch("branch2").commit().create();
+
+    ProjectControl pc = newProjectControl();
+    RevWalk rw = repo.getRevWalk();
+    Repository r = repo.getRepository();
+
+    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(id1)));
+    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(id2)));
   }
 
   @Test
@@ -134,8 +169,10 @@
 
     ProjectControl pc = newProjectControl();
     RevWalk rw = repo.getRevWalk();
-    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(id1)));
-    assertFalse(pc.canReadCommit(db, rw, rw.parseCommit(id2)));
+    Repository r = repo.getRepository();
+
+    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(id1)));
+    assertFalse(pc.canReadCommit(db, r, rw.parseCommit(id2)));
   }
 
   @Test
@@ -151,8 +188,9 @@
 
     ProjectControl pc = newProjectControl();
     RevWalk rw = repo.getRevWalk();
-    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(parent1)));
-    assertFalse(pc.canReadCommit(db, rw, rw.parseCommit(parent2)));
+    Repository r = repo.getRepository();
+    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(parent1)));
+    assertFalse(pc.canReadCommit(db, r, rw.parseCommit(parent2)));
   }
 
   @Test
@@ -164,12 +202,14 @@
 
     ProjectControl pc = newProjectControl();
     RevWalk rw = repo.getRevWalk();
-    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(parent1)));
-    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(id1)));
+    Repository r = repo.getRepository();
+
+    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(parent1)));
+    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(id1)));
 
     repo.branch("branch1").update(parent1);
-    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(parent1)));
-    assertFalse(pc.canReadCommit(db, rw, rw.parseCommit(id1)));
+    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(parent1)));
+    assertFalse(pc.canReadCommit(db, r, rw.parseCommit(id1)));
   }
 
   @Test
@@ -181,15 +221,49 @@
 
     ProjectControl pc = newProjectControl();
     RevWalk rw = repo.getRevWalk();
-    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(parent1)));
-    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(id1)));
+    Repository r = repo.getRepository();
+
+    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(parent1)));
+    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(id1)));
 
     repo.branch("branch1").update(parent1);
-    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(parent1)));
-    assertFalse(pc.canReadCommit(db, rw, rw.parseCommit(id1)));
+    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(parent1)));
+    assertFalse(pc.canReadCommit(db, r, rw.parseCommit(id1)));
   }
 
   private ProjectControl newProjectControl() throws Exception {
     return projectControlFactory.controlFor(project.getName(), user);
   }
+
+  protected void allow(ProjectConfig project, String permission,
+      AccountGroup.UUID id, String ref)
+      throws Exception {
+    Util.allow(project, permission, id, ref);
+    saveProjectConfig(project);
+  }
+
+  protected void deny(ProjectConfig project, String permission,
+      AccountGroup.UUID id, String ref)
+      throws Exception {
+    Util.deny(project, permission, id, ref);
+    saveProjectConfig(project);
+  }
+
+  protected void saveProjectConfig(ProjectConfig cfg) throws Exception {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(cfg.getName())) {
+      cfg.commit(md);
+    }
+    projectCache.evict(cfg.getProject());
+  }
+
+  private void setUpPermissions() throws Exception {
+    // Remove read permissions for all users besides admin, because by default
+    // Anonymous user group has ALLOW READ permission in refs/*.
+    // This method is idempotent, so is safe to call on every test setup.
+    ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig();
+    for (AccessSection sec : pc.getAccessSections()) {
+      sec.removePermission(Permission.READ);
+    }
+    allow(pc, Permission.READ, admins, "refs/*");
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index e6d488d..d4d77bd 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -47,6 +47,7 @@
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
@@ -235,6 +236,7 @@
   private ChangeControl.Factory changeControlFactory;
   private ReviewDb db;
 
+  @Inject private CapabilityCollection.Factory capabilityCollectionFactory;
   @Inject private CapabilityControl.Factory capabilityControlFactory;
   @Inject private SchemaCreator schemaCreator;
   @Inject private InMemoryDatabase schemaFactory;
@@ -243,18 +245,6 @@
   @Before
   public void setUp() throws Exception {
     repoManager = new InMemoryRepositoryManager();
-    try {
-      Repository repo = repoManager.createRepository(allProjectsName);
-      ProjectConfig allProjects =
-          new ProjectConfig(new Project.NameKey(allProjectsName.get()));
-      allProjects.load(repo);
-      LabelType cr = Util.codeReview();
-      allProjects.getLabelSections().put(cr.getName(), cr);
-      add(allProjects);
-    } catch (IOException | ConfigInvalidException e) {
-      throw new RuntimeException(e);
-    }
-
     projectCache = new ProjectCache() {
       @Override
       public ProjectState getAllProjects() {
@@ -312,6 +302,18 @@
     Injector injector = Guice.createInjector(new InMemoryModule());
     injector.injectMembers(this);
 
+    try {
+      Repository repo = repoManager.createRepository(allProjectsName);
+      ProjectConfig allProjects =
+          new ProjectConfig(new Project.NameKey(allProjectsName.get()));
+      allProjects.load(repo);
+      LabelType cr = Util.codeReview();
+      allProjects.getLabelSections().put(cr.getName(), cr);
+      add(allProjects);
+    } catch (IOException | ConfigInvalidException e) {
+      throw new RuntimeException(e);
+    }
+
     db = schemaFactory.open();
     schemaCreator.create(db);
 
@@ -865,7 +867,7 @@
     all.put(pc.getName(),
         new ProjectState(sitePaths, projectCache, allProjectsName, allUsersName,
             projectControlFactory, envFactory, repoManager, rulesCache,
-            commentLinks, pc));
+            commentLinks, capabilityCollectionFactory, pc));
     return repo;
   }
 
@@ -880,7 +882,7 @@
 
     return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
         Collections.<AccountGroup.UUID> emptySet(), projectCache,
-        sectionSorter, repoManager, null, changeControlFactory, null, null,
+        sectionSorter, null, changeControlFactory, null, null,
         canonicalWebUrl, new MockUser(name, memberOf), newProjectState(local));
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 355edaf..f7b3b11 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
@@ -83,6 +84,9 @@
   protected IdentifiedUser.GenericFactory userFactory;
 
   @Inject
+  private Provider<AnonymousUser> anonymousUser;
+
+  @Inject
   protected InMemoryDatabase schemaFactory;
 
   @Inject
@@ -134,6 +138,20 @@
     };
   }
 
+  protected void setAnonymous() {
+    requestContext.setContext(new RequestContext() {
+      @Override
+      public CurrentUser getUser() {
+        return anonymousUser.get();
+      }
+
+      @Override
+      public Provider<ReviewDb> getReviewDbProvider() {
+        return Providers.of(db);
+      }
+    });
+  }
+
   @After
   public void tearDownInjector() {
     if (lifecycle != null) {
@@ -225,6 +243,7 @@
   public void byName() throws Exception {
     AccountInfo user1 = newAccountWithFullName("jdoe", "John Doe");
     AccountInfo user2 = newAccountWithFullName("jroe", "Jane Roe");
+    AccountInfo user3 = newAccountWithFullName("user3", "Mr Selfish");
 
     assertQuery("notexisting");
     assertQuery("Not Existing");
@@ -236,11 +255,15 @@
     assertQuery("Doe", user1);
     assertQuery("doe", user1);
     assertQuery("DOE", user1);
+    assertQuery("Jo Do", user1);
+    assertQuery("jo do", user1);
+    assertQuery("self", currentUserInfo, user3);
     assertQuery("name:John", user1);
     assertQuery("name:john", user1);
     assertQuery("name:Doe", user1);
     assertQuery("name:doe", user1);
     assertQuery("name:DOE", user1);
+    assertQuery("name:self", user3);
 
     assertQuery(quote(user2.name), user2);
     assertQuery("name:" + quote(user2.name), user2);
@@ -294,6 +317,44 @@
     assertThat(ai.avatars).isNull();
   }
 
+  @Test
+  public void withSecondaryEmails() throws Exception {
+    AccountInfo user1 =
+        newAccount("myuser", "My User", "my.user@example.com", true);
+    String[] secondaryEmails =
+        new String[] {"bar@example.com", "foo@example.com"};
+    addEmails(user1, secondaryEmails);
+
+
+    List<AccountInfo> result = assertQuery(user1.username, user1);
+    assertThat(result.get(0).secondaryEmails).isNull();
+
+    result = assertQuery(
+        newQuery(user1.username).withOption(ListAccountsOption.DETAILS), user1);
+    assertThat(result.get(0).secondaryEmails).isNull();
+
+    result = assertQuery(
+        newQuery(user1.username).withOption(ListAccountsOption.ALL_EMAILS),
+        user1);
+    assertThat(result.get(0).secondaryEmails)
+        .containsExactlyElementsIn(Arrays.asList(secondaryEmails)).inOrder();
+
+    result = assertQuery(newQuery(user1.username).withOptions(
+        ListAccountsOption.DETAILS, ListAccountsOption.ALL_EMAILS), user1);
+    assertThat(result.get(0).secondaryEmails)
+        .containsExactlyElementsIn(Arrays.asList(secondaryEmails)).inOrder();
+  }
+
+  @Test
+  public void asAnonymous() throws Exception {
+    AccountInfo user1 = newAccount("user1");
+
+    setAnonymous();
+    assertQuery("9999999");
+    assertQuery("self");
+    assertQuery("username:" + user1.username, user1);
+  }
+
   protected AccountInfo newAccount(String username) throws Exception {
     return newAccountWithEmail(username, null);
   }
@@ -359,6 +420,15 @@
     return id;
   }
 
+  private void addEmails(AccountInfo account, String... emails)
+      throws Exception {
+    Account.Id id = new Account.Id(account._accountId);
+    for (String email : emails) {
+      accountManager.link(id, AuthRequest.forEmail(email));
+    }
+    accountCache.evict(id);
+  }
+
   protected QueryRequest newQuery(Object query) throws RestApiException {
     return gApi.accounts().query(query.toString());
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 5a53d65..0c658bf 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -1584,7 +1584,7 @@
     PatchSetInserter inserter = patchSetFactory.create(
           ctl, new PatchSet.Id(c.getId(), n), commit)
         .setSendMail(false)
-        .setRunHooks(false)
+        .setFireRevisionCreated(false)
         .setValidatePolicy(CommitValidators.Policy.NONE);
     try (BatchUpdate bu = updateFactory.create(
         db, c.getProject(), user, TimeUtil.nowTs());
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 d9841a3..07cd63e 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
@@ -19,11 +19,14 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 
 /** Fake implementation of {@link AccountCache} for testing. */
 public class FakeAccountCache implements AccountCache {
@@ -64,6 +67,12 @@
     byUsername.remove(username);
   }
 
+  @Override
+  public synchronized void evictAll() {
+    byId.clear();
+    byUsername.clear();
+  }
+
   public synchronized void put(Account account) {
     AccountState state = newState(account);
     byId.put(account.getId(), state);
@@ -73,8 +82,8 @@
   }
 
   private static AccountState newState(Account account) {
-    return new AccountState(
-        account, ImmutableSet.<AccountGroup.UUID> of(),
-        ImmutableSet.<AccountExternalId> of());
+    return new AccountState(account, ImmutableSet.<AccountGroup.UUID> of(),
+        ImmutableSet.<AccountExternalId> of(),
+        new HashMap<ProjectWatchKey, Set<NotifyType>>());
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index 9f1f031..5fb930c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -18,13 +18,11 @@
 
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.DisabledChangeHooks;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
@@ -180,7 +178,6 @@
 
     bind(SecureStore.class).to(DefaultSecureStore.class);
 
-    bind(ChangeHooks.class).to(DisabledChangeHooks.class);
     install(NoSshKeyCache.module());
     install(new CanonicalWebUrlModule() {
       @Override
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
index 3208d00..f98d63f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
@@ -83,7 +83,7 @@
       }
     } catch (RepositoryNotFoundException e) {
       repo = new Repo(name);
-      repos.put(name.get().toLowerCase(), repo);
+      repos.put(normalize(name), repo);
     }
     return repo;
   }
@@ -113,12 +113,20 @@
     }
   }
 
+  public synchronized void deleteRepository(Project.NameKey name) {
+    repos.remove(normalize(name));
+  }
+
   private synchronized Repo get(Project.NameKey name)
       throws RepositoryNotFoundException {
-    Repo repo = repos.get(name.get().toLowerCase());
+    Repo repo = repos.get(normalize(name));
     if (repo != null) {
       return repo;
     }
     throw new RepositoryNotFoundException(name.get());
   }
+
+  private static String normalize(Project.NameKey name) {
+    return name.get().toLowerCase();
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
index ce2fe8f..2f9d67f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
@@ -29,6 +29,13 @@
     return readChanges;
   }
 
+  @Override
+  public boolean readChangeSequence() {
+    // Unlike ConfigNotesMigration, read change numbers from NoteDb by default
+    // when reads are enabled, to improve test coverage.
+    return readChanges;
+  }
+
   // Increase visbility from superclass, as tests may want to check whether
   // NoteDb data is written in specific migration scenarios.
   @Override
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
index fde3a66..4ddca0c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -97,7 +97,7 @@
       try {
         cmd.destroy();
       } catch (Exception e) {
-        Throwables.propagateIfPossible(e);
+        Throwables.throwIfUnchecked(e);
         throw new RuntimeException(e);
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index f2911dc..f3243c6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -136,7 +136,7 @@
       try {
         cmd.destroy();
       } catch (Exception e) {
-        Throwables.propagateIfPossible(e);
+        Throwables.throwIfUnchecked(e);
         throw new RuntimeException(e);
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index 57ef530..466edf5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -330,6 +330,7 @@
       if (getSessionFactory() == null) {
         setSessionFactory(createSessionFactory());
       }
+      setupSessionTimeout(getSessionFactory());
       daemonAcceptor = createAcceptor();
 
       try {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index 24bd8c2..c88a02c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -153,7 +153,7 @@
       try {
         cmd.destroy();
       } catch (Exception e) {
-        Throwables.propagateIfPossible(e);
+        Throwables.throwIfUnchecked(e);
         throw new RuntimeException(e);
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index a84fe04..d3ff06f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -67,7 +68,7 @@
   @Override
   protected void run() throws OrmException, IOException, ConfigInvalidException,
       UnloggedFailure {
-    CreateAccount.Input input = new CreateAccount.Input();
+    AccountInput input = new AccountInput();
     input.username = username;
     input.email = email;
     input.name = fullName;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
index 73e9f33..6629e3c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
@@ -23,11 +23,15 @@
 import com.google.inject.Inject;
 
 import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(name = "start", description = "Start the online reindexer")
 public class IndexStartCommand extends SshCommand {
 
+  @Option(name = "--force", usage = "force a re-index")
+  private boolean force;
+
   @Argument(index = 0, required = true, metaVar = "INDEX",
       usage = "index name to start")
   private String name;
@@ -38,7 +42,7 @@
   @Override
   protected void run() throws UnloggedFailure {
     try {
-      if (luceneVersionManager.startReindexer(name)) {
+      if (luceneVersionManager.startReindexer(name, force)) {
         stdout.println("Reindexer started");
       } else {
         stdout.println("Nothing to reindex, index is already the latest version");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index 63bcaad..2173652 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -85,7 +85,7 @@
   protected void run() throws Failure {
     Account userAccount;
     try {
-      userAccount = accountResolver.find(userName);
+      userAccount = accountResolver.find(db, userName);
     } catch (OrmException e) {
       throw die(e);
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
index 4c930c8..25e49b2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
 import com.google.gwtorm.jdbc.JdbcSchema;
@@ -78,7 +79,7 @@
 
   public void run() {
     try {
-      db = dbFactory.open();
+      db = ReviewDbUtil.unwrapDb(dbFactory.open());
       try {
         connection = ((JdbcSchema) db).getConnection();
         connection.setAutoCommit(true);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 3c5d5a3..45bf649 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -23,9 +23,9 @@
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.PatchSet;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
index d255e84..5e4568f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
 import com.google.gerrit.common.TimeUtil;
@@ -107,32 +108,41 @@
 
     hostNameWidth = wide ? Integer.MAX_VALUE : columns - 9 - 9 - 10 - 32;
 
-    final long now = TimeUtil.nowMs();
-    stdout.print(String.format("%-8s %8s %8s   %-15s %s\n", //
-        "Session", "Start", "Idle", "User", "Remote Host"));
-    stdout.print("--------------------------------------------------------------\n");
-    for (final IoSession io : list) {
-      AbstractSession s = AbstractSession.getSession(io, true);
-      SshSession sd = s != null ? s.getAttribute(SshSession.KEY) : null;
+    if (getBackend().equals("mina")) {
+      long now = TimeUtil.nowMs();
+      stdout.print(String.format("%-8s %8s %8s   %-15s %s\n",
+          "Session", "Start", "Idle", "User", "Remote Host"));
+      stdout.print("--------------------------------------------------------------\n");
+      for (final IoSession io : list) {
+        checkState(io instanceof MinaSession, "expected MinaSession");
+        MinaSession minaSession = (MinaSession) io;
+        long start = minaSession.getSession().getCreationTime();
+        long idle = now - minaSession.getSession().getLastIoTime();
+        AbstractSession s = AbstractSession.getSession(io, true);
+        SshSession sd = s != null ? s.getAttribute(SshSession.KEY) : null;
 
-      final SocketAddress remoteAddress = io.getRemoteAddress();
-      MinaSession minaSession = io instanceof MinaSession
-          ? (MinaSession) io
-          : null;
-      final long start = minaSession == null
-          ? 0
-          : minaSession.getSession().getCreationTime();
-      final long idle = minaSession == null
-          ? now
-          : now - minaSession.getSession().getLastIoTime();
+        stdout.print(String.format("%8s %8s %8s   %-15.15s %s\n",
+            id(sd),
+            time(now, start),
+            age(idle),
+            username(sd),
+            hostname(io.getRemoteAddress())));
+      }
+    } else {
+      stdout.print(String.format("%-8s   %-15s %s\n",
+          "Session", "User", "Remote Host"));
+      stdout.print("--------------------------------------------------------------\n");
+      for (final IoSession io : list) {
+        AbstractSession s = AbstractSession.getSession(io, true);
+        SshSession sd = s != null ? s.getAttribute(SshSession.KEY) : null;
 
-      stdout.print(String.format("%8s %8s %8s   %-15.15s %s\n", //
-          id(sd), //
-          time(now, start), //
-          age(idle), //
-          username(sd), //
-          hostname(remoteAddress)));
+        stdout.print(String.format("%8s   %-15.15s %s\n",
+            id(sd),
+            username(sd),
+            hostname(io.getRemoteAddress())));
+      }
     }
+
     stdout.print("--\n");
     stdout.print("SSHD Backend: " + getBackend() + "\n");
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 34f281e..9a20ba8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -17,12 +17,15 @@
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.ListTasks;
 import com.google.gerrit.server.config.ListTasks.TaskInfo;
+import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.Task;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -39,18 +42,27 @@
 
 /** Display the current work queue. */
 @AdminHighPriorityCommand
-@CommandMetaData(name = "show-queue", description = "Display the background work queues",
-  runsAt = MASTER_OR_SLAVE)
+@CommandMetaData(name = "show-queue",
+    description = "Display the background work queues",
+    runsAt = MASTER_OR_SLAVE)
 final class ShowQueue extends SshCommand {
-  @Option(name = "--wide", aliases = {"-w"}, usage = "display without line width truncation")
+  @Option(name = "--wide", aliases = {"-w"},
+      usage = "display without line width truncation")
   private boolean wide;
 
+  @Option(name = "--by-queue", aliases = {"-q"},
+      usage = "group tasks by queue and print queue info")
+  private boolean groupByQueue;
+
   @Inject
   private ListTasks listTasks;
 
   @Inject
   private IdentifiedUser currentUser;
 
+  @Inject
+  private WorkQueue workQueue;
+
   private int columns = 80;
   private int maxCommandWidth;
 
@@ -75,50 +87,78 @@
     stdout.print("----------------------------------------------"
         + "--------------------------------\n");
 
+    List<TaskInfo> tasks;
     try {
-      List<TaskInfo> tasks = listTasks.apply(new ConfigResource());
-      long now = TimeUtil.nowMs();
-      boolean viewAll = currentUser.getCapabilities().canViewQueue();
-      for (TaskInfo task : tasks) {
-        String start;
-        switch (task.state) {
-          case DONE:
-          case CANCELLED:
-          case RUNNING:
-          case READY:
-            start = format(task.state);
-            break;
-          case OTHER:
-          case SLEEPING:
-          default:
-            start = time(now, task.delay);
-            break;
-        }
-
-        // Shows information about tasks depending on the user rights
-        if (viewAll || task.projectName == null) {
-          String command = task.command.length() < maxCommandWidth
-              ? task.command
-              : task.command.substring(0, maxCommandWidth);
-
-          stdout.print(String.format("%8s %-12s %-12s %-4s %s\n",
-              task.id, start, startTime(task.startTime), "", command));
-        } else {
-          String remoteName = task.remoteName != null
-              ? task.remoteName + "/" + task.projectName
-              : task.projectName;
-
-          stdout.print(String.format("%8s %-12s %-4s %s\n",
-              task.id, start, startTime(task.startTime),
-              MoreObjects.firstNonNull(remoteName, "n/a")));
-        }
-      }
-      stdout.print("----------------------------------------------"
-          + "--------------------------------\n");
-      stdout.print("  " + tasks.size() + " tasks\n");
+      tasks = listTasks.apply(new ConfigResource());
     } catch (AuthException e) {
       throw die(e);
     }
+    boolean viewAll = currentUser.getCapabilities().canViewQueue();
+    long now = TimeUtil.nowMs();
+
+    if (groupByQueue) {
+      ListMultimap<String, TaskInfo> byQueue = byQueue(tasks);
+      for (String queueName : byQueue.keySet()) {
+        WorkQueue.Executor e = workQueue.getExecutor(queueName);
+        stdout.print(String.format("Queue: %s\n", queueName));
+        print(byQueue.get(queueName), now, viewAll, e.getCorePoolSize());
+      }
+    } else {
+      print(tasks, now, viewAll, 0);
+    }
+  }
+
+  private ListMultimap<String, TaskInfo> byQueue(List<TaskInfo> tasks) {
+    ListMultimap<String, TaskInfo> byQueue = LinkedListMultimap.create();
+    for (TaskInfo task : tasks) {
+      byQueue.put(task.queueName, task);
+    }
+    return byQueue;
+  }
+
+  private void print(List<TaskInfo> tasks, long now, boolean viewAll,
+      int threadPoolSize) {
+    for (TaskInfo task : tasks) {
+      String start;
+      switch (task.state) {
+        case DONE:
+        case CANCELLED:
+        case RUNNING:
+        case READY:
+          start = format(task.state);
+          break;
+        case OTHER:
+        case SLEEPING:
+        default:
+          start = time(now, task.delay);
+          break;
+      }
+
+      // Shows information about tasks depending on the user rights
+      if (viewAll || task.projectName == null) {
+        String command = task.command.length() < maxCommandWidth
+            ? task.command
+                : task.command.substring(0, maxCommandWidth);
+
+        stdout.print(String.format("%8s %-12s %-12s %-4s %s\n",
+            task.id, start, startTime(task.startTime), "", command));
+      } else {
+        String remoteName = task.remoteName != null
+            ? task.remoteName + "/" + task.projectName
+                : task.projectName;
+
+        stdout.print(String.format("%8s %-12s %-4s %s\n",
+            task.id, start, startTime(task.startTime),
+            MoreObjects.firstNonNull(remoteName, "n/a")));
+      }
+    }
+    stdout.print("----------------------------------------------"
+        + "--------------------------------\n");
+    stdout.print("  " + tasks.size() + " tasks");
+    if (threadPoolSize > 0) {
+      stdout.print(", " + threadPoolSize + " worker threads");
+    }
+    stdout.print("\n\n");
   }
 
   private static String time(long now, long delay) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index 9e6630a..0cfe4a7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -22,10 +22,12 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.EventTypes;
+import com.google.gerrit.server.events.ProjectNameKeySerializer;
 import com.google.gerrit.server.events.SupplierSerializer;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
@@ -160,6 +162,8 @@
 
     gson = new GsonBuilder()
         .registerTypeAdapter(Supplier.class, new SupplierSerializer())
+        .registerTypeAdapter(
+            Project.NameKey.class, new ProjectNameKeySerializer())
         .create();
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
index 644ff6e..181b0c6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
-import com.google.gerrit.server.git.UploadPackMetricsHook;
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.server.git.validators.UploadValidationException;
 import com.google.gerrit.server.git.validators.UploadValidators;
@@ -30,6 +29,8 @@
 import com.google.gerrit.sshd.SshSession;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.transport.PostUploadHook;
+import org.eclipse.jgit.transport.PostUploadHookChain;
 import org.eclipse.jgit.transport.PreUploadHook;
 import org.eclipse.jgit.transport.PreUploadHookChain;
 import org.eclipse.jgit.transport.UploadPack;
@@ -59,14 +60,14 @@
   private DynamicSet<PreUploadHook> preUploadHooks;
 
   @Inject
+  private DynamicSet<PostUploadHook> postUploadHooks;
+
+  @Inject
   private UploadValidators.Factory uploadValidatorsFactory;
 
   @Inject
   private SshSession session;
 
-  @Inject
-  private UploadPackMetricsHook uploadMetrics;
-
   @Override
   protected void runImpl() throws IOException, Failure {
     if (!projectControl.canRunUploadPack()) {
@@ -80,7 +81,8 @@
             true));
     up.setPackConfig(config.getPackConfig());
     up.setTimeout(config.getTimeout());
-    up.setPostUploadHook(uploadMetrics);
+    up.setPostUploadHook(
+        PostUploadHookChain.newChain(Lists.newArrayList(postUploadHooks)));
 
     List<PreUploadHook> allPreUploadHooks = Lists.newArrayList(preUploadHooks);
     allPreUploadHooks.add(uploadValidatorsFactory.create(project, repo,
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 5e2480e..0edba4f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -217,7 +217,7 @@
   private boolean canRead(ObjectId revId) throws IOException {
     try (RevWalk rw = new RevWalk(repo)) {
       RevCommit commit = rw.parseCommit(revId);
-      return projectControl.canReadCommit(db, rw, commit);
+      return projectControl.canReadCommit(db, repo, commit);
     }
   }
 }
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index f5b078e..acc9b86 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 64dce26..5790453 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -18,10 +18,8 @@
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.common.base.Splitter;
-import com.google.gerrit.common.ChangeHookApiListener;
-import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.common.EventBroker;
-import com.google.gerrit.common.StreamEventsApiListener;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.auth.oauth.OAuthModule;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
@@ -31,7 +29,7 @@
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
-import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.pgm.util.LogFileCompressor;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.change.ChangeCleanupRunner;
@@ -44,6 +42,7 @@
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.RestCacheAdminModule;
 import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.events.StreamEventsApiListener;
 import com.google.gerrit.server.git.GarbageCollectionModule;
 import com.google.gerrit.server.git.GitRepositoryManagerModule;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
@@ -297,12 +296,11 @@
   private Injector createSysInjector() {
     final List<Module> modules = new ArrayList<>();
     modules.add(new DropWizardMetricMaker.RestModule());
+    modules.add(new LogFileCompressor.Module());
     modules.add(new EventBroker.Module());
     modules.add(new H2AccountPatchReviewStore.Module());
     modules.add(cfgInjector.getInstance(GitRepositoryManagerModule.class));
-    modules.add(new ChangeHookApiListener.Module());
     modules.add(new StreamEventsApiListener.Module());
-    modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
     modules.add(new DiffExecutorModule());
     modules.add(new MimeUtil2Module());
diff --git a/lib/BUCK b/lib/BUCK
index 48e2ff4..efdf0eb 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -4,6 +4,7 @@
 define_license(name = 'Apache1.1')
 define_license(name = 'Apache2.0')
 define_license(name = 'args4j')
+define_license(name = 'asciidoctor')
 define_license(name = 'automaton')
 define_license(name = 'bouncycastle')
 define_license(name = 'CC-BY3.0')
@@ -13,6 +14,8 @@
 define_license(name = 'diffy')
 define_license(name = 'fetch')
 define_license(name = 'h2')
+define_license(name = 'highlightjs')
+define_license(name = 'icu4j')
 define_license(name = 'jgit')
 define_license(name = 'jsch')
 define_license(name = 'MPL1.1')
@@ -57,16 +60,17 @@
 
 maven_jar(
   name = 'gson',
-  id = 'com.google.code.gson:gson:2.6.2',
-  sha1 = 'f1bc476cc167b18e66c297df599b2377131a8947',
+  id = 'com.google.code.gson:gson:2.7',
+  sha1 = '751f548c85fa49f330cecbb1875893f971b33c4e',
   license = 'Apache2.0',
 )
 
 maven_jar(
   name = 'guava',
-  id = 'com.google.guava:guava:19.0',
-  sha1 = '6ce200f6b23222af3d8abb6b6459e6c44f4bb0e9',
+  id = 'com.google.guava:guava:20.0:20160818.201422-323',
+  sha1 = '13af7470db1026c57aedd0144018e06fe79bba33',
   license = 'Apache2.0',
+  repository = MAVEN_SNAPSHOT,
 )
 
 maven_jar(
@@ -86,7 +90,11 @@
   # Whitelist lib targets that have jsr305 as a dependency. Generally speaking
   # Gerrit core should not depend on these annotations, and instead use
   # equivalent annotations in com.google.gerrit.common.
-  visibility = ['//lib:guava-retrying'],
+  visibility = [
+    '//gerrit-plugin-api:lib',
+    '//lib:guava-retrying',
+    '//lib:soy',
+  ],
 )
 
 maven_jar(
@@ -269,3 +277,34 @@
   license = 'Apache2.0',
   repository = GERRIT,
 )
+
+# Keep this version of Soy synchronized with the version used in Gitiles.
+maven_jar(
+  name = 'soy',
+  id = 'com.google.template:soy:2016-08-09',
+  sha1 = '43d33651e95480d515fe26c10a662faafe3ad1e4',
+  license = 'Apache2.0',
+  deps = [
+    ':args4j',
+    ':guava',
+    ':gson',
+    ':icu4j',
+    ':jsr305',
+    ':protobuf',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:multibindings',
+    '//lib/guice:javax-inject',
+    '//lib/ow2:ow2-asm',
+    '//lib/ow2:ow2-asm-analysis',
+    '//lib/ow2:ow2-asm-commons',
+    '//lib/ow2:ow2-asm-util',
+  ],
+)
+
+maven_jar(
+  name = 'icu4j',
+  id = 'com.ibm.icu:icu4j:57.1',
+  sha1 = '198ea005f41219f038f4291f0b0e9f3259730e92',
+  license = 'icu4j',
+)
diff --git a/lib/JGIT_VERSION b/lib/JGIT_VERSION
index 6878780..6457323 100644
--- a/lib/JGIT_VERSION
+++ b/lib/JGIT_VERSION
@@ -1,4 +1,4 @@
 include_defs('//lib/maven.defs')
 
 REPO = GERRIT # Leave here even if set to MAVEN_CENTRAL.
-VERS = '4.3.0.201604071810-r.23-gc9b0028'
+VERS = '4.4.1.201607150455-r.118-g1096652'
diff --git a/lib/LICENSE-asciidoctor b/lib/LICENSE-asciidoctor
new file mode 100644
index 0000000..d7e3a20
--- /dev/null
+++ b/lib/LICENSE-asciidoctor
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (C) 2012-2016 Dan Allen, Ryan Waldron and the Asciidoctor Project
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/lib/LICENSE-highlightjs b/lib/LICENSE-highlightjs
new file mode 100644
index 0000000..da266fa
--- /dev/null
+++ b/lib/LICENSE-highlightjs
@@ -0,0 +1,24 @@
+Copyright (c) 2006, Ivan Sagalaev
+All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of highlight.js nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/lib/LICENSE-icu4j b/lib/LICENSE-icu4j
new file mode 100644
index 0000000..90be7cd
--- /dev/null
+++ b/lib/LICENSE-icu4j
@@ -0,0 +1,385 @@
+COPYRIGHT AND PERMISSION NOTICE (ICU 58 and later)
+
+Copyright © 1991-2016 Unicode, Inc. All rights reserved.
+Distributed under the Terms of Use in http://www.unicode.org/copyright.html
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Unicode data files and any associated documentation
+(the "Data Files") or Unicode software and any associated documentation
+(the "Software") to deal in the Data Files or Software
+without restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, and/or sell copies of
+the Data Files or Software, and to permit persons to whom the Data Files
+or Software are furnished to do so, provided that either
+(a) this copyright and permission notice appear with all copies
+of the Data Files or Software, or
+(b) this copyright and permission notice appear in associated
+Documentation.
+
+THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT OF THIRD PARTY RIGHTS.
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS
+NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
+DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
+DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THE DATA FILES OR SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder
+shall not be used in advertising or otherwise to promote the sale,
+use or other dealings in these Data Files or Software without prior
+written authorization of the copyright holder.
+
+---------------------
+
+Third-Party Software Licenses
+
+This section contains third-party software notices and/or additional
+terms for licensed third-party software components included within ICU
+libraries.
+
+1. ICU License - ICU 1.8.1 to ICU 57.1
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright (c) 1995-2016 International Business Machines Corporation and others
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, and/or sell copies of the Software, and to permit persons
+to whom the Software is furnished to do so, provided that the above
+copyright notice(s) and this permission notice appear in all copies of
+the Software and that both the above copyright notice(s) and this
+permission notice appear in supporting documentation.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY
+SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
+RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
+CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder
+shall not be used in advertising or otherwise to promote the sale, use
+or other dealings in this Software without prior written authorization
+of the copyright holder.
+
+All trademarks and registered trademarks mentioned herein are the
+property of their respective owners.
+
+2. Chinese/Japanese Word Break Dictionary Data (cjdict.txt)
+
+ #     The Google Chrome software developed by Google is licensed under
+ # the BSD license. Other software included in this distribution is
+ # provided under other licenses, as set forth below.
+ #
+ #  The BSD License
+ #  http://opensource.org/licenses/bsd-license.php
+ #  Copyright (C) 2006-2008, Google Inc.
+ #
+ #  All rights reserved.
+ #
+ #  Redistribution and use in source and binary forms, with or without
+ # modification, are permitted provided that the following conditions are met:
+ #
+ #  Redistributions of source code must retain the above copyright notice,
+ # this list of conditions and the following disclaimer.
+ #  Redistributions in binary form must reproduce the above
+ # copyright notice, this list of conditions and the following
+ # disclaimer in the documentation and/or other materials provided with
+ # the distribution.
+ #  Neither the name of  Google Inc. nor the names of its
+ # contributors may be used to endorse or promote products derived from
+ # this software without specific prior written permission.
+ #
+ #
+ #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ #
+ #
+ #  The word list in cjdict.txt are generated by combining three word lists
+ # listed below with further processing for compound word breaking. The
+ # frequency is generated with an iterative training against Google web
+ # corpora.
+ #
+ #  * Libtabe (Chinese)
+ #    - https://sourceforge.net/project/?group_id=1519
+ #    - Its license terms and conditions are shown below.
+ #
+ #  * IPADIC (Japanese)
+ #    - http://chasen.aist-nara.ac.jp/chasen/distribution.html
+ #    - Its license terms and conditions are shown below.
+ #
+ #  ---------COPYING.libtabe ---- BEGIN--------------------
+ #
+ #  /*
+ #   * Copyrighy (c) 1999 TaBE Project.
+ #   * Copyright (c) 1999 Pai-Hsiang Hsiao.
+ #   * All rights reserved.
+ #   *
+ #   * Redistribution and use in source and binary forms, with or without
+ #   * modification, are permitted provided that the following conditions
+ #   * are met:
+ #   *
+ #   * . Redistributions of source code must retain the above copyright
+ #   *   notice, this list of conditions and the following disclaimer.
+ #   * . Redistributions in binary form must reproduce the above copyright
+ #   *   notice, this list of conditions and the following disclaimer in
+ #   *   the documentation and/or other materials provided with the
+ #   *   distribution.
+ #   * . Neither the name of the TaBE Project nor the names of its
+ #   *   contributors may be used to endorse or promote products derived
+ #   *   from this software without specific prior written permission.
+ #   *
+ #   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ #   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ #   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ #   * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ #   * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ #   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ #   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ #   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ #   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ #   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ #   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ #   * OF THE POSSIBILITY OF SUCH DAMAGE.
+ #   */
+ #
+ #  /*
+ #   * Copyright (c) 1999 Computer Systems and Communication Lab,
+ #   *                    Institute of Information Science, Academia
+ #       *                    Sinica. All rights reserved.
+ #   *
+ #   * Redistribution and use in source and binary forms, with or without
+ #   * modification, are permitted provided that the following conditions
+ #   * are met:
+ #   *
+ #   * . Redistributions of source code must retain the above copyright
+ #   *   notice, this list of conditions and the following disclaimer.
+ #   * . Redistributions in binary form must reproduce the above copyright
+ #   *   notice, this list of conditions and the following disclaimer in
+ #   *   the documentation and/or other materials provided with the
+ #   *   distribution.
+ #   * . Neither the name of the Computer Systems and Communication Lab
+ #   *   nor the names of its contributors may be used to endorse or
+ #   *   promote products derived from this software without specific
+ #   *   prior written permission.
+ #   *
+ #   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ #   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ #   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ #   * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ #   * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ #   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ #   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ #   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ #   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ #   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ #   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ #   * OF THE POSSIBILITY OF SUCH DAMAGE.
+ #   */
+ #
+ #  Copyright 1996 Chih-Hao Tsai @ Beckman Institute,
+ #      University of Illinois
+ #  c-tsai4@uiuc.edu  http://casper.beckman.uiuc.edu/~c-tsai4
+ #
+ #  ---------------COPYING.libtabe-----END--------------------------------
+ #
+ #
+ #  ---------------COPYING.ipadic-----BEGIN-------------------------------
+ #
+ #  Copyright 2000, 2001, 2002, 2003 Nara Institute of Science
+ #  and Technology.  All Rights Reserved.
+ #
+ #  Use, reproduction, and distribution of this software is permitted.
+ #  Any copy of this software, whether in its original form or modified,
+ #  must include both the above copyright notice and the following
+ #  paragraphs.
+ #
+ #  Nara Institute of Science and Technology (NAIST),
+ #  the copyright holders, disclaims all warranties with regard to this
+ #  software, including all implied warranties of merchantability and
+ #  fitness, in no event shall NAIST be liable for
+ #  any special, indirect or consequential damages or any damages
+ #  whatsoever resulting from loss of use, data or profits, whether in an
+ #  action of contract, negligence or other tortuous action, arising out
+ #  of or in connection with the use or performance of this software.
+ #
+ #  A large portion of the dictionary entries
+ #  originate from ICOT Free Software.  The following conditions for ICOT
+ #  Free Software applies to the current dictionary as well.
+ #
+ #  Each User may also freely distribute the Program, whether in its
+ #  original form or modified, to any third party or parties, PROVIDED
+ #  that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear
+ #  on, or be attached to, the Program, which is distributed substantially
+ #  in the same form as set out herein and that such intended
+ #  distribution, if actually made, will neither violate or otherwise
+ #  contravene any of the laws and regulations of the countries having
+ #  jurisdiction over the User or the intended distribution itself.
+ #
+ #  NO WARRANTY
+ #
+ #  The program was produced on an experimental basis in the course of the
+ #  research and development conducted during the project and is provided
+ #  to users as so produced on an experimental basis.  Accordingly, the
+ #  program is provided without any warranty whatsoever, whether express,
+ #  implied, statutory or otherwise.  The term "warranty" used herein
+ #  includes, but is not limited to, any warranty of the quality,
+ #  performance, merchantability and fitness for a particular purpose of
+ #  the program and the nonexistence of any infringement or violation of
+ #  any right of any third party.
+ #
+ #  Each user of the program will agree and understand, and be deemed to
+ #  have agreed and understood, that there is no warranty whatsoever for
+ #  the program and, accordingly, the entire risk arising from or
+ #  otherwise connected with the program is assumed by the user.
+ #
+ #  Therefore, neither ICOT, the copyright holder, or any other
+ #  organization that participated in or was otherwise related to the
+ #  development of the program and their respective officials, directors,
+ #  officers and other employees shall be held liable for any and all
+ #  damages, including, without limitation, general, special, incidental
+ #  and consequential damages, arising out of or otherwise in connection
+ #  with the use or inability to use the program or any product, material
+ #  or result produced or otherwise obtained by using the program,
+ #  regardless of whether they have been advised of, or otherwise had
+ #  knowledge of, the possibility of such damages at any time during the
+ #  project or thereafter.  Each user will be deemed to have agreed to the
+ #  foregoing by his or her commencement of use of the program.  The term
+ #  "use" as used herein includes, but is not limited to, the use,
+ #  modification, copying and distribution of the program and the
+ #  production of secondary products from the program.
+ #
+ #  In the case where the program, whether in its original form or
+ #  modified, was distributed or delivered to or received by a user from
+ #  any person, organization or entity other than ICOT, unless it makes or
+ #  grants independently of ICOT any specific warranty to the user in
+ #  writing, such person, organization or entity, will also be exempted
+ #  from and not be held liable to the user for any such damages as noted
+ #  above as far as the program is concerned.
+ #
+ #  ---------------COPYING.ipadic-----END----------------------------------
+
+3. Lao Word Break Dictionary Data (laodict.txt)
+
+ #  Copyright (c) 2013 International Business Machines Corporation
+ #  and others. All Rights Reserved.
+ #
+ # Project: http://code.google.com/p/lao-dictionary/
+ # Dictionary: http://lao-dictionary.googlecode.com/git/Lao-Dictionary.txt
+ # License: http://lao-dictionary.googlecode.com/git/Lao-Dictionary-LICENSE.txt
+ #              (copied below)
+ #
+ #  This file is derived from the above dictionary, with slight
+ #  modifications.
+ #  ----------------------------------------------------------------------
+ #  Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell.
+ #  All rights reserved.
+ #
+ #  Redistribution and use in source and binary forms, with or without
+ #  modification,
+ #  are permitted provided that the following conditions are met:
+ #
+ #
+ # Redistributions of source code must retain the above copyright notice, this
+ #  list of conditions and the following disclaimer. Redistributions in
+ #  binary form must reproduce the above copyright notice, this list of
+ #  conditions and the following disclaimer in the documentation and/or
+ #  other materials provided with the distribution.
+ #
+ #
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ # OF THE POSSIBILITY OF SUCH DAMAGE.
+ #  --------------------------------------------------------------------------
+
+4. Burmese Word Break Dictionary Data (burmesedict.txt)
+
+ #  Copyright (c) 2014 International Business Machines Corporation
+ #  and others. All Rights Reserved.
+ #
+ #  This list is part of a project hosted at:
+ #    github.com/kanyawtech/myanmar-karen-word-lists
+ #
+ #  --------------------------------------------------------------------------
+ #  Copyright (c) 2013, LeRoy Benjamin Sharon
+ #  All rights reserved.
+ #
+ #  Redistribution and use in source and binary forms, with or without
+ #  modification, are permitted provided that the following conditions
+ #  are met: Redistributions of source code must retain the above
+ #  copyright notice, this list of conditions and the following
+ #  disclaimer.  Redistributions in binary form must reproduce the
+ #  above copyright notice, this list of conditions and the following
+ #  disclaimer in the documentation and/or other materials provided
+ #  with the distribution.
+ #
+ #    Neither the name Myanmar Karen Word Lists, nor the names of its
+ #    contributors may be used to endorse or promote products derived
+ #    from this software without specific prior written permission.
+ #
+ #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ #  CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ #  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ #  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
+ #  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ #  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ #  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ #  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ #  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ #  TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ #  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ #  SUCH DAMAGE.
+ #  --------------------------------------------------------------------------
+
+5. Time Zone Database
+
+  ICU uses the public domain data and code derived from Time Zone
+Database for its time zone support. The ownership of the TZ database
+is explained in BCP 175: Procedure for Maintaining the Time Zone
+Database section 7.
+
+ # 7.  Database Ownership
+ #
+ #    The TZ database itself is not an IETF Contribution or an IETF
+ #    document.  Rather it is a pre-existing and regularly updated work
+ #    that is in the public domain, and is intended to remain in the
+ #    public domain.  Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do
+ #    not apply to the TZ Database or contributions that individuals make
+ #    to it.  Should any claims be made and substantiated against the TZ
+ #    Database, the organization that is providing the IANA
+ #    Considerations defined in this RFC, under the memorandum of
+ #    understanding with the IETF, currently ICANN, may act in accordance
+ #    with all competent court orders.  No ownership claims will be made
+ #    by ICANN or the IETF Trust on the database or the code.  Any person
+ #    making a contribution to the database or code waives all rights to
+ #    future claims in that contribution or in the TZ Database.
diff --git a/lib/asciidoctor/BUCK b/lib/asciidoctor/BUCK
index 4cf692e..733c670 100644
--- a/lib/asciidoctor/BUCK
+++ b/lib/asciidoctor/BUCK
@@ -44,17 +44,17 @@
 
 maven_jar(
   name = 'asciidoctor',
-  id = 'org.asciidoctor:asciidoctorj:1.5.2',
-  sha1 = '39d33f739ec1c46f6e908a725264eb74b23c9f99',
-  license = 'Apache2.0',
+  id = 'org.asciidoctor:asciidoctorj:1.5.4.1',
+  sha1 = 'f7ddfb2bbed2f8da3f9ad0d1a5514f04b4274a5a',
+  license = 'asciidoctor',
   visibility = [],
   attach_source = False,
 )
 
 maven_jar(
   name = 'jruby',
-  id = 'org.jruby:jruby-complete:1.7.18',
-  sha1 = 'a1be3e1790aace5c99614a87785454d875eb21c2',
+  id = 'org.jruby:jruby-complete:1.7.25',
+  sha1 = '8eb234259ec88edc05eedab05655f458a84bfcab',
   license = 'DO_NOT_DISTRIBUTE',
   visibility = [],
   attach_source = False,
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index 8013de7..a0e0e9a 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -1,14 +1,14 @@
 include_defs('//lib/maven.defs')
 include_defs('//lib/codemirror/cm.defs')
 
-VERSION = '5.16.0'
+VERSION = '5.17.0'
 TOP = 'META-INF/resources/webjars/codemirror/%s' % VERSION
 TOP_MINIFIED = 'META-INF/resources/webjars/codemirror-minified/%s' % VERSION
 
 maven_jar(
   name = 'codemirror-minified',
   id = 'org.webjars.npm:codemirror-minified:' + VERSION,
-  sha1 = 'ff5a4ae7e1719c4f0ad3e7bfcb56e8ae3910898c',
+  sha1 = '05ad901fc9be67eb7ba8997d896488093deb898e',
   attach_source = False,
   license = 'codemirror-minified',
   visibility = [],
@@ -17,7 +17,7 @@
 maven_jar(
   name = 'codemirror-original',
   id = 'org.webjars.npm:codemirror:' + VERSION,
-  sha1 = '4e547e93f8d06787bea8a1efb290d6dceda31abc',
+  sha1 = 'c025b8d9aca1061e26d1fa482bea0ecea1412e85',
   attach_source = False,
   license = 'codemirror-original',
   visibility = [],
diff --git a/lib/guice/BUCK b/lib/guice/BUCK
index 3893b80..8022ac8 100644
--- a/lib/guice/BUCK
+++ b/lib/guice/BUCK
@@ -1,6 +1,6 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '4.0'
+VERSION = '4.1.0'
 EXCLUDE = [
   'META-INF/DEPENDENCIES',
   'META-INF/LICENSE',
@@ -12,6 +12,7 @@
   exported_deps = [
     ':guice_library',
     ':javax-inject',
+    ':multibindings',
   ],
   visibility = ['PUBLIC'],
 )
@@ -19,7 +20,7 @@
 maven_jar(
   name = 'guice_library',
   id = 'com.google.inject:guice:' + VERSION,
-  sha1 = '0f990a43d3725781b6db7cd0acf0a8b62dfd1649',
+  sha1 = 'eeb69005da379a10071aa4948c48d89250febb07',
   license = 'Apache2.0',
   deps = [':aopalliance'],
   exclude_java_sources = True,
@@ -33,7 +34,7 @@
 maven_jar(
   name = 'guice-assistedinject',
   id = 'com.google.inject.extensions:guice-assistedinject:' + VERSION,
-  sha1 = '8fa6431da1a2187817e3e52e967535899e2e46ca',
+  sha1 = 'af799dd7e23e6fe8c988da12314582072b07edcb',
   license = 'Apache2.0',
   deps = [':guice'],
   exclude = EXCLUDE,
@@ -42,7 +43,7 @@
 maven_jar(
   name = 'guice-servlet',
   id = 'com.google.inject.extensions:guice-servlet:' + VERSION,
-  sha1 = '4503da866f4c402b5090579b40c1c4aaefabb164',
+  sha1 = '90ac2db772d9b85e2b05417b74f7464bcc061dcb',
   license = 'Apache2.0',
   deps = [':guice'],
   exclude = EXCLUDE,
@@ -63,3 +64,16 @@
   license = 'Apache2.0',
   visibility = ['PUBLIC'],
 )
+
+maven_jar(
+  name = 'multibindings',
+  id = 'com.google.inject.extensions:guice-multibindings:' + VERSION,
+  sha1 = '3b27257997ac51b0f8d19676f1ea170427e86d51',
+  exclude_java_sources = True,
+  exclude = EXCLUDE + [
+    'META-INF/maven/com.google.guava/guava/pom.properties',
+    'META-INF/maven/com.google.guava/guava/pom.xml',
+  ],
+  license = 'Apache2.0',
+  visibility = ['PUBLIC']
+)
diff --git a/lib/highlightjs/BUCK b/lib/highlightjs/BUCK
new file mode 100644
index 0000000..9940136
--- /dev/null
+++ b/lib/highlightjs/BUCK
@@ -0,0 +1,5 @@
+export_file(
+  name = 'highlightjs',
+  src = 'highlight.min.js',
+  visibility = ['PUBLIC'],
+)
diff --git a/lib/highlightjs/building.md b/lib/highlightjs/building.md
new file mode 100644
index 0000000..8cb9e8b
--- /dev/null
+++ b/lib/highlightjs/building.md
@@ -0,0 +1,72 @@
+# Building Highlight.js for Gerrit
+
+Highlight JS needs to be built with specific language support. Here are the
+steps to build the minified file that appears here.
+
+NOTE: If you are adding support for a language to Highlight.js make sure to add
+it to the list of languages in the build command below.
+
+## Prerequisites
+
+You will need:
+
+* nodejs
+* closure-compiler
+* git
+
+## Steps to Create the Pack File
+
+The packed version of Highlight.js is an un-minified JS file with all of the
+languages included. Build it with the following:
+
+    $>  # start in some temp directory
+    $>  git clone https://github.com/isagalaev/highlight.js.git
+    $>  cd highlight.js
+    $>  node tools/build.js -n \
+          bash \
+          cpp \
+          cs \
+          clojure \
+          css \
+          d \
+          dart \
+          go \
+          haskell \
+          java \
+          javascript \
+          json \
+          lisp \
+          lua \
+          markdown \
+          objectivec \
+          ocaml \
+          perl \
+          protobuf \
+          python \
+          ruby \
+          rust \
+          scala \
+          sql \
+          swift \
+          typescript \
+          xml \
+          yaml
+
+The resulting JS file will appear in the "build" directory of the Highlight.js
+repo under the name "highlight.pack.js".
+
+## Minification
+
+Minify the file using closure-compiler using the command below. (Modify
+`/path/to` with the path to your compiler jar.)
+
+    $>  java -jar /path/to/closure-compiler.jar \
+            --js build/highlight.pack.js \
+            --js_output_file build/highlight.min.js
+
+Copy the header comment that appears on the first line of
+build/highlight.pack.js and add it to the start of build/highlight.min.js.
+
+## Finish
+
+Copy the resulting build/highlight.min.js file to lib/highlightjs
diff --git a/lib/highlightjs/highlight.min.js b/lib/highlightjs/highlight.min.js
new file mode 100644
index 0000000..cfc8c1c
--- /dev/null
+++ b/lib/highlightjs/highlight.min.js
@@ -0,0 +1,105 @@
+/*! highlight.js v9.5.0 | BSD3 License | git.io/hljslicense */
+(function(b){var p="object"===typeof window&&window||"object"===typeof self&&self;"undefined"!==typeof exports?b(exports):p&&(p.hljs=b({}),"function"===typeof define&&define.amd&&define([],function(){return p.hljs}))})(function(b){function p(a){return a.replace(/[&<>]/gm,function(a){return M[a]})}function C(a,c){var e=a&&a.exec(c);return e&&0===e.index}function v(a,c){var e,b={};for(e in a)b[e]=a[e];if(c)for(e in c)b[e]=c[e];return b}function H(a){var c=[];(function g(a,b){for(var k=a.firstChild;k;k=
+k.nextSibling)3===k.nodeType?b+=k.nodeValue.length:1===k.nodeType&&(c.push({event:"start",offset:b,node:k}),b=g(k,b),k.nodeName.toLowerCase().match(/br|hr|img|input/)||c.push({event:"stop",offset:b,node:k}));return b})(a,0);return c}function N(a,c,e){function b(){return a.length&&c.length?a[0].offset!==c[0].offset?a[0].offset<c[0].offset?a:c:"start"===c[0].event?a:c:a.length?a:c}function d(a){n+="<"+a.nodeName.toLowerCase()+I.map.call(a.attributes,function(a){return" "+a.nodeName+'="'+p(a.value)+
+'"'}).join("")+">"}function f(a){n+="</"+a.nodeName.toLowerCase()+">"}function k(a){("start"===a.event?d:f)(a.node)}for(var l=0,n="",m=[];a.length||c.length;){var h=b(),n=n+p(e.substr(l,h[0].offset-l)),l=h[0].offset;if(h===a){m.reverse().forEach(f);do k(h.splice(0,1)[0]),h=b();while(h===a&&h.length&&h[0].offset===l);m.reverse().forEach(d)}else"start"===h[0].event?m.push(h[0].node):m.pop(),k(h.splice(0,1)[0])}return n+p(e.substr(l))}function O(a){function c(a){return a&&a.source||a}function e(e,b){return new RegExp(c(e),
+"m"+(a.case_insensitive?"i":"")+(b?"g":""))}function b(d,f){if(!d.compiled){d.compiled=!0;d.keywords=d.keywords||d.beginKeywords;if(d.keywords){var k={},l=function(c,e){a.case_insensitive&&(e=e.toLowerCase());e.split(" ").forEach(function(a){a=a.split("|");k[a[0]]=[c,a[1]?Number(a[1]):1]})};"string"===typeof d.keywords?l("keyword",d.keywords):D(d.keywords).forEach(function(a){l(a,d.keywords[a])});d.keywords=k}d.lexemesRe=e(d.lexemes||/\w+/,!0);f&&(d.beginKeywords&&(d.begin="\\b("+d.beginKeywords.split(" ").join("|")+
+")\\b"),d.begin||(d.begin=/\B|\b/),d.beginRe=e(d.begin),d.end||d.endsWithParent||(d.end=/\B|\b/),d.end&&(d.endRe=e(d.end)),d.terminator_end=c(d.end)||"",d.endsWithParent&&f.terminator_end&&(d.terminator_end+=(d.end?"|":"")+f.terminator_end));d.illegal&&(d.illegalRe=e(d.illegal));null==d.relevance&&(d.relevance=1);d.contains||(d.contains=[]);var n=[];d.contains.forEach(function(a){a.variants?a.variants.forEach(function(c){n.push(v(a,c))}):n.push("self"===a?d:a)});d.contains=n;d.contains.forEach(function(a){b(a,
+d)});d.starts&&b(d.starts,f);var m=d.contains.map(function(a){return a.beginKeywords?"\\.?("+a.begin+")\\.?":a.begin}).concat([d.terminator_end,d.illegal]).map(c).filter(Boolean);d.terminators=m.length?e(m.join("|"),!0):{exec:function(){return null}}}}b(a)}function A(a,c,e,b){function d(a,c){if(C(a.endRe,c)){for(;a.endsParent&&a.parent;)a=a.parent;return a}if(a.endsWithParent)return d(a.parent,c)}function f(a,c,e,b){return'<span class="'+(b?"":t.classPrefix)+(a+'">')+c+(e?"":"</span>")}function k(){var a=
+r,c;if(null!=h.subLanguage)if((c="string"===typeof h.subLanguage)&&!w[h.subLanguage])c=p(q);else{var e=c?A(h.subLanguage,q,!0,u[h.subLanguage]):F(q,h.subLanguage.length?h.subLanguage:void 0);0<h.relevance&&(B+=e.relevance);c&&(u[h.subLanguage]=e.top);c=f(e.language,e.value,!1,!0)}else{var b;if(h.keywords){e="";b=0;h.lexemesRe.lastIndex=0;for(c=h.lexemesRe.exec(q);c;){e+=p(q.substr(b,c.index-b));b=h;var d=c,d=m.case_insensitive?d[0].toLowerCase():d[0];(b=b.keywords.hasOwnProperty(d)&&b.keywords[d])?
+(B+=b[1],e+=f(b[0],p(c[0]))):e+=p(c[0]);b=h.lexemesRe.lastIndex;c=h.lexemesRe.exec(q)}c=e+p(q.substr(b))}else c=p(q)}r=a+c;q=""}function l(a){r+=a.className?f(a.className,"",!0):"";h=Object.create(a,{parent:{value:h}})}function n(a,c){q+=a;if(null==c)return k(),0;var b;a:{b=h;var f,g;f=0;for(g=b.contains.length;f<g;f++)if(C(b.contains[f].beginRe,c)){b=b.contains[f];break a}b=void 0}if(b)return b.skip?q+=c:(b.excludeBegin&&(q+=c),k(),b.returnBegin||b.excludeBegin||(q=c)),l(b,c),b.returnBegin?0:c.length;
+if(b=d(h,c)){f=h;f.skip?q+=c:(f.returnEnd||f.excludeEnd||(q+=c),k(),f.excludeEnd&&(q=c));do h.className&&(r+="</span>"),h.skip||(B+=h.relevance),h=h.parent;while(h!==b.parent);b.starts&&l(b.starts,"");return f.returnEnd?0:c.length}if(!e&&C(h.illegalRe,c))throw Error('Illegal lexeme "'+c+'" for mode "'+(h.className||"<unnamed>")+'"');q+=c;return c.length||1}var m=x(a);if(!m)throw Error('Unknown language: "'+a+'"');O(m);var h=b||m,u={},r="";for(b=h;b!==m;b=b.parent)b.className&&(r=f(b.className,"",
+!0)+r);var q="",B=0;try{for(var y,v,z=0;;){h.terminators.lastIndex=z;y=h.terminators.exec(c);if(!y)break;v=n(c.substr(z,y.index-z),y[0]);z=y.index+v}n(c.substr(z));for(b=h;b.parent;b=b.parent)b.className&&(r+="</span>");return{relevance:B,value:r,language:a,top:h}}catch(E){if(E.message&&-1!==E.message.indexOf("Illegal"))return{relevance:0,value:p(c)};throw E;}}function F(a,c){c=c||t.languages||D(w);var b={relevance:0,value:p(a)},g=b;c.filter(x).forEach(function(c){var f=A(c,a,!1);f.language=c;f.relevance>
+g.relevance&&(g=f);f.relevance>b.relevance&&(g=b,b=f)});g.language&&(b.second_best=g);return b}function J(a){return t.tabReplace||t.useBR?a.replace(P,function(a,b){if(t.useBR&&"\n"===a)return"<br>";if(t.tabReplace)return b.replace(/\t/g,t.tabReplace)}):a}function K(a){var c,b,g,d,f;a:if(b=a.className+" ",b+=a.parentNode?a.parentNode.className:"",f=Q.exec(b))f=x(f[1])?f[1]:"no-highlight";else{b=b.split(/\s+/);f=0;for(d=b.length;f<d;f++)if(c=b[f],L.test(c)||x(c)){f=c;break a}f=void 0}L.test(f)||(t.useBR?
+(c=document.createElementNS("http://www.w3.org/1999/xhtml","div"),c.innerHTML=a.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")):c=a,d=c.textContent,b=f?A(f,d,!0):F(d),c=H(c),c.length&&(g=document.createElementNS("http://www.w3.org/1999/xhtml","div"),g.innerHTML=b.value,b.value=N(c,H(g),d)),b.value=J(b.value),a.innerHTML=b.value,d=a.className,f=f?G[f]:b.language,c=[d.trim()],d.match(/\bhljs\b/)||c.push("hljs"),-1===d.indexOf(f)&&c.push(f),f=c.join(" ").trim(),a.className=f,a.result={language:b.language,
+re:b.relevance},b.second_best&&(a.second_best={language:b.second_best.language,re:b.second_best.relevance}))}function u(){if(!u.called){u.called=!0;var a=document.querySelectorAll("pre code");I.forEach.call(a,K)}}function x(a){a=(a||"").toLowerCase();return w[a]||w[G[a]]}var I=[],D=Object.keys,w={},G={},L=/^(no-?highlight|plain|text)$/i,Q=/\blang(?:uage)?-([\w-]+)\b/i,P=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,t={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},M={"&":"&amp;","<":"&lt;",">":"&gt;"};
+b.highlight=A;b.highlightAuto=F;b.fixMarkup=J;b.highlightBlock=K;b.configure=function(a){t=v(t,a)};b.initHighlighting=u;b.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",u,!1);addEventListener("load",u,!1)};b.registerLanguage=function(a,c){var e=w[a]=c(b);e.aliases&&e.aliases.forEach(function(c){G[c]=a})};b.listLanguages=function(){return D(w)};b.getLanguage=x;b.inherit=v;b.IDENT_RE="[a-zA-Z]\\w*";b.UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*";b.NUMBER_RE="\\b\\d+(\\.\\d+)?";b.C_NUMBER_RE=
+"(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";b.BINARY_NUMBER_RE="\\b(0b[01]+)";b.RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";b.BACKSLASH_ESCAPE={begin:"\\\\[\\s\\S]",relevance:0};b.APOS_STRING_MODE={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.QUOTE_STRING_MODE={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};
+b.PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/};b.COMMENT=function(a,c,e){a=b.inherit({className:"comment",begin:a,end:c,contains:[]},e||{});a.contains.push(b.PHRASAL_WORDS_MODE);a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|XXX):",relevance:0});return a};b.C_LINE_COMMENT_MODE=b.COMMENT("//","$");b.C_BLOCK_COMMENT_MODE=b.COMMENT("/\\*","\\*/");b.HASH_COMMENT_MODE=b.COMMENT("#",
+"$");b.NUMBER_MODE={className:"number",begin:b.NUMBER_RE,relevance:0};b.C_NUMBER_MODE={className:"number",begin:b.C_NUMBER_RE,relevance:0};b.BINARY_NUMBER_MODE={className:"number",begin:b.BINARY_NUMBER_RE,relevance:0};b.CSS_NUMBER_MODE={className:"number",begin:b.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0};b.REGEXP_MODE={className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[b.BACKSLASH_ESCAPE,{begin:/\[/,
+end:/\]/,relevance:0,contains:[b.BACKSLASH_ESCAPE]}]};b.TITLE_MODE={className:"title",begin:b.IDENT_RE,relevance:0};b.UNDERSCORE_TITLE_MODE={className:"title",begin:b.UNDERSCORE_IDENT_RE,relevance:0};b.METHOD_GUARD={begin:"\\.\\s*"+b.UNDERSCORE_IDENT_RE,relevance:0};b.registerLanguage("bash",function(a){var c={className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)}/}]},b={className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,c,{className:"variable",begin:/\$\(/,
+end:/\)/,contains:[a.BACKSLASH_ESCAPE]}]};return{aliases:["sh","zsh"],lexemes:/-?[a-z\.]+/,keywords:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",
+_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[{className:"meta",begin:/^#![^\n]+sh\s*$/,relevance:10},{className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0},a.HASH_COMMENT_MODE,b,{className:"string",begin:/'/,end:/'/},c]}});b.registerLanguage("clojure",function(a){var c={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",relevance:0},b=a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),g=a.COMMENT(";","$",{relevance:0}),
+d={className:"literal",begin:/\b(true|false|nil)\b/},f={begin:"[\\[\\{]",end:"[\\]\\}]"},k={className:"comment",begin:"\\^[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},l=a.COMMENT("\\^\\{","\\}"),n={className:"symbol",begin:"[:]{1,2}[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},m={begin:"\\(",end:"\\)"},h={endsWithParent:!0,relevance:0},p={keywords:{"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},
+lexemes:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",className:"name",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",starts:h},r=[m,b,k,l,g,n,f,c,d,{begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0}];m.contains=[a.COMMENT("comment",""),p,h];h.contains=r;f.contains=r;return{aliases:["clj"],illegal:/\S/,contains:[m,b,k,l,g,n,f,c,d]}});b.registerLanguage("cpp",function(a){var c={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},b={className:"string",variants:[{begin:'(u8?|U)?L?"',
+end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'(u8?|U)?R"',end:'"',contains:[a.BACKSLASH_ESCAPE]},{begin:"'\\\\?.",end:"'",illegal:"."}]},g={className:"number",variants:[{begin:"\\b(0b[01'_]+)"},{begin:"\\b([\\d'_]+(\\.[\\d'_]*)?|\\.[\\d'_]+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9'_]+|(\\b[\\d'_]+(\\.[\\d'_]*)?|\\.[\\d'_]+)([eE][-+]?[\\d'_]+)?)"}],relevance:0},d={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},
+contains:[{begin:/\\\n/,relevance:0},a.inherit(b,{className:"meta-string"}),{className:"meta-string",begin:"<",end:">",illegal:"\\n"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},f=a.IDENT_RE+"\\s*\\(",k={keyword:"int float while private char catch export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const struct for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using class asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return",
+built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr",
+literal:"true false nullptr NULL"},l=[c,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,g,b];return{aliases:"c cc h c++ h++ hpp".split(" "),keywords:k,illegal:"</",contains:l.concat([d,{begin:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",end:">",keywords:k,contains:["self",c]},{begin:a.IDENT_RE+"::",keywords:k},{variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",
+end:/;/}],keywords:k,contains:l.concat([{begin:/\(/,end:/\)/,keywords:k,contains:l.concat(["self"]),relevance:0}]),relevance:0},{className:"function",begin:"("+a.IDENT_RE+"[\\*&\\s]+)+"+f,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:k,illegal:/[^\w\s\*&]/,contains:[{begin:f,returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:k,relevance:0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,g,c]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,
+d]}]),exports:{preprocessor:d,strings:b,keywords:k}}});b.registerLanguage("cs",function(a){var c={keyword:"abstract as base bool break byte case catch char checked const continue decimal dynamic default delegate do double else enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long when object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual volatile void while async nameof ascending descending from get group into join let orderby partial select set value var where yield",
+literal:"null false true"},b={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},g=a.inherit(b,{illegal:/\n/}),d={className:"subst",begin:"{",end:"}",keywords:c},f=a.inherit(d,{illegal:/\n/}),k={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},a.BACKSLASH_ESCAPE,f]},l={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},d]},n=a.inherit(l,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},f]});d.contains=
+[l,k,b,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE];f.contains=[n,k,g,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.inherit(a.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];b={variants:[l,k,b,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]};g=a.IDENT_RE+"(<"+a.IDENT_RE+">)?(\\[\\])?";return{aliases:["csharp"],keywords:c,illegal:/::/,contains:[a.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:"</?",
+end:">"}]}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},b,a.C_NUMBER_MODE,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.TITLE_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},
+{beginKeywords:"new return throw await",relevance:0},{className:"function",begin:"("+g+"\\s+)+"+a.IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:c,contains:[{begin:a.IDENT_RE+"\\s*\\(",returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:c,relevance:0,contains:[b,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}});b.registerLanguage("css",function(a){return{case_insensitive:!0,
+illegal:/[=\/|'\$]/,contains:[a.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/},{className:"selector-class",begin:/\.[A-Za-z0-9_-]+/},{className:"selector-attr",begin:/\[/,end:/\]/,illegal:"$"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"@(font-face|page)",lexemes:"[a-z-]+",keywords:"font-face page"},{begin:"@",end:"[{;]",illegal:/:/,contains:[{className:"keyword",begin:/\w+/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,contains:[a.APOS_STRING_MODE,
+a.QUOTE_STRING_MODE,a.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0},{begin:"{",end:"}",illegal:/\S/,contains:[a.C_BLOCK_COMMENT_MODE,{begin:/[A-Z\_\.\-]+\s*:/,returnBegin:!0,end:";",endsWithParent:!0,contains:[{className:"attribute",begin:/\S/,end:":",excludeEnd:!0,starts:{endsWithParent:!0,excludeEnd:!0,contains:[{begin:/[\w-]+\(/,returnBegin:!0,contains:[{className:"built_in",begin:/[\w-]+/},{begin:/\(/,end:/\)/,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}]},
+a.CSS_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",begin:"#[0-9A-Fa-f]+"},{className:"meta",begin:"!important"}]}}]}]}]}});b.registerLanguage("d",function(a){var b=a.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{lexemes:a.UNDERSCORE_IDENT_RE,keywords:{keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",
+built_in:"bool cdouble cent cfloat char creal dchar delegate double dstring float function idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar wstring",literal:"false null true"},contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,{className:"string",begin:'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?',relevance:10},{className:"string",begin:'"',contains:[{begin:"\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};",relevance:0}],
+end:'"[cwd]?'},{className:"string",begin:'[rq]"',end:'"[cwd]?',relevance:5},{className:"string",begin:"`",end:"`[cwd]?"},{className:"string",begin:'q"\\{',end:'\\}"'},{className:"number",begin:"\\b(((0[xX](([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)\\.([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)|\\.?([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))[pP][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))|((0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(\\.\\d*|([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)))|\\d+\\.(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)|\\.(0|[1-9][\\d_]*)([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))?))([fF]|L|i|[fF]i|Li)?|((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))(i|[fF]i|Li))",
+relevance:0},{className:"number",begin:"\\b((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))(L|u|U|Lu|LU|uL|UL)?",relevance:0},{className:"string",begin:"'(\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};|.)",end:"'",illegal:"."},{className:"meta",begin:"^#!",end:"$",relevance:5},{className:"meta",begin:"#(line)",end:"$",relevance:5},{className:"keyword",begin:"@[a-zA-Z_][a-zA-Z_\\d]*"}]}});b.registerLanguage("markdown",
+function(a){return{aliases:["md","mkdown","mkd"],contains:[{className:"section",variants:[{begin:"^#{1,6}",end:"$"},{begin:"^.+?\\n[=-]{2,}$"}]},{begin:"<",end:">",subLanguage:"xml",relevance:0},{className:"bullet",begin:"^([*+-]|(\\d+\\.))\\s+"},{className:"strong",begin:"[*_]{2}.+?[*_]{2}"},{className:"emphasis",variants:[{begin:"\\*.+?\\*"},{begin:"_.+?_",relevance:0}]},{className:"quote",begin:"^>\\s+",end:"$"},{className:"code",variants:[{begin:"^```w*s*$",end:"^```s*$"},{begin:"`.+?`"},{begin:"^( {4}|\t)",
+end:"$",relevance:0}]},{begin:"^[-\\*]{3,}",end:"$"},{begin:"\\[.+?\\][\\(\\[].*?[\\)\\]]",returnBegin:!0,contains:[{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0,relevance:0},{className:"link",begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}],relevance:10},{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link",
+begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}});b.registerLanguage("dart",function(a){var b={className:"subst",begin:"\\$\\{",end:"}",keywords:"true false null this is new super"},e={className:"string",variants:[{begin:"r'''",end:"'''"},{begin:'r"""',end:'"""'},{begin:"r'",end:"'",illegal:"\\n"},{begin:'r"',end:'"',illegal:"\\n"},{begin:"'''",end:"'''",contains:[a.BACKSLASH_ESCAPE,b]},{begin:'"""',end:'"""',contains:[a.BACKSLASH_ESCAPE,b]},{begin:"'",end:"'",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,
+b]},{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]}]};b.contains=[a.C_NUMBER_MODE,e];return{keywords:{keyword:"assert async await break case catch class const continue default do else enum extends false final finally for if in is new null rethrow return super switch sync this throw true try var void while with yield abstract as dynamic export external factory get implements import library operator part set static typedef",built_in:"print Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set Stopwatch String StringBuffer StringSink Symbol Type Uri bool double int num document window querySelector querySelectorAll Element ElementList"},
+contains:[e,a.COMMENT("/\\*\\*","\\*/",{subLanguage:"markdown"}),a.COMMENT("///","$",{subLanguage:"markdown"}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{begin:"=>"}]}});b.registerLanguage("go",function(a){var b={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",
+literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{aliases:["golang"],keywords:b,illegal:"</",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[a.QUOTE_STRING_MODE,{begin:"'",end:"[^\\\\]'"},{begin:"`",end:"`"}]},{className:"number",variants:[{begin:a.C_NUMBER_RE+"[dflsi]",relevance:1},a.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func",end:/\s*\{/,excludeEnd:!0,
+contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:b,illegal:/["']/}]}]}});b.registerLanguage("haskell",function(a){var b={variants:[a.COMMENT("--","$"),a.COMMENT("{-","-}",{contains:["self"]})]},e={className:"meta",begin:"{-#",end:"#-}"},g={className:"meta",begin:"^#",end:"$"},d={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},f={begin:"\\(",end:"\\)",illegal:'"',contains:[e,g,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},a.inherit(a.TITLE_MODE,{begin:"[_a-z][\\w']*"}),
+b]};return{aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[f,b],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[f,b],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b",
+end:"where",keywords:"class family instance where",contains:[d,f,b]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[e,d,f,{begin:"{",end:"}",contains:f.contains},b]},{beginKeywords:"default",end:"$",contains:[d,f,b]},{beginKeywords:"infix infixl infixr",end:"$",contains:[a.C_NUMBER_MODE,b]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[d,a.QUOTE_STRING_MODE,
+b]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},e,g,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,d,a.inherit(a.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),b,{begin:"->|<-"}]}});b.registerLanguage("java",function(a){var b=a.UNDERSCORE_IDENT_RE+"(<"+a.UNDERSCORE_IDENT_RE+"(\\s*,\\s*"+a.UNDERSCORE_IDENT_RE+")*>)?";return{aliases:["jsp"],keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports",
+illegal:/<\/|#/,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"("+
+b+"\\s+)+"+a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports",contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,
+relevance:0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports",relevance:0,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,
+a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0},{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("javascript",function(a){return{aliases:["js","jsx"],keywords:{keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",
+literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},
+contains:[{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},{className:"meta",begin:/^#!/,end:/$/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,{className:"subst",begin:"\\$\\{",end:"\\}"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",
+contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{begin:/</,end:/(\/\w+|\w+\/)>/,subLanguage:"xml",contains:[{begin:/<\w+\s*\/>/,skip:!0},{begin:/<\w+/,end:/(\/\w+|\w+\/)>/,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}],
+illegal:/\[|%/},{begin:/\$[(.]/},a.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0}],illegal:/#(?!!)/}});b.registerLanguage("json",function(a){var b={literal:"true false null"},e=[a.QUOTE_STRING_MODE,a.C_NUMBER_MODE],g={end:",",endsWithParent:!0,excludeEnd:!0,contains:e,keywords:b},d={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,
+end:/"/,contains:[a.BACKSLASH_ESCAPE],illegal:"\\n"},a.inherit(g,{begin:/:/})],illegal:"\\S"};a={begin:"\\[",end:"\\]",contains:[a.inherit(g)],illegal:"\\S"};e.splice(e.length,0,d,a);return{contains:e,keywords:b,illegal:"\\S"}});b.registerLanguage("lisp",function(a){var b={className:"literal",begin:"\\b(t{1}|nil)\\b"},e={className:"number",variants:[{begin:"(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",relevance:0},{begin:"#(b|B)[0-1]+(/[0-1]+)?"},{begin:"#(o|O)[0-7]+(/[0-7]+)?"},
+{begin:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"},{begin:"#(c|C)\\((\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)? +(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",end:"\\)"}]},g=a.inherit(a.QUOTE_STRING_MODE,{illegal:null});a=a.COMMENT(";","$",{relevance:0});var d={begin:"\\*",end:"\\*"},f={className:"symbol",begin:"[:&][a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},k={begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*",
+relevance:0},l={contains:[e,g,d,f,{begin:"\\(",end:"\\)",contains:["self",b,g,e,k]},k],variants:[{begin:"['`]\\(",end:"\\)"},{begin:"\\(quote ",end:"\\)",keywords:{name:"quote"}},{begin:"'\\|[^]*?\\|"}]},n={variants:[{begin:"'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"#'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*(::[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*)*"}]},m={begin:"\\(\\s*",end:"\\)"},
+h={endsWithParent:!0,relevance:0};m.contains=[{className:"name",variants:[{begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"\\|[^]*?\\|"}]},h];h.contains=[l,n,m,b,e,g,a,d,f,{begin:"\\|[^]*?\\|"},k];return{illegal:/\S/,contains:[e,{className:"meta",begin:"^#!",end:"$"},b,g,a,l,n,m,k]}});b.registerLanguage("lua",function(a){var b={begin:"\\[=*\\[",end:"\\]=*\\]",contains:["self"]},e=[a.COMMENT("--(?!\\[=*\\[)","$"),a.COMMENT("--\\[=*\\[","\\]=*\\]",{contains:[b],
+relevance:10})];return{lexemes:a.UNDERSCORE_IDENT_RE,keywords:{keyword:"and break do else elseif end false for if in local nil not or repeat return then true until while",built_in:"_G _VERSION assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall coroutine debug io math os package string table"},contains:e.concat([{className:"function",beginKeywords:"function",
+end:"\\)",contains:[a.inherit(a.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:e}].concat(e)},a.C_NUMBER_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[b],relevance:5}])}});b.registerLanguage("xml",function(a){var b={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:"[A-Za-z0-9\\._:-]+",relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",
+endsParent:!0,variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/},{begin:/[^\s"'=<>`]+/}]}]}]};return{aliases:"html xhtml rss atom xjb xsd xsl plist".split(" "),case_insensitive:!0,contains:[{className:"meta",begin:"<!DOCTYPE",end:">",relevance:10,contains:[{begin:"\\[",end:"\\]"}]},a.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{begin:/<\?(php)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0}]},{className:"tag",begin:"<style(?=\\s|>|$)",
+end:">",keywords:{name:"style"},contains:[b],starts:{end:"</style>",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"<script(?=\\s|>|$)",end:">",keywords:{name:"script"},contains:[b],starts:{end:"\x3c/script>",returnEnd:!0,subLanguage:["actionscript","javascript","handlebars","xml"]}},{className:"meta",variants:[{begin:/<\?xml/,end:/\?>/,relevance:10},{begin:/<\?\w+/,end:/\?>/}]},{className:"tag",begin:"</?",end:"/?>",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},b]}]}});
+b.registerLanguage("objectivec",function(a){var b=/[a-zA-Z@][a-zA-Z0-9_]*/;return{aliases:["mm","objc","obj-c"],keywords:{keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",
+literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},lexemes:b,illegal:"</",contains:[{className:"built_in",begin:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.C_NUMBER_MODE,a.QUOTE_STRING_MODE,{className:"string",variants:[{begin:'@"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:"'",end:"[^\\\\]'",illegal:"[^\\\\][^']"}]},
+{className:"meta",begin:"#",end:"$",contains:[{className:"meta-string",variants:[{begin:'"',end:'"'},{begin:"<",end:">"}]}]},{className:"class",begin:"(@interface|@class|@protocol|@implementation)\\b",end:"({|$)",excludeEnd:!0,keywords:"@interface @class @protocol @implementation",lexemes:b,contains:[a.UNDERSCORE_TITLE_MODE]},{begin:"\\."+a.UNDERSCORE_IDENT_RE,relevance:0}]}});b.registerLanguage("ocaml",function(a){return{aliases:["ml"],keywords:{keyword:"and as assert asr begin class constraint do done downto else end exception external for fun function functor if in include inherit! inherit initializer land lazy let lor lsl lsr lxor match method!|10 method mod module mutable new object of open! open or private rec sig struct then to try type val! val virtual when while with parser value",
+built_in:"array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 string unit in_channel out_channel ref",literal:"true false"},illegal:/\/\/|>>/,lexemes:"[a-z_]\\w*!?",contains:[{className:"literal",begin:"\\[(\\|\\|)?\\]|\\(\\)",relevance:0},a.COMMENT("\\(\\*","\\*\\)",{contains:["self"]}),{className:"symbol",begin:"'[A-Za-z_](?!')[\\w']*"},{className:"type",begin:"`[A-Z][\\w']*"},{className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},{begin:"[a-z_]\\w*'[\\w']*",relevance:0},
+a.inherit(a.APOS_STRING_MODE,{className:"string",relevance:0}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),{className:"number",begin:"\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)",relevance:0},{begin:/[-=]>/}]}});b.registerLanguage("perl",function(a){var b={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when"},
+e={begin:"->{",end:"}"},g={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},d=[a.BACKSLASH_ESCAPE,b,g];a=[g,a.HASH_COMMENT_MODE,a.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),e,{className:"string",contains:d,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<",
+end:"\\>",relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},{begin:"{\\w+}",contains:[],relevance:0},{begin:"-?\\w+\\s*\\=\\>",contains:[],relevance:0}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\/\\/|"+a.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",keywords:"split return print reverse grep",
+relevance:0,contains:[a.HASH_COMMENT_MODE,{className:"regexp",begin:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",relevance:10},{className:"regexp",begin:"(m|qr)?/",end:"/[a-z]*",contains:[a.BACKSLASH_ESCAPE],relevance:0}]},{className:"function",beginKeywords:"sub",end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[a.TITLE_MODE]},{begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]}];b.contains=
+a;e.contains=a;return{aliases:["pl","pm"],lexemes:/[\w\.]+/,keywords:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",
+contains:a}});b.registerLanguage("php",function(a){var b={begin:"\\$+[a-zA-Z_\u007f-\u00ff][a-zA-Z0-9_\u007f-\u00ff]*"},e={className:"meta",begin:/<\?(php)?|\?>/},g={className:"string",contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},d={variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]};return{aliases:["php3","php4","php5","php6"],case_insensitive:!0,keywords:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",
+contains:[a.HASH_COMMENT_MODE,a.COMMENT("//","$",{contains:[e]}),a.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),a.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler",lexemes:a.UNDERSCORE_IDENT_RE}),{className:"string",begin:/<<<['"]?\w+['"]?$/,end:/^\w+;?$/,contains:[a.BACKSLASH_ESCAPE,{className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]}]},e,{className:"keyword",begin:/\$this\b/},b,{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},
+{className:"function",beginKeywords:"function",end:/[;{]/,excludeEnd:!0,illegal:"\\$|\\[|%",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",contains:["self",b,a.C_BLOCK_COMMENT_MODE,g,d]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[a.UNDERSCORE_TITLE_MODE]},
+{begin:"=>"},g,d]}});b.registerLanguage("protobuf",function(a){return{keywords:{keyword:"package import option optional required repeated group",built_in:"double float int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string bytes",literal:"true false"},contains:[a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.C_LINE_COMMENT_MODE,{className:"class",beginKeywords:"message enum service",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]},
+{className:"function",beginKeywords:"rpc",end:/;/,excludeEnd:!0,keywords:"rpc returns"},{begin:/^\s*[A-Z_]+/,end:/\s*=/,excludeEnd:!0}]}});b.registerLanguage("python",function(a){var b={className:"meta",begin:/^(>>>|\.\.\.) /},e={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[b],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[b],relevance:10},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,
+end:/'/},{begin:/(b|br)"/,end:/"/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},g={className:"number",relevance:0,variants:[{begin:a.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:a.C_NUMBER_RE+"[lLjJ]?"}]};return{aliases:["py","gyp"],keywords:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},
+illegal:/(<\/|->|\?)/,contains:[b,g,e,a.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def",relevance:10},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,contains:["self",b,g,e]},{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}});b.registerLanguage("ruby",function(a){var b={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",
+literal:"true false nil"},e={className:"doctag",begin:"@[A-Za-z]+"},g={begin:"#<",end:">"},e=[a.COMMENT("#","$",{contains:[e]}),a.COMMENT("^\\=begin","^\\=end",{contains:[e],relevance:10}),a.COMMENT("^__END__","\\n$")],d={className:"subst",begin:"#\\{",end:"}",keywords:b},f={className:"string",contains:[a.BACKSLASH_ESCAPE,d],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",
+end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]},k={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:b};a=[f,g,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+a.IDENT_RE+"::)?"+a.IDENT_RE}]}].concat(e)},
+{className:"function",beginKeywords:"def",end:"$|;",contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}),k].concat(e)},{begin:a.IDENT_RE+"::"},{className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[f,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",
+relevance:0},{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:b},{begin:"("+a.RE_STARTERS_RE+")\\s*",contains:[g,{className:"regexp",contains:[a.BACKSLASH_ESCAPE,d],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(e),relevance:0}].concat(e);d.contains=a;k.contains=a;return{aliases:["rb","gemspec","podspec","thor","irb"],keywords:b,
+illegal:/\/\*/,contains:e.concat([{begin:/^\s*=>/,starts:{end:"$",contains:a}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:a}}]).concat(a)}});b.registerLanguage("rust",function(a){var b=a.inherit(a.C_BLOCK_COMMENT_MODE);b.contains.push("self");return{aliases:["rs"],keywords:{keyword:"alignof as be box break const continue crate do else enum extern false fn for if impl in let loop match mod mut offsetof once priv proc pub pure ref return self Self sizeof static struct super trait true type typeof unsafe unsized use virtual while where yield move default int i8 i16 i32 i64 isize uint u8 u32 u64 usize float f32 f64 str char bool",
+literal:"true false Some None Ok Err",built_in:"Copy Send Sized Sync Drop Fn FnMut FnOnce drop Box ToOwned Clone PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator Option Result SliceConcatExt String ToString Vec assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules!"},
+lexemes:a.IDENT_RE+"!?",illegal:"</",contains:[a.C_LINE_COMMENT_MODE,b,a.inherit(a.QUOTE_STRING_MODE,{begin:/b?"/,illegal:null}),{className:"string",variants:[{begin:/r(#*)".*?"\1(?!#)/},{begin:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{className:"symbol",begin:/'[a-zA-Z_][a-zA-Z0-9_]*/},{className:"number",variants:[{begin:"\\b0b([01_]+)([uif](8|16|32|64|size))?"},{begin:"\\b0o([0-7_]+)([uif](8|16|32|64|size))?"},{begin:"\\b0x([A-Fa-f0-9_]+)([uif](8|16|32|64|size))?"},{begin:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)([uif](8|16|32|64|size))?"}],
+relevance:0},{className:"function",beginKeywords:"fn",end:"(\\(|<)",excludeEnd:!0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"meta",begin:"#\\!?\\[",end:"\\]",contains:[{className:"meta-string",begin:/"/,end:/"/}]},{className:"class",beginKeywords:"type",end:";",contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"\\S"},{className:"class",beginKeywords:"trait enum struct",end:"{",contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"[\\w\\d]"},{begin:a.IDENT_RE+
+"::",keywords:{built_in:"Copy Send Sized Sync Drop Fn FnMut FnOnce drop Box ToOwned Clone PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator Option Result SliceConcatExt String ToString Vec assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules!"}},
+{begin:"->"}]}});b.registerLanguage("scala",function(a){var b={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},e={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},g={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0};return{keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},
+contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[b],relevance:10}]},{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},e,{className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[g]},{className:"class",beginKeywords:"class object trait type",
+end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[e]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[e]},g]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("sql",function(a){var b=a.COMMENT("--","$");return{case_insensitive:!0,illegal:/[<>{}*#]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke",
+end:/;/,endsWithParent:!0,lexemes:/[\w\.]+/,keywords:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",
+literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE,{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE,{begin:'""'}]},{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE,b]},
+a.C_BLOCK_COMMENT_MODE,b]}});b.registerLanguage("swift",function(a){var b={keyword:"__COLUMN__ __FILE__ __FUNCTION__ __LINE__ as as! as? associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",
+literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},
+e=a.COMMENT("/\\*","\\*/",{contains:["self"]}),g={className:"subst",begin:/\\\(/,end:"\\)",keywords:b,contains:[]},d={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0},f=a.inherit(a.QUOTE_STRING_MODE,{contains:[g,a.BACKSLASH_ESCAPE]});g.contains=[d];return{keywords:b,contains:[f,a.C_LINE_COMMENT_MODE,e,{className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},d,{className:"function",beginKeywords:"func",end:"{",excludeEnd:!0,
+contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin:/</,end:/>/},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,contains:["self",d,f,a.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:b,end:"\\{",excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/})]},{className:"meta",begin:"(@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain)"},
+{beginKeywords:"import",end:/$/,contains:[a.C_LINE_COMMENT_MODE,e]}]}});b.registerLanguage("typescript",function(a){var b={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void"};
+return{aliases:["ts"],keywords:b,contains:[{className:"meta",begin:/^\s*['"]use strict['"]/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,{className:"subst",begin:"\\$\\{",end:"\\}"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",
+contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE],relevance:0},{className:"function",begin:"function",end:/[\{;]/,excludeEnd:!0,keywords:b,contains:["self",a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/["'\(]/}],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0},{begin:/module\./,keywords:{built_in:"module"},
+relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+a.IDENT_RE,relevance:0}]}});b.registerLanguage("yaml",function(a){var b={className:"attr",variants:[{begin:"^[ \\-]*[a-zA-Z_][\\w\\-]*:"},{begin:'^[ \\-]*"[a-zA-Z_][\\w\\-]*":'},{begin:"^[ \\-]*'[a-zA-Z_][\\w\\-]*':"}]},e={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/}],contains:[a.BACKSLASH_ESCAPE,
+{className:"template-variable",variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]};return{case_insensitive:!0,aliases:["yml","YAML","yaml"],contains:[b,{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>] *$",returnEnd:!0,contains:e.contains,end:b.variants[0].begin},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!!"+a.UNDERSCORE_IDENT_RE},{className:"meta",begin:"&"+a.UNDERSCORE_IDENT_RE+"$"},
+{className:"meta",begin:"\\*"+a.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"^ *-",relevance:0},e,a.HASH_COMMENT_MODE,a.C_NUMBER_MODE],keywords:{literal:"{ } true false yes no Yes No True False null"}}});return b});
diff --git a/lib/jgit/org.eclipse.jgit.archive/BUCK b/lib/jgit/org.eclipse.jgit.archive/BUCK
index 10ab2b0..384a5e0 100644
--- a/lib/jgit/org.eclipse.jgit.archive/BUCK
+++ b/lib/jgit/org.eclipse.jgit.archive/BUCK
@@ -4,7 +4,7 @@
 maven_jar(
   name = 'jgit-archive',
   id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
-  sha1 = 'c612e5bd40ebf6226032cb32c14b396d7ebfe036',
+  sha1 = '3f45cd199e40a7c68ee07a1743c06d1c3d07308a',
   license = 'jgit',
   repository = REPO,
   deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
diff --git a/lib/jgit/org.eclipse.jgit.http.server/BUCK b/lib/jgit/org.eclipse.jgit.http.server/BUCK
index 8ebc18df..2ade9ff 100644
--- a/lib/jgit/org.eclipse.jgit.http.server/BUCK
+++ b/lib/jgit/org.eclipse.jgit.http.server/BUCK
@@ -4,7 +4,7 @@
 maven_jar(
   name = 'jgit-servlet',
   id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
-  sha1 = 'bb01841b74a48abe506c2e44f238e107188e6c8f',
+  sha1 = 'fa67bf925001cfc663bf98772f37d5c5c1abd756',
   license = 'jgit',
   repository = REPO,
   deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
diff --git a/lib/jgit/org.eclipse.jgit.junit/BUCK b/lib/jgit/org.eclipse.jgit.junit/BUCK
index 4b06573..a31ee6f 100644
--- a/lib/jgit/org.eclipse.jgit.junit/BUCK
+++ b/lib/jgit/org.eclipse.jgit.junit/BUCK
@@ -4,7 +4,7 @@
 maven_jar(
   name = 'junit',
   id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
-  sha1 = '62dddedccdcd67b622d0d35a4bfb15c7eab8e171',
+  sha1 = 'dc7edb9c3060655c7fb93ab9b9349e815bab266f',
   license = 'DO_NOT_DISTRIBUTE',
   repository = REPO,
   unsign = True,
diff --git a/lib/jgit/org.eclipse.jgit/BUCK b/lib/jgit/org.eclipse.jgit/BUCK
index 0d19343..7c06726 100644
--- a/lib/jgit/org.eclipse.jgit/BUCK
+++ b/lib/jgit/org.eclipse.jgit/BUCK
@@ -4,8 +4,8 @@
 maven_jar(
   name = 'jgit',
   id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
-  bin_sha1 = 'dc4464c876cbf3815fd6cf6cb9d29d375566d6b1',
-  src_sha1 = 'ab3f9344d524f71c74307e68c82c698266e4bcec',
+  bin_sha1 = 'cd142b9030910babd119702f1c4eeae13ee90018',
+  src_sha1 = '3e65e476bfb4a529e18752ffcd27b566e7ee7241',
   license = 'jgit',
   repository = REPO,
   unsign = True,
diff --git a/lib/js/BUCK b/lib/js/BUCK
index 275941c..d6d8b3c 100644
--- a/lib/js/BUCK
+++ b/lib/js/BUCK
@@ -402,3 +402,18 @@
   sha1 = '8ba97a4a279ec6973a19b171c462a7b5cf454fb9',
 )
 
+# Zip highlightjs so that it can be imported as though it were a
+# bower_component and also attach the library license to the Buck dependency
+# graph.
+HLJS_DIR = 'bower_components/highlightjs'
+genrule(
+  name = 'highlightjs',
+  cmd = ' && '.join([
+    'mkdir -p %s' % HLJS_DIR,
+    'cp $(location //lib/highlightjs:highlightjs) %s/highlight.min.js' % HLJS_DIR,
+    'zip -r $OUT bower_components',
+  ]),
+  out = 'highlightjs.zip',
+  license = 'highlightjs',
+  visibility = ['PUBLIC'],
+)
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index 1cc51eb..c4a9872 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -1,6 +1,6 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '5.4.1'
+VERSION = '5.5.0'
 
 # core and backward-codecs both provide
 # META-INF/services/org.apache.lucene.codecs.Codec, so they must be merged.
@@ -16,7 +16,7 @@
 maven_jar(
   name = 'lucene-core',
   id = 'org.apache.lucene:lucene-core:' + VERSION,
-  sha1 = 'c52b2088e2c30dfd95fd296ab6fb9cf8de9855ab',
+  sha1 = 'a74fd869bb5ad7fe6b4cd29df9543a34aea81164',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
@@ -28,7 +28,7 @@
 maven_jar(
   name = 'lucene-analyzers-common',
   id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
-  sha1 = 'c2aa2c4e00eb9cdeb5ac00dc0495e70c441f681e',
+  sha1 = '1e0e8243a4410be20c34683034fafa7bb52e55cc',
   license = 'Apache2.0',
   deps = [':lucene-core-and-backward-codecs'],
   exclude = [
@@ -40,7 +40,7 @@
 maven_jar(
   name = 'backward-codecs_jar',
   id = 'org.apache.lucene:lucene-backward-codecs:' + VERSION,
-  sha1 = '5273da96380dfab302ad06c27fe58100db4c4e2f',
+  sha1 = '68480974b2f54f519763632a7c1c5d51cbff3805',
   license = 'Apache2.0',
   deps = [':lucene-core'],
   exclude = [
@@ -53,7 +53,7 @@
 maven_jar(
   name = 'lucene-misc',
   id = 'org.apache.lucene:lucene-misc:' + VERSION,
-  sha1 = '95f433b9d7dd470cc0aa5076e0f233907745674b',
+  sha1 = '504d855a1a38190622fdf990b2298c067e7d60ca',
   license = 'Apache2.0',
   deps = [':lucene-core-and-backward-codecs'],
   exclude = [
@@ -65,7 +65,7 @@
 maven_jar(
   name = 'lucene-queryparser',
   id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
-  sha1 = 'dccd5279bfa656dec21af444a7a66820eb1cd618',
+  sha1 = '0fddc49725b562fd48dff0cff004336ad2a090a4',
   license = 'Apache2.0',
   deps = [':lucene-core-and-backward-codecs'],
   exclude = [
diff --git a/lib/mina/BUCK b/lib/mina/BUCK
index 35488525..f22a710 100644
--- a/lib/mina/BUCK
+++ b/lib/mina/BUCK
@@ -10,6 +10,7 @@
   name = 'sshd',
   id = 'org.apache.sshd:sshd-core:1.2.0',
   sha1 = '4bc24a8228ba83dac832680366cf219da71dae8e',
+  src_sha1 = '490e3f03d7628ecf1cbb8317563fdbf06e68e29f',
   license = 'Apache2.0',
   deps = [':core'],
   exclude = EXCLUDE,
@@ -19,6 +20,7 @@
   name = 'core',
   id = 'org.apache.mina:mina-core:2.0.10',
   sha1 = 'a1cb1136b104219d6238de886bf5a3ea4554eb58',
+  src_sha1 = 'b70ff94ba379b4e825caca1af4ec83193fac4b10',
   license = 'Apache2.0',
   exclude = EXCLUDE,
 )
diff --git a/lib/ow2/BUCK b/lib/ow2/BUCK
index fabcb25..653bd2b 100644
--- a/lib/ow2/BUCK
+++ b/lib/ow2/BUCK
@@ -1,25 +1,25 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '5.0.3'
+VERSION = '5.1'
 
 maven_jar(
   name = 'ow2-asm',
   id = 'org.ow2.asm:asm:' + VERSION,
-  sha1 = 'dcc2193db20e19e1feca8b1240dbbc4e190824fa',
+  sha1 = '5ef31c4fe953b1fd00b8a88fa1d6820e8785bb45',
   license = 'ow2',
 )
 
 maven_jar(
   name = 'ow2-asm-analysis',
   id = 'org.ow2.asm:asm-analysis:' + VERSION,
-  sha1 = 'c7126aded0e8e13fed5f913559a0dd7b770a10f3',
+  sha1 = '6d1bf8989fc7901f868bee3863c44f21aa63d110',
   license = 'ow2',
 )
 
 maven_jar(
   name = 'ow2-asm-commons',
   id = 'org.ow2.asm:asm-commons:' + VERSION,
-  sha1 = 'a7111830132c7f87d08fe48cb0ca07630f8cb91c',
+  sha1 = '25d8a575034dd9cfcb375a39b5334f0ba9c8474e',
   deps = [':ow2-asm-tree'],
   license = 'ow2',
 )
@@ -27,14 +27,13 @@
 maven_jar(
   name = 'ow2-asm-tree',
   id = 'org.ow2.asm:asm-tree:' + VERSION,
-  sha1 = '287749b48ba7162fb67c93a026d690b29f410bed',
+  sha1 = '87b38c12a0ea645791ead9d3e74ae5268d1d6c34',
   license = 'ow2',
 )
 
 maven_jar(
   name = 'ow2-asm-util',
   id = 'org.ow2.asm:asm-util:' + VERSION,
-  sha1 = '1512e5571325854b05fb1efce1db75fcced54389',
+  sha1 = 'b60e33a6bd0d71831e0c249816d01e6c1dd90a47',
   license = 'ow2',
 )
-
diff --git a/plugins/BUCK b/plugins/BUCK
index 9948720..c6bb7f1 100644
--- a/plugins/BUCK
+++ b/plugins/BUCK
@@ -2,6 +2,7 @@
 CORE = [
   'commit-message-length-validator',
   'download-commands',
+  'hooks',
   'replication',
   'reviewnotes',
   'singleusergroup'
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index 69b8f9f..fc39c55 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 69b8f9f413ce83a71593a4068a3b8e81f684cbad
+Subproject commit fc39c552cffb94d15797d02e272fdc543c35b6bd
diff --git a/plugins/download-commands b/plugins/download-commands
index 7b41f3a..3fb4fb6 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 7b41f3a413b46140b050ae5324cbbcdd467d2b3a
+Subproject commit 3fb4fb63317b6004761d1fea98a8f4d288d95409
diff --git a/plugins/hooks b/plugins/hooks
new file mode 160000
index 0000000..c1705a7
--- /dev/null
+++ b/plugins/hooks
@@ -0,0 +1 @@
+Subproject commit c1705a739f117b9123e1d63aebf07d043afb0867
diff --git a/plugins/replication b/plugins/replication
index b9c11b4..75af773 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit b9c11b4d4ed37f566e6e2daa11d96e1ca3d23c02
+Subproject commit 75af77375b34133e85f3ee5f1b19dac19d3f3837
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 3f3d572..46079ec 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 3f3d572e9618f268b19cc54856deee4c96180e4c
+Subproject commit 46079ec92478ddc1e9ffd84eae22fb6af788c9fd
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index b355c90..3ca1167 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit b355c909345e3ccf5ebb139ded538e35cbdbb67c
+Subproject commit 3ca1167edda713f4bfdcecd9c0e2626797d7027f
diff --git a/polygerrit-ui/BUCK b/polygerrit-ui/BUCK
index 614e85c..e26e40c 100644
--- a/polygerrit-ui/BUCK
+++ b/polygerrit-ui/BUCK
@@ -4,6 +4,7 @@
   name = 'polygerrit_components',
   deps = [
     '//lib/js:fetch',
+    '//lib/js:highlightjs',
     '//lib/js:iron-autogrow-textarea',
     '//lib/js:iron-dropdown',
     '//lib/js:iron-input',
@@ -25,7 +26,6 @@
   ]),
   srcs = [
     '//lib/fonts:sourcecodepro',
-    '//lib/fonts:opensans',
   ],
   out = 'fonts.zip',
   visibility = ['PUBLIC'],
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 4ec5411..ad3fb36 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -50,13 +50,17 @@
 1. [Install Buck](https://gerrit-review.googlesource.com/Documentation/dev-buck.html#_installation)
    for building Gerrit.
 2. [Build Gerrit](https://gerrit-review.googlesource.com/Documentation/dev-buck.html#_gerrit_development_war_file)
-   and set up a [local test site](https://gerrit-review.googlesource.com/Documentation/dev-readme.html#init).
+   and set up a local test site. Docs
+   [here](https://gerrit-review.googlesource.com/Documentation/install-quick.html) and
+   [here](https://gerrit-review.googlesource.com/Documentation/dev-readme.html#init).
 
-Run a test server:
+When your project is set up and works using the classic UI, run a test server
+that serves PolyGerrit:
 
 ```sh
 buck build polygerrit && \
-java -jar buck-out/gen/polygerrit/polygerrit.war daemon --polygerrit-dev -d ../gerrit_testsite --console-log --show-stack-trace
+java -jar buck-out/gen/polygerrit/polygerrit.war daemon --polygerrit-dev \
+-d ../gerrit_testsite --console-log --show-stack-trace
 ```
 
 ## Running Tests
diff --git a/polygerrit-ui/app/BUCK b/polygerrit-ui/app/BUCK
index dfff22f..d03acf2 100644
--- a/polygerrit-ui/app/BUCK
+++ b/polygerrit-ui/app/BUCK
@@ -14,7 +14,21 @@
     'test/**',
   ] + WCT_TEST_PATTERNS + PY_TEST_PATTERNS)
 
-WEBJS = 'bower_components/webcomponentsjs/webcomponents-lite.js'
+# List libraries to be copied statically into the build. (i.e. Libraries not
+# expected to be Vulcanized.)
+WEB_JS_LIBS = [
+  ('bower_components/webcomponentsjs', 'webcomponents-lite.js'),
+  ('bower_components/highlightjs', 'highlight.min.js'),
+]
+
+# Map the static libraries to commands for the polygerrit_ui rule.
+JS_LIBS_MKDIR_CMDS = []
+JS_LIBS_UNZIP_CMDS = []
+for lib in WEB_JS_LIBS:
+  JS_LIBS_MKDIR_CMDS.append('mkdir -p ' + lib[0])
+  path = lib[0] + '/' + lib[1]
+  cmd = 'unzip -p $(location //polygerrit-ui:polygerrit_components) %s>%s' % (path, path)
+  JS_LIBS_UNZIP_CMDS.append(cmd)
 
 # TODO(dborowitz): Putting these rules in this package avoids having to handle
 # the app/ prefix like we would have to if this were in the parent directory.
@@ -26,11 +40,12 @@
   cmd = ' && '.join([
     'mkdir $TMP/polygerrit_ui',
     'cd $TMP/polygerrit_ui',
-    'mkdir -p {fonts,elements,bower_components/webcomponentsjs}',
+    'mkdir -p {fonts,elements}',
+    ' && '.join(JS_LIBS_MKDIR_CMDS),
     'unzip -qd fonts $(location //polygerrit-ui:fonts)',
     'unzip -qd elements $(location :gr-app)',
     'cp -rp $SRCDIR/* .',
-    'unzip -p $(location //polygerrit-ui:polygerrit_components) %s>%s' % (WEBJS, WEBJS),
+    ' && '.join(JS_LIBS_UNZIP_CMDS),
     'cd $TMP',
     'zip -9qr $OUT .',
   ]),
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior.html
index 08b25db..4def9b2 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior.html
@@ -18,7 +18,6 @@
 (function(window) {
   'use strict';
 
-
   /** @polymerBehavior Gerrit.RESTClientBehavior */
   var RESTClientBehavior = {
     ChangeDiffType: {
@@ -31,10 +30,10 @@
     },
 
     ChangeStatus: {
-      NEW: 'NEW',
-      MERGED: 'MERGED',
       ABANDONED: 'ABANDONED',
       DRAFT: 'DRAFT',
+      MERGED: 'MERGED',
+      NEW: 'NEW',
     },
 
     // Must be kept in sync with the ListChangesOption enum and protobuf.
@@ -109,6 +108,23 @@
       return status === this.ChangeStatus.NEW ||
           status === this.ChangeStatus.DRAFT;
     },
+
+    changeStatusString: function(change) {
+      // "Closed" states should take precedence over "open" ones.
+      if (change.status === this.ChangeStatus.MERGED) {
+        return 'Merged';
+      }
+      if (change.status === this.ChangeStatus.ABANDONED) {
+        return 'Abandoned';
+      }
+      if (change.mergeable === false) {
+        return 'Merge Conflict';
+      }
+      if (change.status === this.ChangeStatus.DRAFT) {
+        return 'Draft';
+      }
+      return '';
+    },
   };
 
   window.Gerrit = window.Gerrit || {};
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index 24f915a..9126785 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -75,7 +75,7 @@
       [[change._number]]
     </a>
     <a class="cell subject" href$="[[changeURL]]">[[change.subject]]</a>
-    <span class="cell status">[[_computeChangeStatusString(change)]]</span>
+    <span class="cell status">[[changeStatusString(change)]]</span>
     <span class="cell owner">
       <gr-account-link account="[[change.owner]]"></gr-account-link>
     </span>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index d160933..90b2e1d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -51,25 +51,6 @@
       return '/c/' + changeNum + '/';
     },
 
-    _computeChangeStatusString: function(change) {
-      // "Closed" states should take precedence over "open" ones.
-      if (change.status == this.ChangeStatus.MERGED) {
-        return 'Merged';
-      }
-      if (change.status == this.ChangeStatus.ABANDONED) {
-        return 'Abandoned';
-      }
-
-      if (change.mergeable != null && change.mergeable == false) {
-        return 'Merge Conflict';
-      }
-      if (change.status == this.ChangeStatus.DRAFT) {
-        return 'Draft';
-      }
-
-      return '';
-    },
-
     _computeLabelTitle: function(change, labelName) {
       var label = change.labels[labelName];
       if (!label) { return 'Label not applicable'; }
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 5507b9f..b7c0853 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -38,18 +38,21 @@
       element = fixture('basic');
     });
 
-    test('computed fields', function() {
-      assert.equal(element._computeChangeStatusString({mergeable: true}), '');
-      assert.equal(element._computeChangeStatusString({mergeable: false}),
-          'Merge Conflict');
-      assert.equal(element._computeChangeStatusString({status: 'NEW'}), '');
-      assert.equal(element._computeChangeStatusString({status: 'MERGED'}),
-          'Merged');
-      assert.equal(element._computeChangeStatusString({status: 'ABANDONED'}),
-          'Abandoned');
-      assert.equal(element._computeChangeStatusString({status: 'DRAFT'}),
-          'Draft');
+    test('change status', function() {
+      var getStatusForChange = function(change) {
+        element.change = change;
+        return element.$$('.cell.status').textContent;
+      };
 
+      assert.equal(getStatusForChange({mergeable: true}), '');
+      assert.equal(getStatusForChange({mergeable: false}), 'Merge Conflict');
+      assert.equal(getStatusForChange({status: 'NEW'}), '');
+      assert.equal(getStatusForChange({status: 'MERGED'}), 'Merged');
+      assert.equal(getStatusForChange({status: 'ABANDONED'}), 'Abandoned');
+      assert.equal(getStatusForChange({status: 'DRAFT'}), 'Draft');
+    });
+
+    test('computed fields', function() {
       assert.equal(element._computeLabelClass({labels: {}}),
           'cell label u-gray-background');
       assert.equal(element._computeLabelClass(
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
new file mode 100644
index 0000000..bb4a520
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
@@ -0,0 +1,42 @@
+<!--
+Copyright (C) 2016 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="../../shared/gr-autocomplete/gr-autocomplete.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<dom-module id="gr-account-entry">
+  <template>
+    <style>
+      gr-autocomplete {
+        display: inline-block;
+        flex: 1;
+        overflow: hidden;
+      }
+    </style>
+    <gr-autocomplete
+        id="input"
+        borderless="[[borderless]]"
+        placeholder="[[placeholder]]"
+        threshold="[[suggestFrom]]"
+        query="[[query]]"
+        on-commit="_handleInputCommit"
+        clear-on-commit>
+    </gr-autocomplete>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-account-entry.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
new file mode 100644
index 0000000..c5827d0
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
@@ -0,0 +1,88 @@
+// Copyright (C) 2016 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';
+
+  Polymer({
+    is: 'gr-account-entry',
+
+    /**
+     * Fired when an account is entered.
+     *
+     * @event add
+     */
+
+    properties: {
+      borderless: Boolean,
+      change: Object,
+      filter: Function,
+      placeholder: String,
+
+      suggestFrom: {
+        type: Number,
+        value: 3,
+      },
+
+      query: {
+        type: Function,
+        value: function() {
+          return this._getReviewerSuggestions.bind(this);
+        },
+      },
+    },
+
+    get focusStart() {
+      return this.$.input.focusStart;
+    },
+
+    focus: function() {
+      this.$.input.focus();
+    },
+
+    clear: function() {
+      this.$.input.clear();
+    },
+
+    _handleInputCommit: function(e) {
+      this.fire('add', {value: e.detail.value});
+    },
+
+    _makeSuggestion: function(reviewer) {
+      if (reviewer.account) {
+        return {
+          name: reviewer.account.name + ' (' + reviewer.account.email + ')',
+          value: reviewer,
+        };
+      } else if (reviewer.group) {
+        return {
+          name: reviewer.group.name + ' (group)',
+          value: reviewer,
+        };
+      }
+    },
+
+    _getReviewerSuggestions: function(input) {
+      var xhr = this.$.restAPI.getChangeSuggestedReviewers(
+          this.change._number, input);
+
+      return xhr.then(function(reviewers) {
+        if (!reviewers) { return []; }
+        if (!this.filter) { return reviewers.map(this._makeSuggestion); }
+        return reviewers
+            .filter(this.filter)
+            .map(this._makeSuggestion);
+      }.bind(this));
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
new file mode 100644
index 0000000..94db890
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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-account-entry</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-account-entry.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-account-entry></gr-account-entry>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-account-entry tests', function() {
+    var _nextAccountId = 0;
+    var makeAccount = function() {
+      var accountId = ++_nextAccountId;
+      return {
+        _account_id: accountId,
+        name: 'name ' + accountId,
+        email: 'email ' + accountId,
+      };
+    };
+
+    var owner;
+    var existingReviewer1;
+    var existingReviewer2;
+    var suggestion1;
+    var suggestion2;
+    var suggestion3;
+    var element;
+
+    setup(function() {
+      owner = makeAccount();
+      existingReviewer1 = makeAccount();
+      existingReviewer2 = makeAccount();
+      suggestion1 = {account: makeAccount()};
+      suggestion2 = {account: makeAccount()};
+      suggestion3 = {
+        group: {
+          id: 'suggested group id',
+          name: 'suggested group',
+        },
+      };
+
+      element = fixture('basic');
+      element.change = {
+        owner: owner,
+        reviewers: {
+          CC: [existingReviewer1],
+          REVIEWER: [existingReviewer2],
+        },
+      };
+
+      stub('gr-rest-api-interface', {
+        getChangeSuggestedReviewers: function() {
+          var redundantSuggestion1 = {account: existingReviewer1};
+          var redundantSuggestion2 = {account: existingReviewer2};
+          var redundantSuggestion3 = {account: owner};
+          return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
+              redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
+        },
+      });
+    });
+
+    test('_makeSuggestion formats account or group accordingly', function() {
+      var account = makeAccount();
+      var suggestion = element._makeSuggestion({account: account});
+      assert.deepEqual(suggestion, {
+        name: account.name + ' (' + account.email + ')',
+        value: {account: account},
+      });
+
+      var group = {name: 'test'};
+      suggestion = element._makeSuggestion({group: group});
+      assert.deepEqual(suggestion, {
+        name: group.name + ' (group)',
+        value: {group: group},
+      });
+    });
+
+    test('_getReviewerSuggestions excludes owner+reviewers', function(done) {
+      element._getReviewerSuggestions().then(function(reviewers) {
+        // Default is no filtering.
+        assert.equal(reviewers.length, 6);
+
+        // Set up filter that only accepts suggestion1.
+        var accountId = suggestion1.account._account_id;
+        element.filter = function(suggestion) {
+          return suggestion.account &&
+              suggestion.account._account_id === accountId;
+        };
+
+        element._getReviewerSuggestions().then(function(reviewers) {
+          assert.deepEqual(reviewers, [element._makeSuggestion(suggestion1)]);
+        }).then(done);
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
new file mode 100644
index 0000000..98f2b18
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
@@ -0,0 +1,59 @@
+<!--
+Copyright (C) 2016 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="../../shared/gr-account-chip/gr-account-chip.html">
+<link rel="import" href="../gr-account-entry/gr-account-entry.html">
+
+<dom-module id="gr-account-list">
+  <template>
+    <style>
+      gr-account-chip {
+        display: inline-block;
+        margin: 0 .2em .2em 0;
+      }
+      gr-account-entry {
+        display: flex;
+        flex: 1;
+        min-width: 10em;
+      }
+      .group {
+        --account-label-suffix: ' (group)';
+      }
+      .pending-add {
+        font-style: italic;
+      }
+    </style>
+    <template id="chips" is="dom-repeat" items="[[accounts]]" as="account">
+      <gr-account-chip
+          account="[[account]]"
+          class$="[[_computeChipClass(account)]]"
+          data-account-id$="[[account._account_id]]"
+          removable="[[_computeRemovable(account)]]">
+      </gr-account-chip>
+    </template>
+    <gr-account-entry
+        borderless
+        hidden$="[[readonly]]"
+        id="entry"
+        change="[[change]]"
+        filter="[[filter]]"
+        placeholder="[[placeholder]]"
+        on-add="_handleAdd">
+    </gr-account-entry>
+  </template>
+  <script src="gr-account-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
new file mode 100644
index 0000000..87d7116
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
@@ -0,0 +1,121 @@
+// Copyright (C) 2016 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';
+
+  Polymer({
+    is: 'gr-account-list',
+
+    properties: {
+      accounts: {
+        type: Array,
+        value: function() { return []; },
+      },
+      change: Object,
+      filter: Function,
+      placeholder: String,
+      pendingConfirmation: {
+        type: Object,
+        value: null,
+        notify: true,
+      },
+      readonly: Boolean,
+    },
+
+    listeners: {
+      'remove': '_handleRemove',
+    },
+
+    get focusStart() {
+      return this.$.entry.focusStart;
+    },
+
+    _handleAdd: function(e) {
+      var reviewer = e.detail.value;
+      // Append new account or group to the accounts property. We add our own
+      // internal properties to the account/group here, so we clone the object
+      // to avoid cluttering up the shared change object.
+      // TODO(logan): Polyfill for Object.assign in IE.
+      if (reviewer.account) {
+        var account = Object.assign({}, reviewer.account, {_pendingAdd: true});
+        this.push('accounts', account);
+      } else if (reviewer.group) {
+        if (reviewer.confirm) {
+          this.pendingConfirmation = reviewer;
+          return;
+        }
+        var group = Object.assign({}, reviewer.group,
+            {_pendingAdd: true, _group: true});
+        this.push('accounts', group);
+      }
+      this.pendingConfirmation = null;
+    },
+
+    confirmGroup: function(group) {
+      group = Object.assign(
+          {}, group, {confirmed: true, _pendingAdd: true, _group: true});
+      this.push('accounts', group);
+      this.pendingConfirmation = null;
+    },
+
+    _computeChipClass: function(account) {
+      var classes = [];
+      if (account._group) {
+        classes.push('group');
+      }
+      if (account._pendingAdd) {
+        classes.push('pendingAdd');
+      }
+      return classes.join(' ');
+    },
+
+    _computeRemovable: function(account) {
+      return !this.readonly && !!account._pendingAdd;
+    },
+
+    _handleRemove: function(e) {
+      var toRemove = e.detail.account;
+      for (var i = 0; i < this.accounts.length; i++) {
+        var matches;
+        var account = this.accounts[i];
+        if (toRemove._group) {
+          matches = toRemove.id === account.id;
+        } else {
+          matches = toRemove._account_id === account._account_id;
+        }
+        if (matches) {
+          this.splice('accounts', i, 1);
+          this.$.entry.focus();
+          return;
+        }
+      }
+      console.warn('received remove event for missing account',
+          e.detail.account);
+    },
+
+    additions: function() {
+      var result = [];
+      return this.accounts.filter(function(account) {
+        return account._pendingAdd;
+      }).map(function(account) {
+        if (account._group) {
+          return {group: account};
+        } else {
+          return {account: account};
+        }
+      });
+      return result;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
new file mode 100644
index 0000000..bb55d08
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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-account-list</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-account-list.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-account-list></gr-account-list>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-account-list tests', function() {
+    var _nextAccountId = 0;
+    var makeAccount = function() {
+      var accountId = ++_nextAccountId;
+      return {
+        _account_id: accountId,
+      };
+    };
+    var makeGroup = function() {
+      var groupId = 'group' + (++_nextAccountId);
+      return {
+        id: groupId,
+      };
+    };
+
+    var existingReviewer1;
+    var existingReviewer2;
+    var element;
+
+    function getChips() {
+      return Polymer.dom(element.root).querySelectorAll('gr-account-chip');
+    }
+
+    setup(function() {
+      existingReviewer1 = makeAccount();
+      existingReviewer2 = makeAccount();
+
+      element = fixture('basic');
+      element.accounts = [existingReviewer1, existingReviewer2];
+
+      stub('gr-rest-api-interface', {
+        getConfig: function() {
+          return Promise.resolve({});
+        },
+      });
+    });
+
+    test('account entry only appears when editable', function() {
+      element.readonly = false;
+      assert.isFalse(element.$.entry.hasAttribute('hidden'));
+      element.readonly = true;
+      assert.isTrue(element.$.entry.hasAttribute('hidden'));
+    });
+
+    test('addition and removal of account/group chips', function() {
+      flushAsynchronousOperations();
+
+      // Existing accounts are listed.
+      var chips = getChips();
+      assert.equal(chips.length, 2);
+      assert.isFalse(chips[0].classList.contains('pendingAdd'));
+      assert.isFalse(chips[1].classList.contains('pendingAdd'));
+
+      // New accounts are added to end with pendingAdd class.
+      var newAccount = makeAccount();
+      element._handleAdd({
+        detail: {
+          value: {
+            account: newAccount,
+          },
+        },
+      });
+      flushAsynchronousOperations();
+      chips = getChips();
+      assert.equal(chips.length, 3);
+      assert.isFalse(chips[0].classList.contains('pendingAdd'));
+      assert.isFalse(chips[1].classList.contains('pendingAdd'));
+      assert.isTrue(chips[2].classList.contains('pendingAdd'));
+
+      // Removed accounts are taken out of the list.
+      element.fire('remove', {account: existingReviewer1});
+      flushAsynchronousOperations();
+      chips = getChips();
+      assert.equal(chips.length, 2);
+      assert.isFalse(chips[0].classList.contains('pendingAdd'));
+      assert.isTrue(chips[1].classList.contains('pendingAdd'));
+
+      // Invalid remove is ignored.
+      element.fire('remove', {account: existingReviewer1});
+      element.fire('remove', {account: newAccount});
+      flushAsynchronousOperations();
+      chips = getChips();
+      assert.equal(chips.length, 1);
+      assert.isFalse(chips[0].classList.contains('pendingAdd'));
+
+      // New groups are added to end with pendingAdd and group classes.
+      var newGroup = makeGroup();
+      element._handleAdd({
+        detail: {
+          value: {
+            group: newGroup,
+          },
+        },
+      });
+      flushAsynchronousOperations();
+      chips = getChips();
+      assert.equal(chips.length, 2);
+      assert.isTrue(chips[1].classList.contains('group'));
+      assert.isTrue(chips[1].classList.contains('pendingAdd'));
+
+      // Removed groups are taken out of the list.
+      element.fire('remove', {account: newGroup});
+      flushAsynchronousOperations();
+      chips = getChips();
+      assert.equal(chips.length, 1);
+      assert.isFalse(chips[0].classList.contains('pendingAdd'));
+    });
+
+    test('_computeChipClass', function() {
+      var account = makeAccount();
+      assert.equal(element._computeChipClass(account), '');
+      account._pendingAdd = true;
+      assert.equal(element._computeChipClass(account), 'pendingAdd');
+      account._group = true;
+      assert.equal(element._computeChipClass(account), 'group pendingAdd');
+      account._pendingAdd = false;
+      assert.equal(element._computeChipClass(account), 'group');
+    });
+
+    test('_computeRemovable', function() {
+      var newAccount = makeAccount();
+      newAccount._pendingAdd = true;
+      element.readonly = false;
+      assert.isFalse(element._computeRemovable(existingReviewer1));
+      assert.isTrue(element._computeRemovable(newAccount));
+
+      element.readonly = true;
+      assert.isFalse(element._computeRemovable(existingReviewer1));
+      assert.isFalse(element._computeRemovable(newAccount));
+    });
+
+    test('additions returns sanitized new accounts and groups', function() {
+      assert.equal(element.additions().length, 0);
+
+      var newAccount = makeAccount();
+      element._handleAdd({
+        detail: {
+          value: {
+            account: newAccount,
+          },
+        },
+      });
+      var newGroup = makeGroup();
+      element._handleAdd({
+        detail: {
+          value: {
+            group: newGroup,
+          },
+        },
+      });
+
+      assert.deepEqual(element.additions(), [
+        {
+          account: {
+            _account_id: newAccount._account_id,
+            _pendingAdd: true,
+          },
+        },
+        {
+          group: {
+            id: newGroup.id,
+            _group: true,
+            _pendingAdd: true,
+          },
+        },
+      ]);
+    });
+
+    test('large group confirmations', function() {
+      assert.isNull(element.pendingConfirmation);
+      assert.deepEqual(element.additions(), []);
+
+      var group = makeGroup();
+      var reviewer = {
+        group: group,
+        count: 10,
+        confirm: true,
+      };
+      element._handleAdd({
+        detail: {
+          value: reviewer,
+        },
+      });
+
+      assert.deepEqual(element.pendingConfirmation, reviewer);
+      assert.deepEqual(element.additions(), []);
+
+      element.confirmGroup(group);
+      assert.isNull(element.pendingConfirmation);
+      assert.deepEqual(element.additions(), [
+        {
+          group: {
+            id: group.id,
+            _group: true,
+            _pendingAdd: true,
+            confirmed: true,
+          },
+        },
+      ]);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index b741784..1fe09e5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -32,19 +32,14 @@
   <template>
     <style>
       :host {
-        display: block;
+        display: inline-block;
+        font-family: var(--font-family);
       }
       section {
-        margin-top: 1em;
-      }
-      .groupLabel {
-        color: #666;
-        margin-bottom: .15em;
-        text-align: center;
+        display: inline-block;
       }
       gr-button {
-        display: block;
-        margin-bottom: .5em;
+        margin-left: .5em;
       }
       gr-button:before {
         content: attr(data-label);
@@ -53,6 +48,15 @@
         content: attr(data-loading-label);
       }
       @media screen and (max-width: 50em) {
+        :host,
+        section,
+        gr-button {
+          display: block;
+        }
+        gr-button {
+          margin-bottom: .5em;
+          margin-left: 0;
+        }
         .confirmDialog {
           width: 90vw;
         }
@@ -60,7 +64,6 @@
     </style>
     <div>
       <section hidden$="[[!_actionCount(actions.*, _additionalActions.*)]]">
-        <div class="groupLabel">Change</div>
         <template is="dom-repeat" items="[[_changeActionValues]]" as="action">
           <gr-button title$="[[action.title]]"
               primary$="[[action.__primary]]"
@@ -73,7 +76,6 @@
         </template>
       </section>
       <section hidden$="[[!_actionCount(_revisionActions.*, _additionalActions.*)]]">
-        <div class="groupLabel">Revision</div>
         <template is="dom-repeat" items="[[_revisionActionValues]]" as="action">
           <gr-button title$="[[action.title]]"
               primary$="[[action.__primary]]"
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 9783831..3445f4e 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
@@ -59,6 +59,7 @@
      */
 
     properties: {
+      change: Object,
       actions: {
         type: Object,
         value: function() { return {}; },
@@ -256,6 +257,11 @@
       return this.$.jsAPI.canSubmitChange();
     },
 
+    _modifyRevertMsg: function() {
+      return this.$.jsAPI.modifyRevertMsg(this.change,
+                                          this.$.confirmRevertDialog.message);
+    },
+
     _handleActionTap: function(e) {
       e.preventDefault();
       var el = Polymer.dom(e).rootTarget;
@@ -268,6 +274,8 @@
       if (type === ActionType.REVISION) {
         this._handleRevisionAction(key);
       } else if (key === ChangeActions.REVERT) {
+        this.$.confirmRevertDialog.populateRevertMessage();
+        this.$.confirmRevertDialog.message = this._modifyRevertMsg();
         this._showActionDialog(this.$.confirmRevertDialog);
       } else if (key === ChangeActions.ABANDON) {
         this._showActionDialog(this.$.confirmAbandonDialog);
@@ -396,11 +404,13 @@
           case RevisionActions.CHERRYPICK:
             page.show(this.changePath(obj._number));
             break;
-          case RevisionActions.DELETE:
-            page.show(this.changePath(this.changeNum));
-            break;
           case ChangeActions.DELETE:
-            page.show('/');
+          case RevisionActions.DELETE:
+            if (action.__type === ActionType.CHANGE) {
+              page.show('/');
+            } else {
+              page.show(this.changePath(this.changeNum));
+            }
             break;
           default:
             this.fire('reload-change', null, {bubbles: false});
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 462b01b..80aaf3b 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
@@ -240,7 +240,29 @@
         fireActionStub.restore();
       });
 
+      test('revert change with plugin hook', function(done) {
+        var newRevertMsg = 'Modified revert msg';
+        var modifyRevertMsgStub = sinon.stub(element, '_modifyRevertMsg',
+            function() { return newRevertMsg; });
+        var populateRevertMsgStub = sinon.stub(
+            element.$.confirmRevertDialog, 'populateRevertMessage',
+            function() { return 'original msg'; });
+        flush(function() {
+          var revertButton = element.$$('gr-button[data-action-key="revert"]');
+          MockInteractions.tap(revertButton);
+
+          assert.equal(element.$.confirmRevertDialog.message, newRevertMsg);
+
+          populateRevertMsgStub.restore();
+          modifyRevertMsgStub.restore();
+          done();
+        });
+      });
+
       test('works', function() {
+        var populateRevertMsgStub = sinon.stub(
+            element.$.confirmRevertDialog, 'populateRevertMessage',
+            function() { return 'original msg'; });
         var revertButton = element.$$('gr-button[data-action-key="revert"]');
         MockInteractions.tap(revertButton);
 
@@ -261,6 +283,7 @@
           '/revert', action, false, {
             message: 'foo message',
           }]);
+        populateRevertMsgStub.restore();
       });
     });
   });
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 a0a9305..8b51312 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
@@ -89,15 +89,36 @@
         <gr-account-link account="[[change.owner]]"></gr-account-link>
       </span>
     </section>
-    <section>
-      <span class="title">Reviewers</span>
-      <span class="value">
-        <gr-reviewer-list
-            change="[[change]]"
-            mutable="[[mutable]]"
-            suggest-from="[[serverConfig.suggest.from]]"></gr-reviewer-list>
-      </span>
-    </section>
+    <template is="dom-if" if="[[_showReviewersByState]]">
+      <section>
+        <span class="title">Reviewers</span>
+        <span class="value">
+          <gr-reviewer-list
+              change="{{change}}"
+              mutable="[[mutable]]"
+              reviewers-only></gr-reviewer-list>
+        </span>
+      </section>
+      <section>
+        <span class="title">CC</span>
+        <span class="value">
+          <gr-reviewer-list
+              change="{{change}}"
+              mutable="[[mutable]]"
+              ccs-only></gr-reviewer-list>
+        </span>
+      </section>
+    </template>
+    <template is="dom-if" if="[[!_showReviewersByState]]">
+      <section>
+        <span class="title">Reviewers</span>
+        <span class="value">
+          <gr-reviewer-list
+              change="{{change}}"
+              mutable="[[mutable]]"></gr-reviewer-list>
+        </span>
+      </section>
+    </template>
     <section>
       <span class="title">Project</span>
       <span class="value">[[change.project]]</span>
@@ -111,7 +132,7 @@
       <span class="value">
         <template is="dom-if" if="[[_showWebLink]]">
           <a target="_blank"
-              href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a>
+             href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a>
         </template>
         <template is="dom-if" if="[[!_showWebLink]]">
           [[_computeShortHash(commitInfo)]]
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 e24cc4a..af19703 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
@@ -42,6 +42,10 @@
         type: Boolean,
         computed: '_computeTopicReadOnly(mutable, change)',
       },
+      _showReviewersByState: {
+        type: Boolean,
+        computed: '_computeShowReviewersByState(serverConfig)',
+      },
     },
 
     behaviors: [
@@ -134,5 +138,9 @@
     _computeTopicPlaceholder: function(_topicReadOnly) {
       return _topicReadOnly ? 'No Topic' : 'Click to add topic';
     },
+
+    _computeShowReviewersByState: function(serverConfig) {
+      return !!serverConfig.note_db_enabled;
+    },
   });
 })();
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 a66d020..01f0649 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
@@ -140,5 +140,17 @@
       assert.equal(link, 'url-base/xx project-name xx commit-sha xx');
       assert.notEqual(link, '../../link-url');
     });
+
+    test('show CC section when NoteDb enabled', function() {
+      function hasCc() {
+        return element._showReviewersByState;
+      }
+
+      element.serverConfig = {};
+      assert.isFalse(hasCc());
+
+      element.serverConfig = {note_db_enabled: true};
+      assert.isTrue(hasCc());
+    });
   });
 </script>
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 79f20ab..cad4298 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
@@ -45,18 +45,16 @@
         color: #666;
         padding: 1em var(--default-horizontal-margin);
       }
-      .headerContainer {
-        height: 4.1em;
-        margin-bottom: .5em;
-      }
       .header {
         align-items: center;
         background-color: var(--view-background-color);
-        border-bottom: 1px solid #ddd;
         display: flex;
-        padding: 1em var(--default-horizontal-margin);
+        padding: .65em var(--default-horizontal-margin);
         z-index: 99;  /* Less than gr-overlay's backdrop */
       }
+      .header .download {
+        margin-right: 1em;
+      }
       .header.pinned {
         border-bottom-color: transparent;
         box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
@@ -69,24 +67,11 @@
         flex: 1;
         font-size: 1.2em;
         font-weight: bold;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        white-space: nowrap;
       }
       gr-change-star {
         margin-right: .25em;
         vertical-align: -.425em;
       }
-      .download,
-      .patchSelectLabel {
-        margin-left: 1em;
-      }
-      .header select {
-        margin-left: .5em;
-      }
-      .header .reply {
-        margin-left: var(--default-horizontal-margin);
-      }
       gr-reply-dialog {
         width: 50em;
       }
@@ -94,29 +79,19 @@
         color: #999;
         text-transform: capitalize;
       }
-      section {
-        margin: 10px 0;
-        padding: 10px var(--default-horizontal-margin);
-      }
       /* Strong specificity here is needed due to
          https://github.com/Polymer/polymer/issues/2531 */
       .container section.changeInfo {
-        border-bottom: 1px solid #ddd;
         display: flex;
-        margin-top: 0;
-        padding-top: 0;
+        padding: 0 var(--default-horizontal-margin);
       }
       .changeInfo-column:not(:last-of-type) {
         margin-right: 1em;
         padding-right: 1em;
       }
       .changeMetadata {
-        border-right: 1px solid #ddd;
         font-size: .9em;
       }
-      gr-change-actions {
-        margin-top: 1em;
-      }
       .commitMessage {
         font-family: var(--monospace-font-family);
         flex: 0 0 72ch;
@@ -124,15 +99,28 @@
         margin-bottom: 1em;
         overflow-x: hidden;
       }
-      .commitMessage h4 {
-        font-family: var(--font-family);
-        font-weight: bold;
-        margin-bottom: .25em;
-      }
       .commitMessage gr-linked-text {
         --linked-text-white-space: pre;
         overflow: auto;
       }
+      .editCommitMessage {
+        margin-top: 1em;
+      }
+      .commitActions {
+        border-bottom: 1px solid #ddd;
+        display: flex;
+        justify-content: space-between;
+        margin-bottom: .5em;
+        padding-bottom: .5em;
+      }
+      .reply {
+        margin-right: .5em;
+      }
+      .mainChangeInfo {
+        display: flex;
+        flex: 1;
+        flex-direction: column;
+      }
       .commitAndRelated {
         align-content: flex-start;
         display: flex;
@@ -144,14 +132,19 @@
         font-size: .9em;
         overflow: hidden;
       }
+      .patchInfo {
+        border: 1px solid #ddd;
+        margin: 1em var(--default-horizontal-margin);
+      }
+      .patchInfo-header,
       gr-file-list {
-        margin-bottom: 1em;
-        padding: 0 var(--default-horizontal-margin);
+        padding: .5em calc(var(--default-horizontal-margin) / 2);
+      }
+      .patchInfo-header {
+        background-color: #f6f6f6;
+        border-bottom: 1px solid #ebebeb;
       }
       @media screen and (max-width: 50em) {
-        .headerContainer {
-          height: 5.15em;
-        }
         .header {
           align-items: flex-start;
           flex-direction: column;
@@ -163,30 +156,17 @@
         .header-title {
           font-size: 1.1em;
         }
-        .header-actions {
-          align-items: center;
-          display: flex;
-          justify-content: space-between;
-          margin-top: .5em;
-        }
         gr-reply-dialog {
           min-width: initial;
           width: 90vw;
         }
-        .download {
+        .downloadContainer {
           display: none;
         }
-        .patchSelectLabel {
-          margin-left: 0;
-          margin-right: .5em;
-        }
-        .header select {
-          margin-left: 0;
-          margin-right: .5em;
-        }
-        .header .reply {
-          margin-left: 0;
-          margin-right: .5em;
+        .reply {
+          display: block;
+          margin-right: 0;
+          margin-bottom: .5em;
         }
         .changeInfo-column:not(:last-of-type) {
           margin-right: 0;
@@ -207,6 +187,9 @@
           margin-top: .25em;
           max-width: none;
         }
+        .commitActions {
+          flex-direction: column;
+        }
         .commitMessage {
           flex: initial;
           margin-right: 0;
@@ -215,85 +198,95 @@
     </style>
     <div class="container loading" hidden$="{{!_loading}}">Loading...</div>
     <div class="container" hidden$="{{_loading}}">
-      <div class="headerContainer">
-        <div class="header">
-          <span class="header-title">
-            <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
-            <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span>
-            <span>[[_change.subject]]</span>
-            <span class="changeStatus">[[_computeChangeStatus(_change, _patchRange.patchNum)]]</span>
-          </span>
-          <span class="header-actions">
-            <gr-button hidden
-                class="reply"
-                primary$="[[_computeReplyButtonHighlighted(_diffDrafts.*)]]"
-                hidden$="[[!_loggedIn]]"
-                on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
-            <gr-button class="download" on-tap="_handleDownloadTap">Download</gr-button>
-            <span>
-              <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
-              <select id="patchSetSelect" on-change="_handlePatchChange">
-                <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
-                  <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]">
-                    <span>[[patchNumber]]</span>
-                    /
-                    <span>[[_computeLatestPatchNum(_change)]]</span>
-                  </option>
-                </template>
-              </select>
-            </span>
-          </span>
-        </div>
+      <div class="header">
+        <span class="header-title">
+          <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
+          <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span>
+          <span>[[_change.subject]]</span>
+          <span class="changeStatus">[[_computeChangeStatus(_change, _patchRange.patchNum)]]</span>
+        </span>
       </div>
       <section class="changeInfo">
         <div class="changeInfo-column changeMetadata">
           <gr-change-metadata
-              change="[[_change]]"
+              change="{{_change}}"
               commit-info="[[_commitInfo]]"
               server-config="[[serverConfig]]"
-              mutable="[[_loggedIn]]"></gr-change-metadata>
-          <gr-change-actions id="actions"
-              actions="[[_change.actions]]"
-              change-num="[[_changeNum]]"
-              patch-num="[[_patchRange.patchNum]]"
-              commit-info="[[_commitInfo]]"
-              on-reload-change="_handleReloadChange"></gr-change-actions>
+              mutable="[[_loggedIn]]"
+              on-show-reply-dialog="_handleShowReplyDialog">
+          </gr-change-metadata>
         </div>
-        <div class="changeInfo-column commitAndRelated">
-          <div class="commitMessage">
-            <h4>
-              Commit message
+        <div class="changeInfo-column mainChangeInfo">
+          <div class="commitActions" hidden$="[[!_loggedIn]]"">
+            <gr-button
+                class="reply"
+                secondary
+                on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
+            <gr-change-actions id="actions"
+                change="[[_change]]"
+                actions="[[_change.actions]]"
+                change-num="[[_changeNum]]"
+                patch-num="[[_patchRange.patchNum]]"
+                commit-info="[[_commitInfo]]"
+                on-reload-change="_handleReloadChange"></gr-change-actions>
+          </div>
+          <div class="commitAndRelated">
+            <div class="commitMessage">
+              <gr-editable-content id="commitMessageEditor"
+                  editing="[[_editingCommitMessage]]"
+                  content="{{_latestCommitMessage}}">
+                <gr-linked-text pre
+                    content="[[_latestCommitMessage]]"
+                    config="[[_projectConfig.commentlinks]]"></gr-linked-text>
+              </gr-editable-content>
               <gr-button link
+                  class="editCommitMessage"
                   on-tap="_handleEditCommitMessage"
                   hidden$="[[_hideEditCommitMessage]]">Edit</gr-button>
-            </h4>
-            <gr-editable-content id="commitMessageEditor"
-                editing="[[_editingCommitMessage]]"
-                content="{{_commitInfo.message}}">
-              <gr-linked-text pre
-                  content="[[_commitInfo.message]]"
-                  config="[[_projectConfig.commentlinks]]"></gr-linked-text>
-            </gr-editable-content>
-          </div>
-          <div class="relatedChanges">
-            <gr-related-changes-list id="relatedChanges"
-                change="[[_change]]"
-                patch-num="[[_patchRange.patchNum]]"></gr-related-changes-list>
+            </div>
+            <div class="relatedChanges">
+              <gr-related-changes-list id="relatedChanges"
+                  change="[[_change]]"
+                  patch-num="[[_patchRange.patchNum]]"></gr-related-changes-list>
+            </div>
           </div>
         </div>
       </section>
-      <gr-file-list id="fileList"
-          change="[[_change]]"
-          change-num="[[_changeNum]]"
-          patch-range="[[_patchRange]]"
-          comments="[[_comments]]"
-          drafts="[[_diffDrafts]]"
-          revisions="[[_change.revisions]]"
-          projectConfig="[[_projectConfig]]"
-          selected-index="{{viewState.selectedFileIndex}}"></gr-file-list>
+      <section class="patchInfo">
+        <div class="patchInfo-header">
+          <span>
+            <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
+            <select id="patchSetSelect" on-change="_handlePatchChange">
+              <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
+                <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]">
+                  <span>[[patchNumber]]</span>
+                  /
+                  <span>[[_computeLatestPatchNum(_allPatchSets)]]</span>
+                </option>
+              </template>
+            </select>
+          </span>
+          <span class="downloadContainer">
+            /
+            <gr-button link
+                class="download"
+                on-tap="_handleDownloadTap">Download</gr-button>
+          </span>
+        </div>
+        <gr-file-list id="fileList"
+            change="[[_change]]"
+            change-num="[[_changeNum]]"
+            patch-range="[[_patchRange]]"
+            comments="[[_comments]]"
+            drafts="[[_diffDrafts]]"
+            revisions="[[_change.revisions]]"
+            projectConfig="[[_projectConfig]]"
+            selected-index="{{viewState.selectedFileIndex}}"></gr-file-list>
+      </section>
       <gr-messages-list id="messageList"
           change-num="[[_changeNum]]"
           messages="[[_change.messages]]"
+          reviewer-updates="[[_change.reviewer_updates]]"
           comments="[[_comments]]"
           project-config="[[_projectConfig]]"
           show-reply-buttons="[[_loggedIn]]"
@@ -308,17 +301,20 @@
           on-close="_handleDownloadDialogClose"></gr-download-dialog>
     </gr-overlay>
     <gr-overlay id="replyOverlay"
+        no-cancel-on-outside-click
         on-iron-overlay-opened="_handleReplyOverlayOpen"
         with-backdrop>
       <gr-reply-dialog id="replyDialog"
-          change-num="[[_changeNum]]"
+          change="[[_change]]"
           patch-num="[[_patchRange.patchNum]]"
           revisions="[[_change.revisions]]"
           labels="[[_change.labels]]"
           permitted-labels="[[_change.permitted_labels]]"
           diff-drafts="[[_diffDrafts]]"
+          server-config="[[serverConfig]]"
           on-send="_handleReplySent"
           on-cancel="_handleReplyCancel"
+          on-autogrow="_handleReplyAutogrow"
           hidden$="[[!_loggedIn]]">Reply</gr-reply-dialog>
     </gr-overlay>
     <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
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 f19d528..726e7e5 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
@@ -66,7 +66,11 @@
       _hideEditCommitMessage: {
         type: Boolean,
         computed: '_computeHideEditCommitMessage(_loggedIn, ' +
-            '_editingCommitMessage, _change.*, _patchRange.patchNum)',
+            '_editingCommitMessage)',
+      },
+      _latestCommitMessage: {
+        type: String,
+        value: '',
       },
       _patchRange: Object,
       _allPatchSets: {
@@ -78,8 +82,6 @@
         value: false,
       },
       _loading: Boolean,
-      _headerContainerEl: Object,
-      _headerEl: Object,
       _projectConfig: Object,
       _replyButtonLabel: {
         type: String,
@@ -95,12 +97,9 @@
 
     observers: [
       '_labelsChanged(_change.labels.*)',
+      '_paramsAndChangeChanged(params, _change)',
     ],
 
-    ready: function() {
-      this._headerEl = this.$$('.header');
-    },
-
     attached: function() {
       this._getLoggedIn().then(function(loggedIn) {
         this._loggedIn = loggedIn;
@@ -113,34 +112,6 @@
           this._handleCommitMessageSave.bind(this));
       this.addEventListener('editable-content-cancel',
           this._handleCommitMessageCancel.bind(this));
-      this.listen(window, 'scroll', '_handleBodyScroll');
-    },
-
-    detached: function() {
-      this.unlisten(window, 'scroll', '_handleBodyScroll');
-    },
-
-    _handleBodyScroll: function(e) {
-      var containerEl = this._headerContainerEl ||
-          this.$$('.headerContainer');
-
-      // Calculate where the header is relative to the window.
-      var top = containerEl.offsetTop;
-      for (var offsetParent = containerEl.offsetParent;
-           offsetParent;
-           offsetParent = offsetParent.offsetParent) {
-        top += offsetParent.offsetTop;
-      }
-      // The element may not be displayed yet, in which case do nothing.
-      if (top == 0) { return; }
-
-      this._headerEl.classList.toggle('pinned', window.scrollY >= top);
-    },
-
-    _resetHeaderEl: function() {
-      var el = this._headerEl || this.$$('.header');
-      this._headerEl = el;
-      el.classList.remove('pinned');
     },
 
     _handleEditCommitMessage: function(e) {
@@ -156,7 +127,7 @@
         this.$.commitMessageEditor.disabled = false;
         if (!resp.ok) { return; }
 
-        this.set('_commitInfo.message', message);
+        this._latestCommitMessage = message;
         this._editingCommitMessage = false;
         this._reloadWindow();
       }.bind(this)).catch(function(err) {
@@ -181,17 +152,8 @@
           }.bind(this));
     },
 
-    _computeHideEditCommitMessage: function(loggedIn, editing, changeRecord,
-        patchNum) {
-      if (!changeRecord || !loggedIn || editing) { return true; }
-
-      patchNum = parseInt(patchNum, 10);
-      if (isNaN(patchNum)) { return true; }
-
-      var change = changeRecord.base;
-      if (change.revisions[change.current_revision]._number !== patchNum) {
-        return true;
-      }
+    _computeHideEditCommitMessage: function(loggedIn, editing) {
+      if (!loggedIn || editing) { return true; }
 
       return false;
     },
@@ -265,14 +227,7 @@
     },
 
     _handlePatchChange: function(e) {
-      var patchNum = e.target.value;
-      var currentPatchNum =
-          this._change.revisions[this._change.current_revision]._number;
-      if (patchNum == currentPatchNum) {
-        page.show(this.changePath(this._changeNum));
-        return;
-      }
-      page.show(this.changePath(this._changeNum) + '/' + patchNum);
+      this._changePatchNum(parseInt(e.target.value, 10));
     },
 
     _handleReplyTap: function(e) {
@@ -310,6 +265,18 @@
       this.$.replyOverlay.close();
     },
 
+    _handleReplyAutogrow: function(e) {
+      this.$.replyOverlay.refit();
+    },
+
+    _handleShowReplyDialog: function(e) {
+      var target = this.$.replyDialog.FocusTarget.REVIEWERS;
+      if (e.detail.value && e.detail.value.ccsOnly) {
+        target = this.$.replyDialog.FocusTarget.CCS;
+      }
+      this._openReplyDialog(target);
+    },
+
     _paramsChanged: function(value) {
       if (value.view !== this.tagName.toLowerCase()) { return; }
 
@@ -319,19 +286,7 @@
         basePatchNum: value.basePatchNum || 'PARENT',
       };
 
-      // If the change number or patch range is different, then reset the
-      // selected file index.
-      var patchRangeState = this.viewState.patchRange;
-      if (this.viewState.changeNum !== this._changeNum ||
-          patchRangeState.basePatchNum !== this._patchRange.basePatchNum ||
-          patchRangeState.patchNum !== this._patchRange.patchNum) {
-        this._resetFileListViewState();
-      }
-
       this._reload().then(function() {
-        this.$.messageList.topMargin = this._headerEl.offsetHeight;
-        this.$.fileList.topMargin = this._headerEl.offsetHeight;
-
         // Allow the message list to render before scrolling.
         this.async(function() {
           this._maybeScrollToMessage();
@@ -346,6 +301,17 @@
       }.bind(this));
     },
 
+    _paramsAndChangeChanged: function(value) {
+      // If the change number or patch range is different, then reset the
+      // selected file index.
+      var patchRangeState = this.viewState.patchRange;
+      if (this.viewState.changeNum !== this._changeNum ||
+          patchRangeState.basePatchNum !== this._patchRange.basePatchNum ||
+          patchRangeState.patchNum !== this._patchRange.patchNum) {
+        this._resetFileListViewState();
+      }
+    },
+
     _maybeScrollToMessage: function() {
       var msgPrefix = '#message-';
       var hash = window.location.hash;
@@ -378,29 +344,50 @@
           this._patchRange.basePatchNum || 'PARENT');
       this.set('_patchRange.patchNum',
           this._patchRange.patchNum ||
-              change.revisions[change.current_revision]._number);
+              this._computeLatestPatchNum(this._allPatchSets));
 
       var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
       this.fire('title-change', {title: title});
     },
 
+    /**
+     * Change active patch to the provided patch num.
+     * @param {int} patchNum the patchn number to be viewed.
+     */
+    _changePatchNum: function(patchNum) {
+      var currentPatchNum;
+      if (this._change.current_revision) {
+        currentPatchNum =
+            this._change.revisions[this._change.current_revision]._number;
+      } else {
+        currentPatchNum = this._computeLatestPatchNum(this._allPatchSets);
+      }
+      if (patchNum === currentPatchNum) {
+        page.show(this.changePath(this._changeNum));
+        return;
+      }
+      page.show(this.changePath(this._changeNum) + '/' + patchNum);
+    },
+
     _computeChangePermalink: function(changeNum) {
       return '/' + changeNum;
     },
 
     _computeChangeStatus: function(change, patchNum) {
-      var status = change.status;
-      if (status == this.ChangeStatus.NEW) {
+      var statusString;
+      if (change.status === this.ChangeStatus.NEW) {
         var rev = this._getRevisionNumber(change, patchNum);
-        // TODO(davido): Figure out, why sometimes revision is not there
-        if (rev == undefined || !rev.draft) { return ''; }
-        status = this.ChangeStatus.DRAFT;
+        if (rev && rev.draft === true) {
+          statusString = 'Draft';
+        }
+      } else {
+        statusString = this.changeStatusString(change);
       }
-      return '(' + status.toLowerCase() + ')';
+      return statusString ? '(' + statusString + ')' : '';
     },
 
-    _computeLatestPatchNum: function(change) {
-      return change.revisions[change.current_revision]._number;
+    _computeLatestPatchNum: function(allPatchSets) {
+      return allPatchSets[allPatchSets.length - 1];
     },
 
     _computeAllPatchSets: function(change) {
@@ -454,11 +441,6 @@
       return result;
     },
 
-    _computeReplyButtonHighlighted: function(changeRecord) {
-      var drafts = (changeRecord && changeRecord.base) || {};
-      return Object.keys(drafts).length > 0;
-    },
-
     _computeReplyButtonLabel: function(changeRecord) {
       var drafts = (changeRecord && changeRecord.base) || {};
       var draftCount = Object.keys(drafts).reduce(function(count, file) {
@@ -472,15 +454,29 @@
       return label;
     },
 
+    _switchToMostRecentPatchNum: function() {
+      this._getChangeDetail().then(function() {
+        var patchNum = this._allPatchSets[this._allPatchSets.length - 1];
+        if (patchNum !== this._patchRange.patchNum) {
+          this._changePatchNum(patchNum);
+        }
+      }.bind(this));
+    },
+
     _handleKey: function(e) {
       if (this.shouldSupressKeyboardShortcut(e)) { return; }
-
       switch (e.keyCode) {
         case 65:  // 'a'
-          if (!this._loggedIn) { return; }
-
-          e.preventDefault();
-          this._openReplyDialog();
+          if (this._loggedIn && !e.shiftKey) {
+            e.preventDefault();
+            this._openReplyDialog();
+          }
+          break;
+        case 82: // 'r'
+          if (e.shiftKey) {
+            e.preventDefault();
+            this._switchToMostRecentPatchNum();
+          }
           break;
         case 85:  // 'u'
           e.preventDefault();
@@ -496,9 +492,10 @@
       });
     },
 
-    _openReplyDialog: function() {
+    _openReplyDialog: function(opt_section) {
       this.$.replyOverlay.open().then(function() {
         this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
+        this.$.replyDialog.open(opt_section);
       }.bind(this));
     },
 
@@ -534,6 +531,9 @@
               function(change) {
                 // Issue 4190: Coalesce missing topics to null.
                 if (!change.topic) { change.topic = null; }
+                if (!change.reviewer_updates) {
+                  change.reviewer_updates = null;
+                }
                 this._change = change;
               }.bind(this));
     },
@@ -545,6 +545,14 @@
           }.bind(this));
     },
 
+    _getLatestCommitMessage: function() {
+      return this.$.restAPI.getChangeCommitInfo(this._changeNum,
+          this._computeLatestPatchNum(this._allPatchSets)).then(
+              function(commitInfo) {
+                this._latestCommitMessage = commitInfo.message;
+              }.bind(this));
+    },
+
     _getCommitInfo: function() {
       return this.$.restAPI.getChangeCommitInfo(
           this._changeNum, this._patchRange.patchNum).then(
@@ -587,13 +595,12 @@
         if (!this._change) { return Promise.resolve(); }
 
         return Promise.all([
+          this._getLatestCommitMessage(),
           this.$.relatedChanges.reload(),
           this._getProjectConfig(),
         ]);
       }.bind(this);
 
-      this._resetHeaderEl();
-
       if (this._patchRange.patchNum) {
         return reloadPatchNumDependentResources().then(function() {
           return detailCompletes;
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 49e0239..7f9fb02 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
@@ -43,41 +43,89 @@
       element = fixture('basic');
     });
 
-    test('keyboard shortcuts', function() {
-      var showStub = sinon.stub(page, 'show');
+    suite('keyboard shortcuts', function() {
+      test('U should navigate to /', function() {
+        var showStub = sinon.stub(page, 'show');
+        MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'U'
+        assert(showStub.lastCall.calledWithExactly('/'));
+        showStub.restore();
+      });
 
-      MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'u'
-      assert(showStub.lastCall.calledWithExactly('/'),
-          'Should navigate to /');
-      showStub.restore();
+      test('A should toggle overlay', function() {
+        MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'A'
+        var overlayEl = element.$.replyOverlay;
+        assert.isFalse(overlayEl.opened);
+        element._loggedIn = true;
 
-      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'a'
-      var overlayEl = element.$.replyOverlay;
-      assert.isFalse(overlayEl.opened);
-      element._loggedIn = true;
+        MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift');  // 'A'
+        assert.isFalse(overlayEl.opened);
 
-      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'a'
-      assert.isTrue(overlayEl.opened);
-      overlayEl.close();
-      assert.isFalse(overlayEl.opened);
+        MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'A'
+        assert.isTrue(overlayEl.opened);
+        overlayEl.close();
+        assert.isFalse(overlayEl.opened);
+      });
+
+      test('shift + R should fetch and navigate to the latest patch set',
+          function(done) {
+        element._changeNum = '42';
+        element._patchRange = {
+          basePatchNum: 'PARENT',
+          patchNum: 1,
+        };
+        element._change = {
+          change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+          revisions: {
+            rev1: {_number: 1},
+          },
+          current_revision: 'rev1',
+          status: 'NEW',
+          labels: {},
+          actions: {},
+        };
+
+        sinon.stub(element.$.restAPI, '_getChangeDetail', function() {
+          // Mock change obj.
+          return Promise.resolve({
+            change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+            revisions: {
+              rev1: {_number: 1},
+              rev13: {_number: 13},
+            },
+            current_revision: 'rev1',
+            status: 'NEW',
+            labels: {},
+            actions: {},
+          });
+        });
+
+        var showStub = sinon.stub(page, 'show', function(arg) {
+          assert.equal(arg, '/c/42/13');
+          showStub.restore();
+          element.$.restAPI._getChangeDetail.restore();
+          done();
+        });
+
+        // 'shift + R'
+        MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift');
+      });
     });
 
-    test('reply button is highlighted when there are drafts', function() {
+    test('reply button has updated count when there are drafts', function() {
       var replyButton = element.$$('gr-button.reply');
       assert.ok(replyButton);
-      assert.isFalse(replyButton.hasAttribute('primary'));
+      assert.equal(replyButton.textContent, 'Reply');
 
       element._diffDrafts = null;
-      assert.isFalse(replyButton.hasAttribute('primary'));
+      assert.equal(replyButton.textContent, 'Reply');
 
       element._diffDrafts = {};
-      assert.isFalse(replyButton.hasAttribute('primary'));
+      assert.equal(replyButton.textContent, 'Reply');
 
       element._diffDrafts = {
         'file1.txt': [{}],
         'file2.txt': [{}, {}],
       };
-      assert.isTrue(replyButton.hasAttribute('primary'));
       assert.equal(replyButton.textContent, 'Reply (3)');
     });
 
@@ -136,17 +184,17 @@
         labels: {},
       };
       flushAsynchronousOperations();
-      var selectEl = element.$$('.header select');
+      var selectEl = element.$$('.patchInfo-header select');
       assert.ok(selectEl);
-      var optionEls =
-          Polymer.dom(element.root).querySelectorAll('.header option');
+      var optionEls = Polymer.dom(element.root).querySelectorAll(
+          '.patchInfo-header option');
       assert.equal(optionEls.length, 4);
-      assert.isFalse(
-          element.$$('.header option[value="1"]').hasAttribute('selected'));
-      assert.isTrue(
-          element.$$('.header option[value="2"]').hasAttribute('selected'));
-      assert.isFalse(
-          element.$$('.header option[value="3"]').hasAttribute('selected'));
+      assert.isFalse(element.$$('.patchInfo-header option[value="1"]')
+          .hasAttribute('selected'));
+      assert.isTrue(element.$$('.patchInfo-header option[value="2"]')
+          .hasAttribute('selected'));
+      assert.isFalse(element.$$('.patchInfo-header option[value="3"]')
+          .hasAttribute('selected'));
       assert.equal(optionEls[3].value, 13);
 
       var showStub = sinon.stub(page, 'show');
@@ -170,6 +218,58 @@
       element.fire('change', {}, {node: selectEl});
     });
 
+    test('patch num change with missing current_revision', function(done) {
+      element._changeNum = '42';
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 2,
+      };
+      element._change = {
+        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+        revisions: {
+          rev2: {_number: 2},
+          rev1: {_number: 1},
+          rev13: {_number: 13},
+          rev3: {_number: 3},
+        },
+        status: 'NEW',
+        labels: {},
+      };
+      flushAsynchronousOperations();
+      var selectEl = element.$$('.patchInfo-header select');
+      assert.ok(selectEl);
+      var optionEls = Polymer.dom(element.root).querySelectorAll(
+          '.patchInfo-header option');
+      assert.equal(optionEls.length, 4);
+      assert.isFalse(element.$$('.patchInfo-header option[value="1"]')
+          .hasAttribute('selected'));
+      assert.isTrue(element.$$('.patchInfo-header option[value="2"]')
+          .hasAttribute('selected'));
+      assert.isFalse(element.$$('.patchInfo-header option[value="3"]')
+          .hasAttribute('selected'));
+      assert.equal(optionEls[3].value, 13);
+
+      var showStub = sinon.stub(page, 'show');
+
+      var numEvents = 0;
+      selectEl.addEventListener('change', function(e) {
+        numEvents++;
+        if (numEvents == 1) {
+          assert(showStub.lastCall.calledWithExactly('/c/42/1'),
+              'Should navigate to /c/42/1');
+          selectEl.value = '3';
+          element.fire('change', {}, {node: selectEl});
+        } else if (numEvents == 2) {
+          assert(showStub.lastCall.calledWithExactly('/c/42/3'),
+              'Should navigate to /c/42/3');
+          showStub.restore();
+          done();
+        }
+      });
+      selectEl.value = '1';
+      element.fire('change', {}, {node: selectEl});
+    });
+
     test('change status new', function() {
       element._changeNum = '1';
       element._patchRange = {
@@ -205,7 +305,7 @@
         labels: {},
       };
       var status = element._computeChangeStatus(element._change, '1');
-      assert.equal(status, '(draft)');
+      assert.equal(status, '(Draft)');
     });
 
     test('revision status draft', function() {
@@ -228,38 +328,41 @@
         labels: {},
       };
       var status = element._computeChangeStatus(element._change, '2');
-      assert.equal(status, '(draft)');
+      assert.equal(status, '(Draft)');
     });
 
     test('show commit message edit button', function() {
-      var changeRecord = {
-        base: {
-          revisions: {
-            rev1: {_number: 1},
-            rev2: {_number: 2},
-          },
-          current_revision: 'rev2',
-        },
-      };
-      assert.isTrue(element._computeHideEditCommitMessage(
-          false, false, changeRecord, '2'));
-      assert.isTrue(element._computeHideEditCommitMessage(
-          true, true, changeRecord, '2'));
-      assert.isTrue(element._computeHideEditCommitMessage(
-          true, false, changeRecord, '1'));
-      assert.isFalse(element._computeHideEditCommitMessage(
-          true, false, changeRecord, '2'));
+      assert.isTrue(element._computeHideEditCommitMessage(false, false));
+      assert.isTrue(element._computeHideEditCommitMessage(true, true));
+      assert.isTrue(element._computeHideEditCommitMessage(false, true));
+      assert.isFalse(element._computeHideEditCommitMessage(true, false));
     });
 
-    test('topic is coalesced to null', function() {
+    test('topic is coalesced to null', function(done) {
       sinon.stub(element, '_changeChanged');
-      sinon.stub(element.$.restAPI, 'getChangeDetail', function(num) {
+      sinon.stub(element.$.restAPI, 'getChangeDetail', function() {
         return Promise.resolve({id: '123456789', labels: {}});
       });
 
       element._getChangeDetail().then(function() {
         assert.isNull(element._change.topic);
+        done();
       });
     });
+
+    test('reply dialog focus can be controlled', function() {
+      var FocusTarget = element.$.replyDialog.FocusTarget;
+      var openSpy = sinon.spy(element, '_openReplyDialog');
+
+      var e = {detail: {}};
+      element._handleShowReplyDialog(e);
+      assert(openSpy.lastCall.calledWithExactly(FocusTarget.REVIEWERS),
+          '_openReplyDialog should have been passed REVIEWERS');
+
+      e.detail.value = {ccsOnly: true};
+      element._handleShowReplyDialog(e);
+      assert(openSpy.lastCall.calledWithExactly(FocusTarget.CCS),
+          '_openReplyDialog should have been passed CCS');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index ac998a4..a7d99a7 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -21,6 +21,7 @@
     <style>
       :host {
         display: block;
+        font-family: var(--monospace-font-family);
       }
       .file {
         border-top: 1px solid #ddd;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index ff0fc32..b4baa26 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -32,24 +32,26 @@
     properties: {
       branch: String,
       message: String,
-      commitInfo: {
-        type: Object,
-        observer: '_commitInfoChanged',
-      },
+      commitInfo: Object,
     },
 
-    _commitInfoChanged: function(commitInfo) {
-      // Strip 'Change-Id: xxx'
-      var commitMessage = commitInfo.message.replace(
-          /\n{1,2}\nChange-Id: \w+\n/gm, '');
-      var revertCommitText = 'This reverts commit ';
-      // Selector for previous revert text and commit.
-      var previousRevertText =
-          new RegExp('\n{1,2}' + revertCommitText + '\\w+.\n*', 'gm');
-      commitMessage = commitMessage.replace(previousRevertText, '');
-      this.message = 'Revert "' + commitMessage + '"\n\n' +
-          revertCommitText + commitInfo.commit + '.\n\n' +
-          'Reason for revert: <INSERT REASONING HERE>\n\n';
+    populateRevertMessage: function() {
+      // Figure out what the revert title should be.
+      var originalTitle = this.commitInfo.message.split('\n')[0];
+      var revertTitle = 'Revert of ' + originalTitle;
+      if (originalTitle.startsWith('Revert of ')) {
+        revertTitle = 'Reland of ' +
+                      originalTitle.substring('Revert of '.length);
+      } else if (originalTitle.startsWith('Reland of ')) {
+        revertTitle = 'Revert of ' +
+                      originalTitle.substring('Reland of '.length);
+      }
+      // Add '> ' in front of the original commit text.
+      var originalCommitText = this.commitInfo.message.replace(/^/gm, '> ');
+
+      this.message = revertTitle + '\n\n' +
+                     'Reason for revert: <INSERT REASONING HERE>\n\n' +
+                     'Original issue\'s description:\n' + originalCommitText;
     },
 
     _handleConfirmTap: function(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
new file mode 100644
index 0000000..1d53eef
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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-confirm-revert-dialog</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-confirm-revert-dialog.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-confirm-revert-dialog></gr-confirm-revert-dialog>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-confirm-revert-dialog tests', function() {
+    var element;
+
+    setup(function() {
+      element = fixture('basic');
+    });
+
+    test('single line', function() {
+      assert.isNotOk(element.message);
+      element.commitInfo = {message: 'one line commit'};
+      assert.isNotOk(element.message);
+      element.populateRevertMessage();
+      var expected = 'Revert of one line commit\n\n' +
+                     'Reason for revert: <INSERT REASONING HERE>\n\n' +
+                     'Original issue\'s description:\n' +
+                     '> one line commit';
+      assert.equal(element.message, expected);
+    });
+
+    test('multi line', function() {
+      assert.isNotOk(element.message);
+      element.commitInfo = {message: 'many lines\ncommit\n\nmessage\n'};
+      assert.isNotOk(element.message);
+      element.populateRevertMessage();
+      var expected = 'Revert of many lines\n\n' +
+                     'Reason for revert: <INSERT REASONING HERE>\n\n' +
+                     'Original issue\'s description:\n' +
+                     '> many lines\n> commit\n> \n> message\n> ';
+      assert.equal(element.message, expected);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index 61fa802..70e934d 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -159,12 +159,13 @@
 
   suite('gr-download-dialog tests', function() {
     var element;
-    var getPrefsStub;
 
     setup(function() {
-      stub('gr-rest-api-interface', { getPreferences: function() {
-        return Promise.resolve({download_scheme: 'repo'});
-      }});
+      stub('gr-rest-api-interface', {
+        getPreferences: function() {
+          return Promise.resolve({download_scheme: 'repo'});
+        },
+      });
 
       element = fixture('loggedIn');
       element.change = getChangeObject();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 11c160f..201671d 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -176,15 +176,12 @@
           patch-range="[[patchRange]]"
           path="[[file.__path]]"
           prefs="[[_diffPrefs]]"
-          has-ranged-comments="[[_localPrefs.ranged_comments]]"
           project-config="[[projectConfig]]"
           view-mode="[[_userPrefs.diff_view]]"></gr-diff>
     </template>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
-    <gr-diff-cursor
-        id="cursor"
-        fold-offset-top="[[topMargin]]"></gr-diff-cursor>
+    <gr-diff-cursor id="cursor"></gr-diff-cursor>
   </template>
   <script src="gr-file-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index b9ad8c9..7bbeab1 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -27,7 +27,6 @@
       drafts: Object,
       revisions: Object,
       projectConfig: Object,
-      topMargin: Number,
       selectedIndex: {
         type: Number,
         notify: true,
@@ -303,6 +302,14 @@
             }
           }
           break;
+        case 65:  // 'a'
+          if (e.shiftKey) { // Hide left diff.
+            e.preventDefault();
+            this._forEachDiff(function(diff) {
+              diff.toggleLeftDiff();
+            });
+          }
+          break;
       }
     },
 
@@ -344,7 +351,7 @@
       }
 
       // Don't scroll if it's already in view.
-      if (top > window.pageYOffset + this.topMargin &&
+      if (top > window.pageYOffset &&
           top < window.pageYOffset + window.innerHeight - el.clientHeight) {
         return;
       }
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 055f961..f61566a 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -77,6 +77,15 @@
       });
     });
 
+    test('toggle left diff via shortcut', function() {
+      var toggleLeftDiffStub = sinon.stub();
+      sinon.stub(element, 'diffs', {get: function() {
+        return [{toggleLeftDiff: toggleLeftDiffStub}];
+      }});
+      MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift');  // 'A'
+      assert.isTrue(toggleLeftDiffStub.calledOnce);
+    });
+
     test('keyboard shortcuts', function() {
       var toggleInlineDiffsStub = sinon.stub(element, '_toggleInlineDiffs');
       MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift');  // 'I'
@@ -102,25 +111,25 @@
       assert.isTrue(elementItems[0].hasAttribute('selected'));
       assert.isFalse(elementItems[1].hasAttribute('selected'));
       assert.isFalse(elementItems[2].hasAttribute('selected'));
-      MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
+      MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'J'
       assert.equal(element.selectedIndex, 1);
-      MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
+      MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'J'
 
       var showStub = sinon.stub(page, 'show');
       assert.equal(element.selectedIndex, 2);
-      MockInteractions.pressAndReleaseKeyOn(element, 13);  // 'enter'
+      MockInteractions.pressAndReleaseKeyOn(element, 13);  // 'ENTER'
       assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
           'Should navigate to /c/42/2/myfile.txt');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
+      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
       assert.equal(element.selectedIndex, 1);
-      MockInteractions.pressAndReleaseKeyOn(element, 79);  // 'o'
+      MockInteractions.pressAndReleaseKeyOn(element, 79);  // 'O'
       assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'),
           'Should navigate to /c/42/2/file_added_in_rev2.txt');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
-      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
-      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
+      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
+      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
+      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
       assert.equal(element.selectedIndex, 0);
 
       showStub.restore();
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index 7c287db..66254d0 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -50,15 +50,15 @@
       }
       .showAvatar.collapsed .contentContainer {
         margin-left: calc(var(--default-horizontal-margin) + 1.75em);
-        padding: 10px 75px 10px 0;
+        padding: .75em 2em .75em 0;
       }
       .hideAvatar.collapsed .contentContainer,
       .hideAvatar.expanded .contentContainer {
         margin-left: 0;
-        padding: 10px 75px 10px 0;
+        padding: .75em 2em .75em 0;
       }
       .collapsed gr-avatar {
-        top: 8px;
+        top: .5em;
         height: 1.75em;
         width: 1.75em;
       }
@@ -75,7 +75,8 @@
       }
       .collapsed .name,
       .collapsed .content,
-      .collapsed .message {
+      .collapsed .message,
+      gr-account-chip {
         display: inline;
       }
       .collapsed gr-comment-list,
@@ -99,23 +100,34 @@
       }
     </style>
     <div class$="[[_computeClass(expanded, showAvatar)]]">
-      <gr-avatar account="[[message.author]]" image-size="100"></gr-avatar>
+      <gr-avatar account="[[author]]" image-size="100"></gr-avatar>
       <div class="contentContainer">
-        <div class="name" on-tap="_handleNameTap">[[message.author.name]]</div>
-        <div class="content">
-          <gr-linked-text class="message"
-              pre="[[expanded]]"
-              content="[[message.message]]"
-              disabled="[[!expanded]]"
-              config="[[projectConfig.commentlinks]]"></gr-linked-text>
-          <gr-comment-list
-              comments="[[comments]]"
-              change-num="[[changeNum]]"
-              patch-num="[[message._revision_number]]"></gr-comment-list>
-        </div>
-        <a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap">
-          <gr-date-formatter date-str="[[message.date]]"></gr-date-formatter>
-        </a>
+        <div class="name" on-tap="_handleNameTap">[[author.name]]</div>
+        <template is="dom-if" if="[[message.message]]">
+          <div class="content">
+            <gr-linked-text
+                class="message"
+                pre="[[expanded]]"
+                content="[[message.message]]"
+                disabled="[[!expanded]]"
+                config="[[projectConfig.commentlinks]]"></gr-linked-text>
+            <gr-comment-list
+                comments="[[comments]]"
+                change-num="[[changeNum]]"
+                patch-num="[[message._revision_number]]"></gr-comment-list>
+          </div>
+          <a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap">
+            <gr-date-formatter date-str="[[message.date]]"></gr-date-formatter>
+          </a>
+        </template>
+        <template is="dom-if" if="[[message.reviewer]]">
+          set reviewer status for
+          <gr-account-chip account="[[message.reviewer]]">
+          </gr-account-chip>
+          to [[message.state]].
+          <gr-date-formatter class="date" date-str="[[message.updated]]">
+          </gr-date-formatter>
+        </template>
       </div>
       <div class="replyContainer" hidden$="[[!showReplyButton]]" hidden>
         <gr-button small on-tap="_handleReplyTap">Reply</gr-button>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 26b9fb9..c92ad07 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -36,10 +36,15 @@
     properties: {
       changeNum: Number,
       message: Object,
+      author: {
+        type: Object,
+        computed: '_computeAuthor(message)',
+      },
       comments: {
         type: Object,
         observer: '_commentsChanged',
       },
+      config: Object,
       expanded: {
         type: Boolean,
         value: true,
@@ -47,22 +52,33 @@
       },
       showAvatar: {
         type: Boolean,
-        value: false,
+        computed: '_computeShowAvatar(author, config)',
       },
       showReplyButton: {
         type: Boolean,
-        value: false,
+        computed: '_computeShowReplyButton(message)',
       },
       projectConfig: Object,
     },
 
     ready: function() {
-      this.$.restAPI.getConfig().then(function(cfg) {
-        this.showAvatar = !!(cfg && cfg.plugin && cfg.plugin.has_avatars) &&
-            this.message && this.message.author;
+      this.$.restAPI.getConfig().then(function(config) {
+        this.config = config;
       }.bind(this));
     },
 
+    _computeAuthor: function(message) {
+      return message.author || message.updated_by;
+    },
+
+    _computeShowAvatar: function(author, config) {
+      return !!(author && config && config.plugin && config.plugin.has_avatars);
+    },
+
+    _computeShowReplyButton: function(message) {
+      return !!message.message;
+    },
+
     _commentsChanged: function(value) {
       this.expanded = Object.keys(value || {}).length > 0;
     },
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 7302cf2..c90f58a 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -56,8 +56,34 @@
         assert.deepEqual(e.detail.message, element.message);
         done();
       });
+      flushAsynchronousOperations();
       MockInteractions.tap(element.$$('.replyContainer gr-button'));
     });
 
+    test('reviewer update', function() {
+      var updatedBy = {
+        _account_id: 1115495,
+        name: 'Andrew Bonventre',
+        email: 'andybons@chromium.org',
+      };
+      var reviewer = {
+        _account_id: 123456,
+        name: 'Foo Bar',
+        email: 'barbar@chromium.org',
+      };
+      element.message = {
+        updated_by: updatedBy,
+        reviewer: reviewer,
+        state: 'CC',
+        updated: '2016-01-12 20:24:49.448000000',
+      };
+      flushAsynchronousOperations();
+      var content = element.$$('.contentContainer');
+      assert.isOk(content);
+      assert.strictEqual(
+          content.querySelector('gr-account-chip').account, reviewer);
+      assert.equal(0, content.textContent.trim().indexOf(updatedBy.name));
+    });
+
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
index 8a66d03..3ae6b44 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -47,11 +47,14 @@
         [[_computeExpandCollapseMessage(_expanded)]]
       </gr-button>
     </div>
-    <template is="dom-repeat" items="[[messages]]" as="message">
+    <template
+        is="dom-repeat"
+        items="[[_computeItems(messages, reviewerUpdates)]]"
+        as="message">
       <gr-message
           change-num="[[changeNum]]"
           message="[[message]]"
-          comments="[[_computeCommentsForMessage(comments, message, index)]]"
+          comments="[[_computeCommentsForMessage(comments, message)]]"
           project-config="[[projectConfig]]"
           show-reply-button="[[showReplyButtons]]"
           on-scroll-to="_handleScrollTo"
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index 1b9ce14..875bf2b 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -23,9 +23,12 @@
         type: Array,
         value: function() { return []; },
       },
+      reviewerUpdates: {
+        type: Array,
+        value: function() { return []; },
+      },
       comments: Object,
       projectConfig: Object,
-      topMargin: Number,
       showReplyButtons: {
         type: Boolean,
         value: false,
@@ -48,10 +51,43 @@
            offsetParent = offsetParent.offsetParent) {
         top += offsetParent.offsetTop;
       }
-      window.scrollTo(0, top - this.topMargin);
+      window.scrollTo(0, top);
       this._highlightEl(el);
     },
 
+    _computeItems: function(messages, reviewerUpdates) {
+      messages = messages || [];
+      reviewerUpdates = reviewerUpdates || [];
+      var mi = 0;
+      var ri = 0;
+      var result = [];
+      var mDate;
+      var rDate;
+      for (var i = 0; i < messages.length; i++) {
+        messages[i]._index = i;
+      }
+      while (mi < messages.length || ri < reviewerUpdates.length) {
+        if (mi >= messages.length) {
+          result = result.concat(reviewerUpdates.slice(ri));
+          break;
+        }
+        if (ri >= reviewerUpdates.length) {
+          result = result.concat(messages.slice(mi));
+          break;
+        }
+        mDate = mDate || util.parseDate(messages[mi].date);
+        rDate = rDate || util.parseDate(reviewerUpdates[ri].updated);
+        if (rDate < mDate) {
+          result.push(reviewerUpdates[ri++]);
+          rDate = null;
+        } else {
+          result.push(messages[mi++]);
+          mDate = null;
+        }
+      }
+      return result;
+    },
+
     _highlightEl: function(el) {
       var highlightedEls =
           Polymer.dom(this.root).querySelectorAll('.highlighted');
@@ -83,8 +119,11 @@
       return expanded ? 'Collapse all' : 'Expand all';
     },
 
-    _computeCommentsForMessage: function(comments, message, index) {
-      comments = comments || {};
+    _computeCommentsForMessage: function(comments, message) {
+      if (message._index === undefined || !comments || !this.messages) {
+        return [];
+      }
+      var index = message._index;
       var messages = this.messages || [];
       var msgComments = {};
       var mDate = util.parseDate(message.date);
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index 9ef9d02..3cda480 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -39,37 +39,37 @@
       element = fixture('basic');
       element.messages = [
         {
-          'id': '47c43261_55aa2c41',
-          'author': {
-            '_account_id': 1115495,
-            'name': 'Andrew Bonventre',
-            'email': 'andybons@chromium.org',
+          id: '47c43261_55aa2c41',
+          author: {
+            _account_id: 1115495,
+            name: 'Andrew Bonventre',
+            email: 'andybons@chromium.org',
           },
-          'date': '2016-01-12 20:24:49.448000000',
-          'message': 'Uploaded patch set 1.',
-          '_revision_number': 1
+          date: '2016-01-12 20:24:49.448000000',
+          message: 'Uploaded patch set 1.',
+          _revision_number: 1
         },
         {
-          'id': '47c43261_9593e420',
-          'author': {
-            '_account_id': 1115495,
-            'name': 'Andrew Bonventre',
-            'email': 'andybons@chromium.org',
+          id: '47c43261_9593e420',
+          author: {
+            _account_id: 1115495,
+            name: 'Andrew Bonventre',
+            email: 'andybons@chromium.org',
           },
-          'date': '2016-01-12 20:28:33.038000000',
-          'message': 'Patch Set 1:\n\n(1 comment)',
-          '_revision_number': 1
+          date: '2016-01-12 20:28:33.038000000',
+          message: 'Patch Set 1:\n\n(1 comment)',
+          _revision_number: 1
         },
         {
-          'id': '87b2aaf4_f73260c5',
-          'author': {
-            '_account_id': 1143760,
-            'name': 'Mark Mentovai',
-            'email': 'mark@chromium.org',
+          id: '87b2aaf4_f73260c5',
+          author: {
+            _account_id: 1143760,
+            name: 'Mark Mentovai',
+            email: 'mark@chromium.org',
           },
-          'date': '2016-01-12 21:17:07.554000000',
-          'message': 'Patch Set 1:\n\n(3 comments)',
-          '_revision_number': 1
+          date: '2016-01-12 21:17:07.554000000',
+          message: 'Patch Set 1:\n\n(3 comments)',
+          _revision_number: 1
         }
       ];
       flushAsynchronousOperations();
@@ -127,6 +127,5 @@
       scrollToStub.restore();
       highlightStub.restore();
     });
-
   });
 </script>
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 6ec04da..f4ee53a 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
@@ -74,7 +74,7 @@
             this._sameTopic = response;
           }.bind(this));
         } else {
-         this._sameTopic = [];
+          this._sameTopic = [];
         }
         return this._sameTopic;
       }.bind(this)).then(Promise.all(promises)).then(function() {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 556691a..cec1e90 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -18,9 +18,13 @@
 <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../bower_components/iron-selector/iron-selector.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.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="../../shared/gr-storage/gr-storage.html">
+<link rel="import" href="../gr-account-list/gr-account-list.html">
 
 <dom-module id="gr-reply-dialog">
   <template>
@@ -43,14 +47,47 @@
       section {
         border-top: 1px solid #ddd;
         padding: .5em .75em;
+        width: 100%;
       }
+      .peopleContainer,
       .labelsContainer,
       .actionsContainer {
         flex-shrink: 0;
       }
-      .textareaContainer {
-        position: relative;
+      .peopleContainer {
+        display: table;
+      }
+      .peopleList {
         display: flex;
+        padding-top: .1em;
+      }
+      .peopleListLabel {
+        color: #666;
+        min-width: 7em;
+        padding-right: .5em;
+      }
+      gr-account-list {
+        display: flex;
+        flex-wrap: wrap;
+      }
+      #reviewerConfirmationOverlay {
+        padding: 1em;
+        text-align: center;
+      }
+      .reviewerConfirmationButtons {
+        margin-top: 1em;
+      }
+      .groupName {
+        font-weight: bold;
+      }
+      .groupSize {
+        font-style: italic;
+      }
+      .textareaContainer {
+        display: flex;
+        flex: 1;
+        min-height: 6em;
+        position: relative;
       }
       iron-autogrow-textarea {
         padding: 0;
@@ -90,6 +127,7 @@
         background-color: #ddd;
       }
       .draftsContainer {
+        flex: 1;
         overflow-y: auto;
       }
       .draftsContainer h3 {
@@ -105,6 +143,61 @@
       }
     </style>
     <div class="container">
+      <section class="peopleContainer">
+        <div class="peopleList">
+          <div class="peopleListLabel">Owner</div>
+          <gr-account-chip account="[[_owner]]">
+          <gr-account-chip>
+        </div>
+      </section>
+      <section class="peopleContainer">
+        <div class="peopleList">
+          <div class="peopleListLabel">Reviewers</div>
+          <gr-account-list
+              id="reviewers"
+              accounts="[[_reviewers]]"
+              change="[[change]]"
+              filter="[[filterReviewerSuggestion]]"
+              pending-confirmation="{{_reviewerPendingConfirmation}}"
+              placeholder="Add reviewer...">
+          </gr-account-list>
+        </div>
+        <template is="dom-if" if="[[serverConfig.note_db_enabled]]">
+          <div class="peopleList">
+            <div class="peopleListLabel">CC</div>
+            <gr-account-list
+                id="ccs"
+                accounts="[[_ccs]]"
+                change="[[change]]"
+                filter="[[filterReviewerSuggestion]]"
+                pending-confirmation="{{_ccPendingConfirmation}}"
+                placeholder="Add CC...">
+            </gr-account-list>
+          </div>
+        </template>
+        <gr-overlay
+            id="reviewerConfirmationOverlay"
+            on-iron-overlay-canceled="_cancelPendingReviewer"
+            with-backdrop>
+          <div class="reviewerConfirmation">
+            Group
+            <span class="groupName">
+              {{_reviewerPendingConfirmation.group.name}}
+            </span>
+            has
+            <span class="groupSize">
+              {{_reviewerPendingConfirmation.count}}
+            </span>
+            members.
+            <br>
+            Are you sure you want to add them all?
+          </div>
+          <div class="reviewerConfirmationButtons">
+            <gr-button on-tap="_confirmPendingReviewer">Yes</gr-button>
+            <gr-button on-tap="_cancelPendingReviewer">No</gr-button>
+          </div>
+        </gr-overlay>
+      </section>
       <section class="textareaContainer">
         <iron-autogrow-textarea
             id="textarea"
@@ -113,7 +206,9 @@
             disabled="{{disabled}}"
             rows="4"
             max-rows="15"
-            bind-value="{{draft}}"></iron-autogrow-textarea>
+            bind-value="{{draft}}"
+            on-bind-value-changed="_handleTextareaChanged">
+        </iron-autogrow-textarea>
       </section>
       <section class="labelsContainer">
         <template is="dom-if" if="[[_computeShowLabels(patchNum, revisions)]]">
@@ -136,7 +231,7 @@
         <template is="dom-if" if="[[!_computeShowLabels(patchNum, revisions)]]">
           <span class="labelsNotShown">
             Labels are not shown because this is not the most recent patch set.
-            <a href$="/c/[[changeNum]]">Go to the latest patch set.</a>
+            <a href$="/c/[[change._number]]">Go to the latest patch set.</a>
           </span>
         </template>
       </section>
@@ -144,7 +239,7 @@
         <h3>[[_computeDraftsTitle(diffDrafts)]]</h3>
         <gr-comment-list
             comments="[[diffDrafts]]"
-            change-num="[[changeNum]]"
+            change-num="[[change._number]]"
             patch-num="[[patchNum]]"></gr-comment-list>
       </section>
       <section class="actionsContainer">
@@ -157,6 +252,7 @@
     </div>
     <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-storage id="storage"></gr-storage>
   </template>
   <script src="gr-reply-dialog.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 0ee779e..d2b279d 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -14,6 +14,15 @@
 (function() {
   'use strict';
 
+  var STORAGE_DEBOUNCE_INTERVAL_MS = 400;
+
+  var FocusTarget = {
+    ANY: 'any',
+    BODY: 'body',
+    CCS: 'cc',
+    REVIEWERS: 'reviewers',
+  };
+
   Polymer({
     is: 'gr-reply-dialog',
 
@@ -29,8 +38,15 @@
      * @event cancel
      */
 
+    /**
+     * Fired when the main textarea's value changes, which may have triggered
+     * a change in size for the dialog.
+     *
+     * @event autogrow
+     */
+
     properties: {
-      changeNum: String,
+      change: Object,
       patchNum: String,
       revisions: Object,
       disabled: {
@@ -41,18 +57,43 @@
       draft: {
         type: String,
         value: '',
+        observer: '_draftChanged',
       },
       diffDrafts: Object,
+      filterReviewerSuggestion: {
+        type: Function,
+        value: function() {
+          return this._filterReviewerSuggestion.bind(this);
+        },
+      },
       labels: Object,
       permittedLabels: Object,
+      serverConfig: Object,
 
       _account: Object,
+      _ccs: Array,
+      _ccPendingConfirmation: {
+        type: Object,
+        observer: '_reviewerPendingConfirmationUpdated',
+      },
+      _owner: Object,
+      _reviewers: Array,
+      _reviewerPendingConfirmation: {
+        type: Object,
+        observer: '_reviewerPendingConfirmationUpdated',
+      },
     },
 
+    FocusTarget: FocusTarget,
+
     behaviors: [
       Gerrit.RESTClientBehavior,
     ],
 
+    observers: [
+      '_changeUpdated(change.reviewers.*, change.owner, serverConfig)',
+    ],
+
     attached: function() {
       this._getAccount().then(function(account) {
         this._account = account;
@@ -63,15 +104,20 @@
       this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
     },
 
+    open: function(opt_focusTarget) {
+      this._focusOn(opt_focusTarget);
+      if (!this.draft || !this.draft.length) {
+        this.draft = this._loadStoredDraft();
+      }
+    },
+
     focus: function() {
-      this.async(function() {
-        this.$.textarea.textarea.focus();
-      }.bind(this));
+      this._focusOn(FocusTarget.ANY);
     },
 
     getFocusStops: function() {
       return {
-        start: this.$.textarea.$.textarea,
+        start: this.$.reviewers.focusStart,
         end: this.$.cancelButton,
       };
     },
@@ -85,6 +131,18 @@
       selectorEl.selectIndex(selectorEl.indexOf(item));
     },
 
+    _mapReviewer: function(reviewer) {
+      var reviewerId;
+      var confirmed;
+      if (reviewer.account) {
+        reviewerId = reviewer.account._account_id;
+      } else if (reviewer.group) {
+        reviewerId = reviewer.group.id;
+        confirmed = reviewer.group.confirmed;
+      }
+      return {reviewer: reviewerId, confirmed: confirmed};
+    },
+
     send: function() {
       var obj = {
         drafts: 'PUBLISH_ALL_REVISIONS',
@@ -105,11 +163,24 @@
       if (this.draft != null) {
         obj.message = this.draft;
       }
-      this.disabled = true;
-      return this._saveReview(obj).then(function(response) {
-        this.disabled = false;
-        if (!response.ok) { return response; }
 
+      obj.reviewers = this.$.reviewers.additions().map(this._mapReviewer);
+      if (this.serverConfig.note_db_enabled) {
+        this.$$('#ccs').additions().forEach(function(reviewer) {
+          reviewer = this._mapReviewer(reviewer);
+          reviewer.state = 'CC';
+          obj.reviewers.push(reviewer);
+        }.bind(this));
+      }
+
+      this.disabled = true;
+
+      var errFn = this._handle400Error.bind(this);
+      return this._saveReview(obj, errFn).then(function(response) {
+        if (!response || !response.ok) {
+          return response;
+        }
+        this.disabled = false;
         this.draft = '';
         this.fire('send', null, {bubbles: false});
       }.bind(this)).catch(function(err) {
@@ -118,6 +189,76 @@
       }.bind(this));
     },
 
+    _focusOn: function(section) {
+      if (section === FocusTarget.ANY) {
+        section = this._chooseFocusTarget();
+      }
+      if (section === FocusTarget.BODY) {
+        var textarea = this.$.textarea;
+        textarea.async(textarea.textarea.focus.bind(textarea.textarea));
+      } else if (section === FocusTarget.REVIEWERS) {
+        var reviewerEntry = this.$.reviewers.focusStart;
+        reviewerEntry.async(reviewerEntry.focus);
+      } else if (section === FocusTarget.CCS) {
+        var ccEntry = this.$$('#ccs').focusStart;
+        ccEntry.async(ccEntry.focus);
+      }
+    },
+
+    _chooseFocusTarget: function() {
+      // If we are the owner and the reviewers field is empty, focus on that.
+      if (this._account && this.change.owner &&
+          this._account._account_id === this.change.owner._account_id &&
+          (!this._reviewers || this._reviewers.length === 0)) {
+        return FocusTarget.REVIEWERS;
+      }
+
+      // Default to BODY.
+      return FocusTarget.BODY;
+    },
+
+    _handle400Error: function(response) {
+      // A call to _saveReview could fail with a server error if erroneous
+      // reviewers were requested. This is signalled with a 400 Bad Request
+      // status. The default gr-rest-api-interface error handling would
+      // result in a large JSON response body being displayed to the user in
+      // the gr-error-manager toast.
+      //
+      // We can modify the error handling behavior by passing this function
+      // through to restAPI as a custom error handling function. Since we're
+      // short-circuiting restAPI we can do our own response parsing and fire
+      // the server-error ourselves.
+      //
+      this.disabled = false;
+
+      if (response.status !== 400) {
+        // This is all restAPI does when there is no custom error handling.
+        this.fire('server-error', {response: response});
+        return response;
+      }
+
+      // Process the response body, format a better error message, and fire
+      // an event for gr-event-manager to display.
+      var jsonPromise = this.$.restAPI.getResponseObject(response);
+      return jsonPromise.then(function(result) {
+        var errors = [];
+        ['reviewers', 'ccs'].forEach(function(state) {
+          for (var input in result[state]) {
+            var reviewer = result[state][input];
+            if (!!reviewer.error) {
+              errors.push(reviewer.error);
+            }
+          }
+        });
+        response = {
+          ok: false,
+          status: response.status,
+          text: function() { return Promise.resolve(errors.join(', ')); },
+        };
+        this.fire('server-error', {response: response});
+      }.bind(this));
+    },
+
     _computeShowLabels: function(patchNum, revisions) {
       var num = parseInt(patchNum, 10);
       for (var rev in revisions) {
@@ -182,6 +323,68 @@
       return permittedLabels[label];
     },
 
+    _changeUpdated: function(changeRecord, owner, serverConfig) {
+      this._owner = owner;
+
+      var reviewers = [];
+      var ccs = [];
+
+      for (var key in changeRecord.base) {
+        if (key !== 'REVIEWER' && key !== 'CC') {
+          console.warn('unexpected reviewer state:', key);
+          continue;
+        }
+        changeRecord.base[key].forEach(function(entry) {
+          if (entry._account_id === owner._account_id) {
+            return;
+          }
+          switch (key) {
+            case 'REVIEWER':
+              reviewers.push(entry);
+              break;
+            case 'CC':
+              ccs.push(entry);
+              break;
+          }
+        });
+      }
+
+      if (serverConfig.note_db_enabled) {
+        this._ccs = ccs;
+      } else {
+        this._ccs = [];
+        reviewers = reviewers.concat(ccs);
+      }
+      this._reviewers = reviewers;
+    },
+
+    _accountOrGroupKey: function(entry) {
+      return entry.id || entry._account_id;
+    },
+
+    _filterReviewerSuggestion: function(suggestion) {
+      var entry;
+      if (suggestion.account) {
+        entry = suggestion.account;
+      } else if (suggestion.group) {
+        entry = suggestion.group;
+      } else {
+        console.warn('received suggestion that was neither account nor group:',
+            suggestion);
+      }
+      if (entry._account_id === this._owner._account_id) {
+        return false;
+      }
+
+      var key = this._accountOrGroupKey(entry);
+      var finder = function(entry) {
+        return this._accountOrGroupKey(entry) === key;
+      }.bind(this);
+
+      return this._reviewers.find(finder) === undefined &&
+          this._ccs.find(finder) === undefined;
+    },
+
     _getAccount: function() {
       return this.$.restAPI.getAccount();
     },
@@ -196,9 +399,69 @@
       this.send();
     },
 
-    _saveReview: function(review) {
-      return this.$.restAPI.saveChangeReview(this.changeNum, this.patchNum,
-          review);
+    _saveReview: function(review, opt_errFn) {
+      return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum,
+          review, opt_errFn);
+    },
+
+    _reviewerPendingConfirmationUpdated: function(reviewer) {
+      if (reviewer === null) {
+        this.$.reviewerConfirmationOverlay.close();
+      } else {
+        this.$.reviewerConfirmationOverlay.open();
+      }
+    },
+
+    _confirmPendingReviewer: function() {
+      if (this._ccPendingConfirmation) {
+        this.$$('#ccs').confirmGroup(this._ccPendingConfirmation.group);
+        this._focusOn(FocusTarget.CCS);
+      } else {
+        this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group);
+        this._focusOn(FocusTarget.REVIEWERS);
+      }
+    },
+
+    _cancelPendingReviewer: function() {
+      this._ccPendingConfirmation = null;
+      this._reviewerPendingConfirmation = null;
+
+      var target =
+          this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS;
+      this._focusOn(target);
+    },
+
+    _getStorageLocation: function() {
+      return {
+        changeNum: this.change._number,
+        patchNum: this.patchNum,
+        path: '@change',
+      };
+    },
+
+    _loadStoredDraft: function() {
+      var draft = this.$.storage.getDraftComment(this._getStorageLocation());
+      return draft ? draft.message : '';
+    },
+
+    _draftChanged: function(newDraft, oldDraft) {
+      this.debounce('store', function() {
+        if (!newDraft.length && oldDraft) {
+          // If the draft has been modified to be empty, then erase the storage
+          // entry.
+          this.$.storage.eraseDraftComment(this._getStorageLocation());
+        } else if (newDraft.length) {
+          this.$.storage.setDraftComment(this._getStorageLocation(),
+              this.draft);
+        }
+      }, STORAGE_DEBOUNCE_INTERVAL_MS);
+    },
+
+    _handleTextareaChanged: function(e) {
+      // If the textarea resizes, we need to re-fit the overlay.
+      this.debounce('autogrow', function() {
+        this.fire('autogrow');
+      });
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index cbef78d..8fb4e45 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -34,14 +34,27 @@
 <script>
   suite('gr-reply-dialog tests', function() {
     var element;
+    var changeNum;
+    var patchNum;
+
+    var sandbox;
+    var getDraftCommentStub;
+    var setDraftCommentStub;
+    var eraseDraftCommentStub;
 
     setup(function() {
+      sandbox = sinon.sandbox.create();
+
+      changeNum = 42;
+      patchNum = 1;
+
       stub('gr-rest-api-interface', {
         getAccount: function() { return Promise.resolve({}); },
       });
+
       element = fixture('basic');
-      element.changeNum = 42;
-      element.patchNum = 1;
+      element.change = { _number: changeNum };
+      element.patchNum = patchNum;
       element.labels = {
         Verified: {
           values: {
@@ -74,11 +87,21 @@
           '+1'
         ]
       };
+      element.serverConfig = {};
+
+      getDraftCommentStub = sandbox.stub(element.$.storage, 'getDraftComment');
+      setDraftCommentStub = sandbox.stub(element.$.storage, 'setDraftComment');
+      eraseDraftCommentStub = sandbox.stub(element.$.storage,
+          'eraseDraftComment');
 
       // Allow the elements created by dom-repeat to be stamped.
       flushAsynchronousOperations();
     });
 
+    teardown(function() {
+      sandbox.restore();
+    });
+
     test('cancel event', function(done) {
       element.addEventListener('cancel', function() { done(); });
       MockInteractions.tap(element.$$('.cancel'));
@@ -122,7 +145,8 @@
               'Code-Review': -1,
               'Verified': -1
             },
-            message: 'I wholeheartedly disapprove'
+            message: 'I wholeheartedly disapprove',
+            reviewers: [],
           });
           return Promise.resolve({ok: true});
         });
@@ -144,5 +168,226 @@
         });
       });
     });
+
+    function getActiveElement() {
+      return Polymer.IronOverlayManager.deepActiveElement;
+    }
+
+    function isVisible(el) {
+      assert.ok(el);
+      return getComputedStyle(el).getPropertyValue('display') != 'none';
+    }
+
+    function overlayObserver(mode) {
+      return new Promise(function(resolve) {
+        function listener() {
+          element.removeEventListener('iron-overlay-' + mode, listener);
+          resolve();
+        }
+        element.addEventListener('iron-overlay-' + mode, listener);
+      });
+    }
+
+    test('reviewer confirmation', function(done) {
+      var yesButton =
+          element.$$('.reviewerConfirmationButtons gr-button:first-child');
+      var noButton =
+          element.$$('.reviewerConfirmationButtons gr-button:last-child');
+
+      element._reviewerPendingConfirmation = null;
+      flushAsynchronousOperations();
+      assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
+
+      // Cause the confirmation dialog to display.
+      var observer = overlayObserver('opened');
+      var group = {
+        id: 'id',
+        name: 'name',
+        count: 10,
+      };
+      element._reviewerPendingConfirmation = {
+        group: group,
+      };
+
+      observer.then(function() {
+        assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
+        observer = overlayObserver('closed');
+        MockInteractions.tap(noButton); // close the overlay
+        return observer;
+      }).then(function() {
+        assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
+
+        // We should be focused on account entry input.
+        assert.equal(getActiveElement().id, 'input');
+
+        // No reviewer should have been added.
+        assert.deepEqual(element.$.reviewers.additions(), []);
+
+        // Reopen confirmation dialog.
+        observer = overlayObserver('opened');
+        element._reviewerPendingConfirmation = {
+          group: group,
+        };
+        return observer;
+      }).then(function() {
+        assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
+        observer = overlayObserver('closed');
+        MockInteractions.tap(yesButton); // confirm the group
+        return observer;
+      }).then(function() {
+        assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
+        assert.deepEqual(
+            element.$.reviewers.additions(),
+            [
+              {
+                group: {
+                  id: 'id',
+                  name: 'name',
+                  count: 10,
+                  confirmed: true,
+                  _group: true,
+                  _pendingAdd: true,
+                },
+              },
+            ]);
+
+        // We should be focused on account entry input.
+        assert.equal(getActiveElement().id, 'input');
+      }).then(done);
+    });
+
+    test('_getStorageLocation', function() {
+      var actual = element._getStorageLocation();
+      assert.equal(actual.changeNum, changeNum);
+      assert.equal(actual.patchNum, patchNum);
+      assert.equal(actual.path, '@change');
+    });
+
+    test('gets draft from storage on open', function() {
+      var storedDraft = 'hello world';
+      getDraftCommentStub.returns({message: storedDraft});
+      element.open();
+      assert.isTrue(getDraftCommentStub.called);
+      assert.equal(element.draft, storedDraft);
+    });
+
+    test('blank if no stored draft', function() {
+      getDraftCommentStub.returns(null);
+      element.open();
+      assert.isTrue(getDraftCommentStub.called);
+      assert.equal(element.draft, '');
+    });
+
+    test('updates stored draft on edits', function() {
+      var firstEdit = 'hello';
+      var location = element._getStorageLocation();
+
+      element.draft = firstEdit;
+      element.flushDebouncer('store');
+
+      assert.isTrue(setDraftCommentStub.calledWith(location, firstEdit));
+
+      element.draft = '';
+      element.flushDebouncer('store');
+
+      assert.isTrue(eraseDraftCommentStub.calledWith(location));
+    });
+
+    test('400 converts to human-readable server-error', function(done) {
+      sandbox.stub(window, 'fetch', function() {
+        var text = '....{"reviewers":{"id1":{"error":"first error"}},' +
+          '"ccs":{"id2":{"error":"second error"}}}';
+        return Promise.resolve({
+          ok: false,
+          status: 400,
+          text: function() { return Promise.resolve(text); },
+        });
+      });
+
+      element.addEventListener('server-error', function(event) {
+        if (event.target !== element) {
+          return;
+        }
+        event.detail.response.text().then(function(body) {
+          assert.equal(body, 'first error, second error');
+        });
+      });
+      element.send().then(done);
+    });
+
+    test('ccs are displayed if NoteDb is enabled', function() {
+      function hasCc() {
+        flushAsynchronousOperations();
+        return !!element.$$('#ccs');
+      }
+
+      element.serverConfig = {};
+      assert.isFalse(hasCc());
+
+      element.serverConfig = {note_db_enabled: true};
+      assert.isTrue(hasCc());
+    });
+
+    test('filterReviewerSuggestion', function() {
+      var counter = 0;
+      function makeAccount() {
+        return {_account_id: counter++};
+      }
+      function makeGroup() {
+        return {id: counter++};
+      }
+
+      var owner = makeAccount();
+      var reviewer1 = makeAccount();
+      var reviewer2 = makeGroup();
+      var cc1 = makeAccount();
+      var cc2 = makeGroup();
+
+      element._owner = owner;
+      element._reviewers = [reviewer1, reviewer2];
+      element._ccs = [cc1, cc2];
+
+      assert.isTrue(
+          element._filterReviewerSuggestion({account: makeAccount()}));
+      assert.isTrue(element._filterReviewerSuggestion({group: makeGroup()}));
+
+      // Owner should be excluded.
+      assert.isFalse(element._filterReviewerSuggestion({account: owner}));
+
+      // Existing and pending reviewers should be excluded.
+      assert.isFalse(element._filterReviewerSuggestion({account: reviewer1}));
+      assert.isFalse(element._filterReviewerSuggestion({group: reviewer2}));
+
+      // Existing and pending CCs should be excluded.
+      assert.isFalse(element._filterReviewerSuggestion({account: cc1}));
+      assert.isFalse(element._filterReviewerSuggestion({group: cc2}));
+    });
+
+    test('_chooseFocusTarget', function() {
+      element._account = null;
+      assert.strictEqual(
+          element._chooseFocusTarget(), element.FocusTarget.BODY);
+
+      element._account = {_account_id: 1};
+      assert.strictEqual(
+          element._chooseFocusTarget(), element.FocusTarget.BODY);
+
+      element.change.owner = {_account_id: 2};
+      assert.strictEqual(
+          element._chooseFocusTarget(), element.FocusTarget.BODY);
+
+      element.change.owner._account_id = 1;
+      element.change._reviewers = null;
+      assert.strictEqual(
+          element._chooseFocusTarget(), element.FocusTarget.REVIEWERS);
+
+      element._reviewers = [];
+      assert.strictEqual(
+          element._chooseFocusTarget(), element.FocusTarget.REVIEWERS);
+
+      element._reviewers.push({});
+      assert.strictEqual(
+          element._chooseFocusTarget(), element.FocusTarget.BODY);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 296da15..435b7de 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -18,7 +18,6 @@
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-reviewer-list">
@@ -45,19 +44,12 @@
       gr-account-chip {
         margin-top: .3em;
       }
-      .remove,
-      .cancel {
+      .remove {
         color: #999;
       }
       .remove {
         font-size: .9em;
       }
-      .cancel {
-        font-size: 2em;
-        line-height: 1;
-        padding: 0 .15em;
-        text-decoration: none;
-      }
       @media screen and (max-width: 50em), screen and (min-width: 75em) {
         gr-account-chip:first-of-type {
           margin-top: 0;
@@ -72,28 +64,11 @@
       </gr-account-chip>
     </template>
     <div class="controlsContainer" hidden$="[[!mutable]]">
-      <div class="autocompleteContainer" hidden$="[[!_showInput]]">
-        <div class="inputContainer">
-          <gr-autocomplete
-              id="input"
-              threshold="3"
-              clear-on-commit
-              query="[[_query]]"
-              disabled="[[disabled]]"
-              on-commit="_sendAddRequest"
-              on-cancel="_handleCancelTap"></gr-autocomplete>
-          <gr-button
-              link
-              class="cancel"
-              on-tap="_handleCancelTap">×</gr-button>
-        </div>
-      </div>
       <gr-button
           link
           id="addReviewer"
           class="addReviewer"
-          on-tap="_handleAddTap"
-          hidden$="[[_showInput]]">Add reviewer</gr-button>
+          on-tap="_handleAddTap">[[_addLabel]]</gr-button>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index 46d6cbd..72a7c9b 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -17,20 +17,30 @@
   Polymer({
     is: 'gr-reviewer-list',
 
+    /**
+     * Fired when the "Add reviewer..." button is tapped.
+     *
+     * @event show-reply-dialog
+     */
+
     properties: {
       change: Object,
-      mutable: {
-        type: Boolean,
-        value: false,
-      },
       disabled: {
         type: Boolean,
         value: false,
         reflectToAttribute: true,
       },
-      suggestFrom: {
-        type: Number,
-        value: 3,
+      mutable: {
+        type: Boolean,
+        value: false,
+      },
+      reviewersOnly: {
+        type: Boolean,
+        value: false,
+      },
+      ccsOnly: {
+        type: Boolean,
+        value: false,
       },
 
       _reviewers: {
@@ -41,12 +51,9 @@
         type: Boolean,
         value: false,
       },
-
-      _query: {
-        type: Function,
-        value: function() {
-          return this._getReviewerSuggestions.bind(this);
-        },
+      _addLabel: {
+        type: String,
+        computed: '_computeAddLabel(ccsOnly)',
       },
 
       // Used for testing.
@@ -62,7 +69,13 @@
       var result = [];
       var reviewers = changeRecord.base;
       for (var key in reviewers) {
-        if (key == 'REVIEWER' || key == 'CC') {
+        if (this.reviewersOnly && key !== 'REVIEWER') {
+          continue;
+        }
+        if (this.ccsOnly && key !== 'CC') {
+          continue;
+        }
+        if (key === 'REVIEWER' || key === 'CC') {
           result = result.concat(reviewers[key]);
         }
       }
@@ -111,98 +124,22 @@
 
     _handleAddTap: function(e) {
       e.preventDefault();
-      this._showInput = true;
-      this.$.input.focus();
-    },
-
-    _handleCancelTap: function(e) {
-      e.preventDefault();
-      this.$.input.clear();
-      this._cancel();
-    },
-
-    _cancel: function() {
-      this._showInput = false;
-      this.$.input.clear();
-      this.$.addReviewer.focus();
-    },
-
-    _sendAddRequest: function(e, detail) {
-      var reviewer = detail.value;
-      var reviewerID;
-      if (reviewer.account) {
-        reviewerID = reviewer.account._account_id;
-      } else if (reviewer.group) {
-        reviewerID = reviewer.group.id;
+      var value = {};
+      if (this.reviewersOnly) {
+        value.reviewersOnly = true;
       }
-
-      this.disabled = true;
-      this._xhrPromise = this._addReviewer(reviewerID).then(function(response) {
-        this.change.reviewers.CC = this.change.reviewers.CC || [];
-        this.disabled = false;
-        if (!response.ok) { return response; }
-
-        return this.$.restAPI.getResponseObject(response).then(function(obj) {
-          obj.reviewers.forEach(function(r) {
-            this.push('change.removable_reviewers', r);
-            this.push('change.reviewers.CC', r);
-          }, this);
-          this.$.input.focus();
-        }.bind(this));
-      }.bind(this)).catch(function(err) {
-        this.disabled = false;
-        throw err;
-      }.bind(this));
-    },
-
-    _addReviewer: function(id) {
-      return this.$.restAPI.addChangeReviewer(this.change._number, id);
+      if (this.ccsOnly) {
+        value.ccsOnly = true;
+      }
+      this.fire('show-reply-dialog', {value: value});
     },
 
     _removeReviewer: function(id) {
       return this.$.restAPI.removeChangeReviewer(this.change._number, id);
     },
 
-    _notInList: function(reviewer) {
-      var account = reviewer.account;
-      if (!account) { return true; }
-      if (account._account_id === this.change.owner._account_id) {
-        return false;
-      }
-      for (var i = 0; i < this._reviewers.length; i++) {
-        if (account._account_id === this._reviewers[i]._account_id) {
-          return false;
-        }
-      }
-      return true;
-    },
-
-    _makeSuggestion: function(reviewer) {
-      if (reviewer.account) {
-        return {
-          name: reviewer.account.name + ' (' + reviewer.account.email + ')',
-          value: reviewer,
-        };
-      } else if (reviewer.group) {
-        return {
-          name: reviewer.group.name,
-          value: reviewer,
-        };
-      }
-    },
-
-    _getReviewerSuggestions: function(input) {
-      var xhr = this.$.restAPI.getChangeSuggestedReviewers(
-          this.change._number, input);
-
-      this._lastAutocompleteRequest = xhr;
-
-      return xhr.then(function(reviewers) {
-        if (!reviewers) { return []; }
-        return reviewers
-            .filter(this._notInList.bind(this))
-            .map(this._makeSuggestion);
-      }.bind(this));
+    _computeAddLabel: function(ccsOnly) {
+      return ccsOnly ? 'Add CC' : 'Add reviewer';
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index f2d4d43..6c6125c 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -34,62 +34,22 @@
 <script>
   suite('gr-reviewer-list tests', function() {
     var element;
-    var autocompleteInput;
+    var sandbox;
 
     setup(function() {
       element = fixture('basic');
-      autocompleteInput = element.$.input.$.input;
+      sandbox = sinon.sandbox.create();
       stub('gr-rest-api-interface', {
-        getChangeSuggestedReviewers: function() {
-          return Promise.resolve([
-            {
-              account: {
-                _account_id: 1021482,
-                name: 'Andrew Bonventre',
-                email: 'andybons@chromium.org',
-              }
-            },
-            {
-              account: {
-                _account_id: 1021863,
-                name: 'Andrew Bonventre',
-                email: 'andybons@google.com',
-              }
-            },
-            {
-              group: {
-                id: 'c7af6dd375c092ff3b23c0937aa910693dc0c41b',
-                name: 'andy',
-              }
-            }
-          ]);
-        },
-        addChangeReviewer: function() {
-          return Promise.resolve({
-            ok: true,
-            text: function() {
-              return Promise.resolve(
-                ')]}\'\n' +
-                JSON.stringify({
-                  reviewers: [{
-                    _account_id: 1021482,
-                    approvals: {
-                      'Code-Review': ' 0'
-                    },
-                    email: 'andybons@chromium.org',
-                    name: 'Andrew Bonventre',
-                  }]
-                })
-              );
-            },
-          });
-        },
         removeChangeReviewer: function() {
           return Promise.resolve({ok: true});
         },
       });
     });
 
+    teardown(function() {
+      sandbox.restore();
+    });
+
     test('controls hidden on immutable element', function() {
       element.mutable = false;
       assert.isTrue(element.$$('.controlsContainer').hasAttribute('hidden'));
@@ -97,30 +57,11 @@
       assert.isFalse(element.$$('.controlsContainer').hasAttribute('hidden'));
     });
 
-    function getActiveElement() {
-      return document.activeElement.shadowRoot ?
-          document.activeElement.shadowRoot.activeElement :
-          document.activeElement;
-    }
-
-    test('show/hide input', function() {
-      element.mutable = true;
-      assert.isFalse(element.$$('.addReviewer').hasAttribute('hidden'));
-      assert.isTrue(
-          element.$$('.autocompleteContainer').hasAttribute('hidden'));
-      assert.notEqual(getActiveElement().id, 'input');
+    test('add reviewer button opens reply dialog', function(done) {
+      element.addEventListener('show-reply-dialog', function() {
+        done();
+      });
       MockInteractions.tap(element.$$('.addReviewer'));
-      assert.isTrue(element.$$('.addReviewer').hasAttribute('hidden'));
-      assert.isFalse(
-          element.$$('.autocompleteContainer').hasAttribute('hidden'));
-      assert.equal(getActiveElement().id, 'input');
-
-      MockInteractions.pressAndReleaseKeyOn(autocompleteInput, 27); // 'esc'
-
-      assert.isFalse(element.$$('.addReviewer').hasAttribute('hidden'));
-      assert.isTrue(
-          element.$$('.autocompleteContainer').hasAttribute('hidden'));
-      assert.equal(getActiveElement().id, 'addReviewer');
     });
 
     test('only show remove for removable reviewers', function() {
@@ -179,126 +120,64 @@
       });
     });
 
-    test('autocomplete starts at >= 3 chars', function() {
-      element._inputRequestTimeout = 0;
-      element._mutable = true;
-      element.change = {_number: 123};
+    test('tracking reviewers and ccs', function() {
+      var counter = 0;
+      function makeAccount() {
+        return {_account_id: counter++};
+      }
 
-      element.$.input.text = 'fo';
+      var owner = makeAccount();
+      var reviewer = makeAccount();
+      var cc = makeAccount();
+      var reviewers = {
+        REMOVED: [makeAccount()],
+        REVIEWER: [owner, reviewer],
+        CC: [owner, cc],
+      };
 
-      flushAsynchronousOperations();
-
-      assert.isFalse(element.$.restAPI.getChangeSuggestedReviewers.called);
-    });
-
-    test('add/remove reviewer flow', function(done) {
+      element.ccsOnly = false;
+      element.reviewersOnly = false;
       element.change = {
-        _number: 42,
-        reviewers: {},
-        removable_reviewers: [],
-        owner: {_account_id: 0},
+        owner: owner,
+        reviewers: reviewers,
       };
-      element._inputRequestTimeout = 0;
-      element._mutable = true;
-      MockInteractions.tap(element.$$('.addReviewer'));
-      flushAsynchronousOperations();
-      element.$.input.text = 'andy';
+      assert.deepEqual(element._reviewers, [reviewer, cc]);
 
-      element._lastAutocompleteRequest.then(function() {
-        flushAsynchronousOperations();
+      element.reviewersOnly = true;
+      element.change = {
+        owner: owner,
+        reviewers: reviewers,
+      };
+      assert.deepEqual(element._reviewers, [reviewer]);
 
-        MockInteractions.pressAndReleaseKeyOn(autocompleteInput, 27); // 'esc'
-        assert.isTrue(element.$$('.autocompleteContainer')
-            .hasAttribute('hidden'));
-
-        MockInteractions.tap(element.$$('.addReviewer'));
-
-        element.$.input.text = 'andyb';
-        element._lastAutocompleteRequest.then(function() {
-
-          MockInteractions.pressAndReleaseKeyOn(
-              autocompleteInput, 13); // 'enter'
-          assert.isTrue(element.disabled);
-
-          element._xhrPromise.then(function() {
-            assert.isFalse(element.disabled);
-            flushAsynchronousOperations();
-            var reviewerEls =
-                Polymer.dom(element.root).querySelectorAll('.reviewer');
-            assert.equal(reviewerEls.length, 1);
-            MockInteractions.tap(element.$$('.reviewer').$$('gr-button'));
-            flushAsynchronousOperations();
-            assert.isTrue(element.disabled);
-
-            element._xhrPromise.then(function() {
-              flushAsynchronousOperations();
-              assert.isFalse(element.disabled);
-              var reviewerEls =
-                  Polymer.dom(element.root).querySelectorAll('.reviewer');
-              assert.equal(reviewerEls.length, 0);
-              done();
-            });
-          });
-        });
-      });
+      element.ccsOnly = true;
+      element.reviewersOnly = false;
+      element.change = {
+        owner: owner,
+        reviewers: reviewers,
+      };
+      assert.deepEqual(element._reviewers, [cc]);
     });
 
-    test('_makeSuggestion', function() {
-      var account = {
-        _account_id: 123456,
-        name: 'name',
-        email: 'email'
-      };
-      var group = {
-        id: '123456',
-        name: 'name',
-      };
+    test('_handleAddTap passes mode with event', function() {
+      var fireStub = sandbox.stub(element, 'fire');
+      var e = {preventDefault: function() {}};
 
-      var suggestion = element._makeSuggestion({account: account});
+      element.ccsOnly = false;
+      element.reviewersOnly = false;
+      element._handleAddTap(e);
+      assert.isTrue(fireStub.calledWith('show-reply-dialog', {value: {}}));
 
-      assert.deepEqual(suggestion, {
-        name: 'name (email)',
-        value: {account: account},
-      });
+      element.reviewersOnly = true;
+      element._handleAddTap(e);
+      assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog',
+          {value: {reviewersOnly: true}}));
 
-      suggestion = element._makeSuggestion({group: group});
-
-      assert.deepEqual(suggestion, {
-        name: 'name',
-        value: {group: group},
-      });
-    });
-
-    test('_notInList', function() {
-      var group = {
-        id: '123456',
-        name: 'name',
-      };
-      var account = {
-        _account_id: 123456,
-        name: 'name',
-        email: 'email',
-      };
-
-      element.change = {owner: {_account_id: 123456}};
-
-      // Is true when passing a group.
-      assert.isTrue(element._notInList({group: group}));
-
-      // Is false when passing the change owner.
-      assert.isFalse(element._notInList({account: account}));
-
-      element.change.owner._account_id = 789;
-
-      // Is true when passing a different user than the change owner, and is not
-      // in the reviewer list.
-      assert.isTrue(element._notInList({account: account}));
-
-      element._reviewers = [{_account_id: 123456}];
-
-      // Is false when passing a different user than the change owner, but *is*
-      // the reviewer list.
-      assert.isFalse(element._notInList({account: account}));
+      element.ccsOnly = true;
+      element.reviewersOnly = false;
+      element._handleAddTap(e);
+      assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog',
+          {value: {ccsOnly: true}}));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 757d79f..7a9c4f9 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -14,16 +14,17 @@
 (function() {
   'use strict';
 
+  var HIDE_ALERT_TIMEOUT_MS = 5000;
+  var CHECK_SIGN_IN_INTERVAL_MS = 60000;
+  var SIGN_IN_WIDTH_PX = 690;
+  var SIGN_IN_HEIGHT_PX = 500;
+
   Polymer({
     is: 'gr-error-manager',
 
     properties: {
       _alertElement: Element,
       _hideAlertHandle: Number,
-      _hideAlertTimeout: {
-        type: Number,
-        value: 5000,
-      },
     },
 
     attached: function() {
@@ -67,7 +68,7 @@
 
       this._clearHideAlertHandle();
       this._hideAlertHandle =
-            this.async(this._hideAlert.bind(this), this._hideAlertTimeout);
+        this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS);
       var el = this._createToastAlert();
       el.show(text);
       this._alertElement = el;
@@ -88,12 +89,20 @@
     },
 
     _showAuthErrorAlert: function() {
+      // TODO(viktard): close alert if it's not for auth error.
       if (this._alertElement) { return; }
 
-      var el = this._createToastAlert();
-      el.addEventListener('action', this._refreshPage.bind(this));
-      el.show('Auth error', 'Refresh page');
-      this._alertElement = el;
+      this._alertElement = this._createToastAlert();
+      this._alertElement.show('Auth error', 'Refresh credentials.');
+      this.listen(this._alertElement, 'action', '_createLoginPopup');
+
+      if (typeof document.hidden !== undefined) {
+        this.listen(document, 'visibilitychange', '_handleVisibilityChange');
+      }
+      this._requestCheckLoggedIn();
+      if (!document.hidden) {
+        this._handleVisibilityChange();
+      }
     },
 
     _createToastAlert: function() {
@@ -102,8 +111,44 @@
       return el;
     },
 
-    _refreshPage: function() {
-      window.location.reload();
+    _handleVisibilityChange: function() {
+      if (!document.hidden) {
+        this.flushDebouncer('checkLoggedIn');
+      }
+    },
+
+    _requestCheckLoggedIn: function() {
+      this.debounce(
+        'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS);
+    },
+
+    _checkSignedIn: function() {
+      this.$.restAPI.refreshCredentials().then(function(isLoggedIn) {
+        if (isLoggedIn) {
+          this._handleCredentialRefresh();
+        } else {
+          this._requestCheckLoggedIn();
+        }
+      }.bind(this));
+    },
+
+    _createLoginPopup: function(e) {
+      var left = window.screenLeft + (window.outerWidth - SIGN_IN_WIDTH_PX) / 2;
+      var top = window.screenTop + (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2;
+      var options = [
+        'width=' + SIGN_IN_WIDTH_PX,
+        'height=' + SIGN_IN_HEIGHT_PX,
+        'left=' + left,
+        'top=' + top,
+      ];
+      window.open('/login/%3FcloseAfterLogin', '_blank', options.join(','));
+    },
+
+    _handleCredentialRefresh: function() {
+      this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
+      this.unlisten(this._alertElement, 'action', '_createLoginPopup');
+      this._hideAlert();
+      this._showAlert('Credentials refreshed.');
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index 08ce303..f633a7e 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -33,27 +33,32 @@
 <script>
   suite('gr-error-manager tests', function() {
     var element;
+    var sandbox;
 
     setup(function() {
+      sandbox = sinon.sandbox.create();
       stub('gr-rest-api-interface', {
         getLoggedIn: function() { return Promise.resolve(true); },
       });
       element = fixture('basic');
     });
 
+    teardown(function() {
+      sandbox.restore();
+    });
+
     test('show auth error', function(done) {
-      var showAuthErrorStub = sinon.stub(element, '_showAuthErrorAlert');
+      var showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
       element.fire('server-error', {response: {status: 403}});
-      flush(function() {
+      element.$.restAPI.getLoggedIn.lastCall.returnValue.then(function() {
         assert.isTrue(showAuthErrorStub.calledOnce);
-        showAuthErrorStub.restore();
         done();
       });
     });
 
     test('show normal server error', function(done) {
-      var showAlertStub = sinon.stub(element, '_showAlert');
-      var textSpy = sinon.spy(function() { return Promise.resolve('ZOMG'); });
+      var showAlertStub = sandbox.stub(element, '_showAlert');
+      var textSpy = sandbox.spy(function() { return Promise.resolve('ZOMG'); });
       element.fire('server-error', {response: {status: 500, text: textSpy}});
 
       assert.isTrue(textSpy.called);
@@ -61,14 +66,13 @@
         assert.isTrue(showAlertStub.calledOnce);
         assert.isTrue(showAlertStub.lastCall.calledWithExactly(
             'Server error: ZOMG'));
-        showAlertStub.restore();
         done();
       });
     });
 
     test('show network error', function(done) {
-      var consoleErrorStub = sinon.stub(console, 'error');
-      var showAlertStub = sinon.stub(element, '_showAlert');
+      var consoleErrorStub = sandbox.stub(console, 'error');
+      var showAlertStub = sandbox.stub(element, '_showAlert');
       element.fire('network-error', {error: new Error('ZOMG')});
       flush(function() {
         assert.isTrue(showAlertStub.calledOnce);
@@ -76,10 +80,45 @@
             'Server unavailable'));
         assert.isTrue(consoleErrorStub.calledOnce);
         assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG'));
-        showAlertStub.restore();
-        consoleErrorStub.restore();
         done();
       });
     });
+
+    test('show auth refresh toast', function(done) {
+      var refreshStub = sandbox.stub(element.$.restAPI, 'refreshCredentials',
+          function() { return Promise.resolve(true); });
+      var toastSpy = sandbox.spy(element, '_createToastAlert');
+      var windowOpen = sandbox.stub(window, 'open');
+      element.fire('server-error', {response: {status: 403}});
+      element.$.restAPI.getLoggedIn.lastCall.returnValue.then(function() {
+        assert.isTrue(toastSpy.called);
+        var toast = toastSpy.lastCall.returnValue;
+        assert.isOk(toast);
+        assert.include(
+            Polymer.dom(toast.root).textContent, 'Auth error');
+        assert.include(
+            Polymer.dom(toast.root).textContent, 'Refresh credentials.');
+
+        assert.isFalse(windowOpen.called);
+        toast.fire('action');
+        assert.isTrue(windowOpen.called);
+
+        var hideToastSpy = sandbox.spy(toast, 'hide');
+
+        assert.isTrue(refreshStub.called);
+        element.flushDebouncer('checkLoggedIn');
+        flush(function() {
+          assert.isTrue(refreshStub.called);
+          assert.isTrue(hideToastSpy.called);
+
+          assert.notStrictEqual(toastSpy.lastCall.returnValue, toast);
+          toast = toastSpy.lastCall.returnValue;
+          assert.isOk(toast);
+          assert.include(
+              Polymer.dom(toast.root).textContent, 'Credentials refreshed');
+          done();
+        });
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 5b70769..7291199 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -221,6 +221,13 @@
           </tr>
           <tr>
             <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">a</span>
+            </td>
+            <td>Hide/show left diff</td>
+          </tr>
+          <tr>
+            <td>
               <span class="key">c</span>
             </td>
             <td>Draft new comment</td>
@@ -277,6 +284,13 @@
           </tr>
           <tr>
             <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">a</span>
+            </td>
+            <td>Hide/show left diff</td>
+          </tr>
+          <tr>
+            <td>
               <span class="key">c</span>
             </td>
             <td>Draft new comment</td>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 930c8cf..916726fd 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -45,34 +45,38 @@
       .links {
         margin-left: 1em;
       }
-      .links ul {
+      .links .menuContainer {
         display: none;
       }
       .links > li {
         cursor: default;
         display: inline-block;
         margin-left: 1em;
-        padding: .4em 0;
+        padding: .5em 0;
         position: relative;
       }
-      .links li:hover ul {
+      .links li:hover .menuContainer,
+      .links li:active .menuContainer {
         background-color: #fff;
+        border-radius: 3px;
         box-shadow: 0 1px 1px rgba(0, 0, 0, .3);
         display: block;
-        left: -.75em;
+        left: -.5em;
+        padding: .5em 0;
         position: absolute;
-        top: 2em;
+        top: 2.45em;
         z-index: 1000;
       }
       .links li ul li a:link,
       .links li ul li a:visited {
         color: #00e;
         display: block;
-        padding: .5em .75em;
+        padding: .3em 1em;
         text-decoration: none;
         white-space: nowrap;
       }
-      .links li ul li:hover a {
+      .links li ul li:hover a,
+      .links li ul li:active a {
         background-color: var(--selection-background-color);
       }
       .linksTitle {
@@ -87,7 +91,8 @@
         height: 0;
         position: absolute;
         right: 0;
-        top: calc(50% - .1em);
+        top: calc(50% - .05em);
+        transition: border-top-color 200ms;
         width: 0;
       }
       .links li:hover .downArrow {
@@ -137,11 +142,13 @@
             <span class="linksTitle">
               [[linkGroup.title]] <i class="downArrow"></i>
             </span>
-            <ul>
-              <template is="dom-repeat" items="[[linkGroup.links]]" as="link">
-                <li><a href$="[[link.url]]">[[link.name]]</a></li>
-              </template>
-            </ul>
+            <div class="menuContainer">
+              <ul>
+                <template is="dom-repeat" items="[[linkGroup.links]]" as="link">
+                  <li><a href$="[[link.url]]">[[link.name]]</a></li>
+                </template>
+              </ul>
+            </div>
           </li>
         </template>
       </ul>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
new file mode 100644
index 0000000..7653655
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
@@ -0,0 +1,21 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-reporting">
+  <script src="gr-reporting.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
new file mode 100644
index 0000000..c7180a7
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -0,0 +1,95 @@
+// Copyright (C) 2016 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';
+
+  var APP_STARTED = 'App Started';
+  var PAGE_LOADED = 'Page Loaded';
+  var TIMING_EVENT = 'timing-report';
+  var DEFAULT_CATEGORY = 'UI Latency';
+  var DEFAULT_TYPE = 'timing';
+
+  Polymer({
+    is: 'gr-reporting',
+
+    properties: {
+      _baselines: {
+        type: Array,
+        value: function() { return {}; },
+      }
+    },
+
+    get performanceTiming() {
+      return window.performance.timing;
+    },
+
+    now: function() {
+      return Math.round(10 * window.performance.now()) / 10;
+    },
+
+    reporter: function(type, category, eventName, eventValue) {
+      eventValue = eventValue;
+      var detail = {
+        type: type,
+        category: category,
+        name: eventName,
+        value: eventValue,
+      };
+      document.dispatchEvent(
+          new CustomEvent(TIMING_EVENT, {detail: detail}));
+      console.log(eventName + ': ' + eventValue);
+    },
+
+    /**
+     * User-perceived app start time, should be reported when the app is ready.
+     */
+    appStarted: function() {
+      var startTime =
+          new Date().getTime() - this.performanceTiming.navigationStart;
+      this.reporter(
+          DEFAULT_TYPE, DEFAULT_CATEGORY, APP_STARTED, startTime);
+    },
+
+    /**
+     * Page load time, should be reported at any time after navigation.
+     */
+    pageLoaded: function() {
+      if (this.performanceTiming.loadEventEnd === 0) {
+        console.error('pageLoaded should be called after window.onload');
+        this.async(this.pageLoaded, 100);
+      } else {
+        var loadTime = this.performanceTiming.loadEventEnd -
+            this.performanceTiming.navigationStart;
+        this.reporter(DEFAULT_TYPE, DEFAULT_CATEGORY, PAGE_LOADED, loadTime);
+      }
+    },
+
+    /**
+     * Reset named timer.
+     */
+    time: function(name) {
+      this._baselines[name] = this.now();
+    },
+
+    /**
+     * Finish named timer and report it to server.
+     */
+    timeEnd: function(name) {
+      var baseTime = this._baselines[name] || 0;
+      var time = this.now() - baseTime;
+      this.reporter(DEFAULT_TYPE, DEFAULT_CATEGORY, name, time);
+      delete this._baselines[name];
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
new file mode 100644
index 0000000..e9226b8
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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-reporting</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="gr-reporting.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-reporting></gr-reporting>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-reporting tests', function() {
+    var element;
+    var sandbox;
+    var clock;
+    var fakePerformance;
+
+    var NOW_TIME = 100;
+
+    setup(function() {
+      sandbox = sinon.sandbox.create();
+      clock = sinon.useFakeTimers(NOW_TIME);
+      element = fixture('basic');
+      fakePerformance = {
+        navigationStart: 1,
+        loadEventEnd: 2,
+      };
+      sinon.stub(element, 'performanceTiming',
+          {get: function() {return fakePerformance;}});
+      sandbox.stub(element, 'reporter');
+    });
+    teardown(function() {
+      sandbox.restore();
+      clock.restore();
+    });
+
+    test('appStarted', function() {
+      element.appStarted();
+      assert.isTrue(
+          element.reporter.calledWithExactly(
+              'timing', 'UI Latency', 'App Started',
+              NOW_TIME - fakePerformance.navigationStart
+      ));
+    });
+
+    test('pageLoaded', function() {
+      element.pageLoaded();
+      assert.isTrue(
+          element.reporter.calledWithExactly(
+              'timing', 'UI Latency', 'Page Loaded',
+              fakePerformance.loadEventEnd - fakePerformance.navigationStart)
+      );
+    });
+
+    test('time and timeEnd', function() {
+      var nowStub = sinon.stub(element, 'now').returns(0);
+      element.time('foo');
+      nowStub.returns(1);
+      element.time('bar');
+      nowStub.returns(2);
+      element.timeEnd('bar');
+      nowStub.returns(3.123);
+      element.timeEnd('foo');
+      assert.isTrue(element.reporter.calledWithExactly(
+          'timing', 'UI Latency', 'foo', 3.123
+      ));
+      assert.isTrue(element.reporter.calledWithExactly(
+          'timing', 'UI Latency', 'bar', 1
+      ));
+    });
+  });
+</script>
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 32246ca..8441372 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -18,8 +18,12 @@
   // custom element having the id "app", but it is made explicit here.
   var app = document.querySelector('#app');
   var restAPI = document.createElement('gr-rest-api-interface');
+  var reporting = document.createElement('gr-reporting');
 
   window.addEventListener('WebComponentsReady', function() {
+    reporting.timeEnd('WebComponentsReady');
+    reporting.pageLoaded();
+
     // Middleware
     page(function(ctx, next) {
       document.body.scrollTop = 0;
@@ -40,6 +44,10 @@
 
     // Routes.
     page('/', loadUser, function(data) {
+      if (data.querystring.match(/^closeAfterLogin/)) {
+        // Close child window on redirect after login.
+        window.close();
+      }
       // For backward compatibility with GWT links.
       if (data.hash) {
         page.redirect(data.hash);
@@ -77,6 +85,13 @@
       page.redirect('/c/' + encodeURIComponent(ctx.params[0]));
     });
 
+    function normalizePatchRangeParams(params) {
+      if (params.basePatchNum && !params.patchNum) {
+        params.patchNum = params.basePatchNum;
+        params.basePatchNum = null;
+      }
+    }
+
     // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>].
     page(/^\/c\/(\d+)\/?(((\d+)(\.\.(\d+))?))?$/, function(ctx) {
       // Parameter order is based on the regex group number matched.
@@ -125,13 +140,6 @@
       app.params = params;
     });
 
-    function normalizePatchRangeParams(params) {
-      if (params.basePatchNum && !params.patchNum) {
-        params.patchNum = params.basePatchNum;
-        params.basePatchNum = null;
-      }
-    }
-
     page(/^\/settings\/?/, function(data) {
       restAPI.getLoggedIn().then(function(loggedIn) {
         if (loggedIn) {
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index f559794..2b05d99 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -15,9 +15,11 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.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">
+
 
 <dom-module id="gr-search-bar">
   <template>
@@ -28,13 +30,14 @@
       form {
         display: flex;
       }
-      input {
+      gr-autocomplete {
+        background-color: white;
         border: 1px solid #d1d2d3;
         border-radius: 2px 0 0 2px;
         flex: 1;
         font: inherit;
         outline: none;
-        padding: 0 .25em;
+        padding: 0 .25em 0 .25em;
       }
       gr-button {
         background-color: #f1f2f3;
@@ -43,8 +46,16 @@
       }
     </style>
     <form>
-      <input is="iron-input" id="searchInput" bind-value="{{_inputVal}}">
+      <gr-autocomplete
+          id="searchInput"
+          text="{{_inputVal}}"
+          query="[[query]]"
+          on-commit="_handleInputCommit"
+          allowNonSuggestedValues
+          multi
+          borderless></gr-autocomplete>
       <gr-button id="searchButton">Search</gr-button>
+      <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     </form>
   </template>
   <script src="gr-search-bar.js"></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 bef461d..e132f44 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
@@ -14,6 +14,74 @@
 (function() {
   'use strict';
 
+  // Possible static search options for auto complete.
+  var SEARCH_OPERATORS = [
+    'added',
+    'age',
+    'age:1week', // Give an example age
+    'author',
+    'branch',
+    'bug',
+    'change',
+    'comment',
+    'commentby',
+    'commit',
+    'committer',
+    'conflicts',
+    'deleted',
+    'delta',
+    'file',
+    'from',
+    'has',
+    'has:draft',
+    'has:edit',
+    'has:star',
+    'has:stars',
+    'intopic',
+    'is',
+    'is:abandoned',
+    'is:closed',
+    'is:draft',
+    'is:mergeable',
+    'is:merged',
+    'is:open',
+    'is:owner',
+    'is:pending',
+    'is:reviewed',
+    'is:reviewer',
+    'is:starred',
+    'is:watched',
+    'label',
+    'message',
+    'owner',
+    'ownerin',
+    'parentproject',
+    'project',
+    'projects',
+    'query',
+    'ref',
+    'reviewedby',
+    'reviewer',
+    'reviewer:self',
+    'reviewerin',
+    'size',
+    'star',
+    'status',
+    'status:abandoned',
+    'status:closed',
+    'status:draft',
+    'status:merged',
+    'status:open',
+    'status:pending',
+    'status:reviewed',
+    'topic',
+    'tr',
+  ];
+
+  var MAX_AUTOCOMPLETE_RESULTS = 10;
+
+  var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
+
   Polymer({
     is: 'gr-search-bar',
 
@@ -22,7 +90,6 @@
     ],
 
     listeners: {
-      'searchInput.keydown': '_inputKeyDownHandler',
       'searchButton.tap': '_preventDefaultAndNavigateToInputVal',
     },
 
@@ -37,7 +104,12 @@
         type: Object,
         value: function() { return document.body; },
       },
-
+      query: {
+        type: Function,
+        value: function() {
+          return this._getSearchSuggestions.bind(this);
+        },
+      },
       _inputVal: String,
     },
 
@@ -45,16 +117,155 @@
       this._inputVal = value;
     },
 
-    _inputKeyDownHandler: function(e) {
-      if (e.keyCode == 13) {  // Enter key
-        this._preventDefaultAndNavigateToInputVal(e);
-      }
+    _handleInputCommit: function(e) {
+      this._preventDefaultAndNavigateToInputVal(e);
     },
 
     _preventDefaultAndNavigateToInputVal: function(e) {
       e.preventDefault();
       Polymer.dom(e).rootTarget.blur();
-      page.show('/q/' + this._inputVal);
+      // @see Issue 4255.
+      page.show('/q/' + encodeURIComponent(encodeURIComponent(this._inputVal)));
+    },
+
+    /**
+     * Fetch from the API the predicted accounts.
+     * @param {string} predicate - The first part of the search term, e.g.
+     *     'owner'
+     * @param {string} expression - The second part of the search term, e.g.
+     *     'kasp'
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchAccounts: function(predicate, expression) {
+      if (expression.length === 0) { return Promise.resolve([]); }
+      return this.$.restAPI.getSuggestedAccounts(
+          expression,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(function(accounts) {
+            if (!accounts) { return []; }
+            return accounts.map(function(acct) {
+              return predicate + ':"' + acct.name + ' <' + acct.email + '>"';
+            });
+      });
+    },
+
+    /**
+     * Fetch from the API the predicted groups.
+     * @param {string} predicate - The first part of the search term, e.g.
+     *     'ownerin'
+     * @param {string} expression - The second part of the search term, e.g.
+     *     'polyger'
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchGroups: function(predicate, expression) {
+      if (expression.length === 0) { return Promise.resolve([]); }
+      return this.$.restAPI.getSuggestedGroups(
+          expression,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(function(groups) {
+            if (!groups) { return []; }
+            var keys = Object.keys(groups);
+            return keys.map(function(key) { return predicate + ':' + key; });
+          });
+    },
+
+    /**
+     * Fetch from the API the predicted projects.
+     * @param {string} predicate - The first part of the search term, e.g.
+     *     'project'
+     * @param {string} expression - The second part of the search term, e.g.
+     *     'gerr'
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchProjects: function(predicate, expression) {
+      return this.$.restAPI.getSuggestedProjects(
+          expression,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(function(projects) {
+            if (!projects) { return []; }
+            var keys = Object.keys(projects);
+            return keys.map(function(key) { return predicate + ':' + key; });
+          });
+    },
+
+    /**
+     * Determine what array of possible suggestions should be provided
+     *     to _getSearchSuggestions.
+     * @param {string} input - The full search term, in lowercase.
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchSuggestions: function(input) {
+      // Split the input on colon to get a two part predicate/expression.
+      var splitInput = input.split(':');
+      var predicate = splitInput[0];
+      var expression = splitInput[1] || '';
+      // Switch on the predicate to determine what to autocomplete.
+      switch (predicate) {
+        case 'ownerin':
+        case 'reviewerin':
+          // Fetch groups.
+          return this._fetchGroups(predicate, expression);
+
+        case 'parentproject':
+        case 'project':
+          // Fetch projects.
+          return this._fetchProjects(predicate, expression);
+
+        case 'author':
+        case 'commentby':
+        case 'committer':
+        case 'from':
+        case 'owner':
+        case 'reviewedby':
+        case 'reviewer':
+          // Fetch accounts.
+          return this._fetchAccounts(predicate, expression);
+
+        default:
+          return Promise.resolve(SEARCH_OPERATORS
+              .filter(function(operator) {
+                return operator.indexOf(input) !== -1;
+              }));
+      }
+    },
+
+    /**
+     * Get the sorted, pruned list of suggestions for the current search query.
+     * @param {string} input - The complete search query.
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _getSearchSuggestions: function(input) {
+      // Allow spaces within quoted terms.
+      var tokens = input.match(TOKENIZE_REGEX);
+      var trimmedInput = tokens[tokens.length - 1].toLowerCase();
+
+      return this._fetchSuggestions(trimmedInput)
+          .then(function(operators) {
+            if (!operators) { return []; }
+            return operators
+                // Disallow autocomplete values that exactly match the str.
+                .filter(function(operator) {
+                  return input.indexOf(operator.toLowerCase()) == -1;
+                })
+                // Prioritize results that start with the input.
+                .sort(function(operator) {
+                  return operator.indexOf(trimmedInput);
+                })
+                // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results.
+                .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1)
+                // Map to an object to play nice with gr-autocomplete.
+                .map(function(operator) {
+                  return {
+                    name: operator,
+                    value: operator,
+                  };
+                });
+          });
     },
 
     _handleKey: function(e) {
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
index 84752e4..60e29b2 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -68,8 +68,93 @@
         assert.notEqual(getActiveElement(), element.$.searchButton);
         done();
       });
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput, 13);
+      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13);
     });
 
+    test('search query should be double-escaped', function() {
+      var showStub = sinon.stub(page, 'show');
+      element.$.searchInput.text = 'fate/stay';
+      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13);
+      assert.equal(showStub.lastCall.args[0], '/q/fate%252Fstay');
+      showStub.restore();
+    });
+
+    suite('_getSearchSuggestions',
+        function() {
+      setup(function() {
+        sinon.stub(element.$.restAPI, 'getSuggestedAccounts', function() {
+          return Promise.resolve([
+            {
+              name: 'fred',
+              email: 'fred@goog.co',
+            },
+          ]);
+        });
+        sinon.stub(element.$.restAPI, 'getSuggestedGroups', function() {
+          return Promise.resolve({
+            Polygerrit: 0,
+          });
+        });
+        sinon.stub(element.$.restAPI, 'getSuggestedProjects', function() {
+          return Promise.resolve({
+            Polygerrit: 0,
+          });
+        });
+      });
+
+      teardown(function() {
+        element.$.restAPI.getSuggestedAccounts.restore();
+        element.$.restAPI.getSuggestedGroups.restore();
+        element.$.restAPI.getSuggestedProjects.restore();
+      });
+
+      test('Autocompletes accounts',
+          function(done) {
+        return element._getSearchSuggestions('owner:fr')
+            .then(function(suggestions) {
+              assert.equal(suggestions[0].value, 'owner:"fred <fred@goog.co>"');
+              done();
+            });
+      });
+
+      test('Autocompletes groups',
+          function(done) {
+        return element._getSearchSuggestions('ownerin:pol')
+            .then(function(suggestions) {
+              assert.equal(suggestions[0].value, 'ownerin:Polygerrit');
+              done();
+            });
+      });
+
+      test('Autocompletes projects',
+          function(done) {
+        return element._getSearchSuggestions('project:pol')
+            .then(function(suggestions) {
+              assert.equal(suggestions[0].value, 'project:Polygerrit');
+              done();
+            });
+      });
+
+      test('Autocompletes simple searches',
+          function(done) {
+        return element._getSearchSuggestions('is:o')
+            .then(function(suggestions) {
+              assert.equal(suggestions[0].name, 'is:open');
+              assert.equal(suggestions[0].value, 'is:open');
+              assert.equal(suggestions[1].name, 'is:owner');
+              assert.equal(suggestions[1].value, 'is:owner');
+              done();
+            });
+      });
+
+      test('Does not autocomplete with no match',
+          function(done) {
+        return element._getSearchSuggestions('asdasdasdasd')
+            .then(function(suggestions) {
+              assert.equal(suggestions.length, 0);
+              done();
+            });
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
index feb21e2..6b1e59e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
@@ -19,7 +19,7 @@
 
   function GrDiffBuilderImage(diff, comments, prefs, outputEl, baseImage,
       revisionImage) {
-    GrDiffBuilderSideBySide.call(this, diff, comments, prefs, outputEl);
+    GrDiffBuilderSideBySide.call(this, diff, comments, prefs, outputEl, []);
     this._baseImage = baseImage;
     this._revisionImage = revisionImage;
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
index 1044b77..1cb8cc7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
@@ -17,8 +17,8 @@
   // Prevent redefinition.
   if (window.GrDiffBuilderSideBySide) { return; }
 
-  function GrDiffBuilderSideBySide(diff, comments, prefs, outputEl) {
-    GrDiffBuilder.call(this, diff, comments, prefs, outputEl);
+  function GrDiffBuilderSideBySide(diff, comments, prefs, outputEl, layers) {
+    GrDiffBuilder.call(this, diff, comments, prefs, outputEl, layers);
   }
   GrDiffBuilderSideBySide.prototype = Object.create(GrDiffBuilder.prototype);
   GrDiffBuilderSideBySide.prototype.constructor = GrDiffBuilderSideBySide;
@@ -57,7 +57,7 @@
     if (action) {
       row.appendChild(action);
     } else {
-      var textEl = this._createTextEl(line);
+      var textEl = this._createTextEl(line, side);
       var threadEl = this._commentThreadForLine(line, side);
       if (threadEl) {
         textEl.appendChild(threadEl);
@@ -66,5 +66,17 @@
     }
   };
 
+  GrDiffBuilderSideBySide.prototype._getNextContentOnSide = function(
+      content, side) {
+    var tr = content.parentElement.parentElement;
+    var content;
+    while (tr = tr.nextSibling) {
+      content = tr.querySelector(
+          'td.content .contentText[data-side="' + side + '"]');
+      if (content) { return content; }
+    }
+    return null;
+  };
+
   window.GrDiffBuilderSideBySide = GrDiffBuilderSideBySide;
 })(window, GrDiffBuilder);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
index e69f369..960bf46 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
@@ -17,8 +17,8 @@
   // Prevent redefinition.
   if (window.GrDiffBuilderUnified) { return; }
 
-  function GrDiffBuilderUnified(diff, comments, prefs, outputEl) {
-    GrDiffBuilder.call(this, diff, comments, prefs, outputEl);
+  function GrDiffBuilderUnified(diff, comments, prefs, outputEl, layers) {
+    GrDiffBuilder.call(this, diff, comments, prefs, outputEl, layers);
   }
   GrDiffBuilderUnified.prototype = Object.create(GrDiffBuilder.prototype);
   GrDiffBuilderUnified.prototype.constructor = GrDiffBuilderUnified;
@@ -59,5 +59,19 @@
     return row;
   };
 
+  GrDiffBuilderUnified.prototype._getNextContentOnSide = function(
+      content, side) {
+    var tr = content.parentElement.parentElement;
+    var content;
+    while (tr = tr.nextSibling) {
+      if (tr.classList.contains('both') || (
+          (side === 'left' && tr.classList.contains('remove')) ||
+          (side === 'right' && tr.classList.contains('add')))) {
+        return tr.querySelector('.contentText');
+      }
+    }
+    return null;
+  };
+
   window.GrDiffBuilderUnified = GrDiffBuilderUnified;
 })(window, GrDiffBuilder);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index 832665b..18c0602 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -14,19 +14,31 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
+<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
 <link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
+<link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
+<link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html">
 
 <dom-module id="gr-diff-builder">
   <template>
     <div class="contentWrapper">
       <content></content>
     </div>
+    <gr-ranged-comment-layer
+        id="rangeLayer"
+        comments="[[comments]]"></gr-ranged-comment-layer>
+    <gr-syntax-layer
+        id="syntaxLayer"
+        diff="[[diff]]"></gr-syntax-layer>
     <gr-diff-processor
         id="processor"
         groups="{{_groups}}"></gr-diff-processor>
+    <gr-reporting id="reporting"></gr-reporting>
   </template>
   <script src="../gr-diff/gr-diff-line.js"></script>
   <script src="../gr-diff/gr-diff-group.js"></script>
+  <script src="../gr-diff-highlight/gr-annotation.js"></script>
   <script src="gr-diff-builder.js"></script>
   <script src="gr-diff-builder-side-by-side.js"></script>
   <script src="gr-diff-builder-unified.js"></script>
@@ -40,6 +52,12 @@
         UNIFIED: 'UNIFIED_DIFF',
       };
 
+      var TimingLabel = {
+        TOTAL: 'Diff Total Render',
+        CONTENT: 'Diff Content Render',
+        SYNTAX: 'Diff Syntax Render',
+      };
+
       Polymer({
         is: 'gr-diff-builder',
 
@@ -50,12 +68,15 @@
          */
 
         properties: {
+          diff: Object,
           viewMode: String,
+          comments: Object,
           isImageDiff: Boolean,
           baseImage: Object,
           revisionImage: Object,
           _builder: Object,
           _groups: Array,
+          _layers: Array,
         },
 
         get diffElement() {
@@ -66,21 +87,47 @@
           '_groupsChanged(_groups.splices)',
         ],
 
-        render: function(diff, comments, prefs) {
+        attached: function() {
+          // Setup annotation layers.
+          this._layers = [
+            this.$.syntaxLayer,
+            this._createIntralineLayer(),
+            this.$.rangeLayer,
+          ];
+
+          this.async(function() {
+            this._preRenderThread();
+          });
+        },
+
+        render: function(comments, prefs) {
+          this.$.syntaxLayer.enabled = prefs.syntax_highlighting;
+
           // Stop the processor (if it's running).
           this.$.processor.cancel();
+          this.$.syntaxLayer.cancel();
 
-          this._builder = this._getDiffBuilder(diff, comments, prefs);
+          this._builder = this._getDiffBuilder(this.diff, comments, prefs);
 
           this.$.processor.context = prefs.context;
           this.$.processor.keyLocations = this._getCommentLocations(comments);
 
           this._clearDiffContent();
 
-          this.$.processor.process(diff.content).then(function() {
+          var reporting = this.$.reporting;
+
+          reporting.time(TimingLabel.TOTAL);
+          reporting.time(TimingLabel.CONTENT);
+          return this.$.processor.process(this.diff.content).then(function() {
             if (this.isImageDiff) {
               this._builder.renderDiffImages();
             }
+            reporting.timeEnd(TimingLabel.CONTENT);
+            reporting.time(TimingLabel.SYNTAX);
+            this.$.syntaxLayer.process().then(function() {
+              reporting.timeEnd(TimingLabel.SYNTAX);
+              reporting.timeEnd(TimingLabel.TOTAL);
+            });
             this.fire('render');
           }.bind(this));
         },
@@ -139,10 +186,7 @@
         },
 
         getContentByLine: function(lineNumber, opt_side, opt_root) {
-          var root = Polymer.dom(opt_root || this.diffElement);
-          var sideSelector = !!opt_side ? ('.' + opt_side) : '';
-          return root.querySelector('td.lineNum[data-value="' + lineNumber +
-              '"]' + sideSelector + ' ~ td.content');
+          return this._builder.getContentByLine(lineNumber, opt_side, opt_root);
         },
 
         getContentByLineEl: function(lineEl) {
@@ -159,29 +203,21 @@
         },
 
         getContentsByLineRange: function(startLine, endLine, opt_side) {
-          var groups =
-              this._builder.getGroupsByLineRange(startLine, endLine, opt_side);
-          // In each group, search Element for lines in range.
-          return groups.reduce((function(acc, group) {
-            for (var line = startLine; line <= endLine; line++) {
-              var content =
-                  this.getContentByLine(line, opt_side, group.element);
-              if (content) {
-                acc.push(content);
-              }
-            }
-            return acc;
-          }).bind(this), []);
+          var result = [];
+          this._builder.findLinesByRange(startLine, endLine, opt_side, null,
+              result);
+          return result;
         },
 
         getCommentThreadByLine: function(lineNumber, opt_side, opt_root) {
-          var root = Polymer.dom(opt_root || this.diffElement);
-          var sideSelector = !!opt_side ? ('.' + opt_side) : '';
           var content = this.getContentByLine(lineNumber, opt_side, opt_root);
           return this.getCommentThreadByContentEl(content);
         },
 
         getCommentThreadByContentEl: function(contentEl) {
+          if (contentEl.classList.contains('contentText')) {
+            contentEl = contentEl.parentElement;
+          }
           return contentEl.querySelector('gr-diff-comment-thread');
         },
 
@@ -224,10 +260,10 @@
                 this.diffElement, this.baseImage, this.revisionImage);
           } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
             return new GrDiffBuilderSideBySide(
-                diff, comments, prefs, this.diffElement);
+                diff, comments, prefs, this.diffElement, this._layers);
           } else if (this.viewMode === DiffViewMode.UNIFIED) {
             return new GrDiffBuilderUnified(
-                diff, comments, prefs, this.diffElement);
+                diff, comments, prefs, this.diffElement, this._layers);
           }
           throw Error('Unsupported diff view mode: ' + this.viewMode);
         },
@@ -264,6 +300,54 @@
             }
           }, this);
         },
+
+        _createIntralineLayer: function() {
+          return {
+            addListener: function() {},
+
+            // Take a DIV.contentText element and a line object with intraline
+            // differences to highlight and apply them to the element as
+            // annotations.
+            annotate: function(el, line) {
+              var HL_CLASS = 'style-scope gr-diff intraline';
+              line.highlights.forEach(function(highlight) {
+                // The start and end indices could be the same if a highlight is
+                // meant to start at the end of a line and continue onto the
+                // next one. Ignore it.
+                if (highlight.startIndex === highlight.endIndex) { return; }
+
+                // If endIndex isn't present, continue to the end of the line.
+                var endIndex = highlight.endIndex === undefined ?
+                    line.text.length : highlight.endIndex;
+
+                GrAnnotation.annotateElement(
+                    el,
+                    highlight.startIndex,
+                    endIndex - highlight.startIndex,
+                    HL_CLASS);
+              });
+            },
+          };
+        },
+
+        /**
+         * In pages with large diffs, creating the first comment thread can be
+         * slow because nested Polymer elements (particularly
+         * iron-autogrow-textarea) add style elements to the document head,
+         * which, in turn, triggers a reflow on the page. Create a hidden
+         * thread, attach it to the page, and remove it so the stylesheet will
+         * already exist and the user's comment will be quick to load.
+         * @see https://gerrit-review.googlesource.com/c/82213/
+         */
+        _preRenderThread: function() {
+          var thread = document.createElement('gr-diff-comment-thread');
+          thread.setAttribute('hidden', true);
+          thread.addDraft();
+          var parent = Polymer.dom(this.root);
+          parent.appendChild(thread);
+          Polymer.dom.flush();
+          parent.removeChild(thread);
+        },
       });
     })();
   </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index f68825c..2090e98 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -17,12 +17,20 @@
   // Prevent redefinition.
   if (window.GrDiffBuilder) { return; }
 
-  function GrDiffBuilder(diff, comments, prefs, outputEl) {
+  var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+
+  function GrDiffBuilder(diff, comments, prefs, outputEl, layers) {
     this._diff = diff;
     this._comments = comments;
     this._prefs = prefs;
     this._outputEl = outputEl;
     this.groups = [];
+
+    this.layers = layers || [];
+
+    this.layers.forEach(function(layer) {
+      layer.addListener(this._handleLayerUpdate.bind(this));
+    }.bind(this));
   }
 
   GrDiffBuilder.LESS_THAN_CODE = '<'.charCodeAt(0);
@@ -30,8 +38,6 @@
   GrDiffBuilder.AMPERSAND_CODE = '&'.charCodeAt(0);
   GrDiffBuilder.SEMICOLON_CODE = ';'.charCodeAt(0);
 
-  GrDiffBuilder.TAB_REGEX = /\t/g;
-
   GrDiffBuilder.LINE_FEED_HTML =
       '<span class="style-scope gr-diff br"></span>';
 
@@ -91,26 +97,11 @@
       }
       var groupStartLine = 0;
       var groupEndLine = 0;
-      switch (group.type) {
-        case GrDiffGroup.Type.BOTH:
-          if (opt_side === GrDiffBuilder.Side.LEFT) {
-            groupStartLine = group.lines[0].beforeNumber;
-            groupEndLine = group.lines[group.lines.length - 1].beforeNumber;
-          } else if (opt_side === GrDiffBuilder.Side.RIGHT) {
-            groupStartLine = group.lines[0].afterNumber;
-            groupEndLine = group.lines[group.lines.length - 1].afterNumber;
-          }
-          break;
-        case GrDiffGroup.Type.DELTA:
-          if (opt_side === GrDiffBuilder.Side.LEFT && group.removes.length) {
-            groupStartLine = group.removes[0].beforeNumber;
-            groupEndLine = group.removes[group.removes.length - 1].beforeNumber;
-          } else if (group.adds.length) {
-            groupStartLine = group.adds[0].afterNumber;
-            groupEndLine = group.adds[group.adds.length - 1].afterNumber;
-          }
-          break;
+      if (opt_side) {
+        groupStartLine = group.lineRange[opt_side].start;
+        groupEndLine = group.lineRange[opt_side].end;
       }
+
       if (groupStartLine === 0) { // Line was removed or added.
         groupStartLine = groupEndLine;
       }
@@ -124,6 +115,71 @@
     return groups;
   };
 
+  GrDiffBuilder.prototype.getContentByLine = function(lineNumber, opt_side,
+      opt_root) {
+    var root = Polymer.dom(opt_root || this._outputEl);
+    var sideSelector = !!opt_side ? ('.' + opt_side) : '';
+    return root.querySelector('td.lineNum[data-value="' + lineNumber +
+        '"]' + sideSelector + ' ~ td.content .contentText');
+  };
+
+  /**
+   * Find line elements or line objects by a range of line numbers and a side.
+   *
+   * @param {Number} start The first line number
+   * @param {Number} end The last line number
+   * @param {String} opt_side The side of the range. Either 'left' or 'right'.
+   * @param {Array<GrDiffLine>} out_lines The output list of line objects. Use
+   *     null if not desired.
+   * @param  {Array<HTMLElement>} out_elements The output list of line elements.
+   *     Use null if not desired.
+   */
+  GrDiffBuilder.prototype.findLinesByRange = function(start, end, opt_side,
+      out_lines, out_elements) {
+    var groups = this.getGroupsByLineRange(start, end, opt_side);
+    groups.forEach(function(group) {
+      var content = null;
+      group.lines.forEach(function(line) {
+        if ((opt_side === 'left' && line.type === GrDiffLine.Type.ADD) ||
+            (opt_side === 'right' && line.type === GrDiffLine.Type.REMOVE)) {
+          return;
+        }
+        var lineNumber = opt_side === 'left' ?
+            line.beforeNumber : line.afterNumber;
+        if (lineNumber < start || lineNumber > end) { return; }
+
+        if (out_lines) { out_lines.push(line); }
+        if (out_elements) {
+          if (content) {
+            content = this._getNextContentOnSide(content, opt_side);
+          } else {
+            content = this.getContentByLine(lineNumber, opt_side,
+                group.element);
+          }
+          if (content) { out_elements.push(content); }
+        }
+      }.bind(this));
+    }.bind(this));
+  };
+
+  /**
+   * Re-renders the DIV.contentText alement for the given side and range of diff
+   * content.
+   */
+  GrDiffBuilder.prototype._renderContentByRange = function(start, end, side) {
+    var lines = [];
+    var elements = [];
+    var line;
+    var el;
+    this.findLinesByRange(start, end, side, lines, elements);
+    for (var i = 0; i < lines.length; i++) {
+      line = lines[i];
+      el = elements[i];
+      el.parentElement.replaceChild(this._createTextEl(line, side).firstChild,
+          el);
+    }
+  };
+
   GrDiffBuilder.prototype.getSectionsByLineRange = function(
       startLine, endLine, opt_side) {
     return this.getGroupsByLineRange(startLine, endLine, opt_side).map(
@@ -305,7 +361,7 @@
     return td;
   };
 
-  GrDiffBuilder.prototype._createTextEl = function(line) {
+  GrDiffBuilder.prototype._createTextEl = function(line, opt_side) {
     var td = this._createElement('td');
     if (line.type !== GrDiffLine.Type.BLANK) {
       td.classList.add('content');
@@ -313,36 +369,48 @@
     td.classList.add(line.type);
     var text = line.text;
     var html = util.escapeHTML(text);
-
-    td.classList.add(line.highlights.length > 0 ?
-        'lightHighlight' : 'darkHighlight');
-
-    if (line.highlights.length > 0) {
-      html = this._addIntralineHighlights(text, html, line.highlights);
-    }
+    html = this._addTabWrappers(html, this._prefs.tab_size);
 
     if (this._textLength(text, this._prefs.tab_size) >
         this._prefs.line_length) {
       html = this._addNewlines(text, html);
     }
-    html = this._addTabWrappers(html);
+
+    var contentText = this._createElement('div', 'contentText');
+    if (opt_side) {
+      contentText.setAttribute('data-side', opt_side);
+    }
 
     // If the html is equivalent to the text then it didn't get highlighted
     // or escaped. Use textContent which is faster than innerHTML.
     if (html === text) {
-      td.textContent = text;
+      contentText.textContent = text;
     } else {
-      td.innerHTML = html;
+      contentText.innerHTML = html;
     }
+
+    td.classList.add(line.highlights.length > 0 ?
+        'lightHighlight' : 'darkHighlight');
+
+    this.layers.forEach(function(layer) {
+      layer.annotate(contentText, line);
+    });
+
+    td.appendChild(contentText);
+
     return td;
   };
 
+  /**
+   * Returns the text length after normalizing unicode and tabs.
+   * @return {Number} The normalized length of the text.
+   */
   GrDiffBuilder.prototype._textLength = function(text, tabSize) {
-    // TODO(andybons): Unicode support.
+    text = text.replace(REGEX_ASTRAL_SYMBOL, '_');
     var numChars = 0;
     for (var i = 0; i < text.length; i++) {
       if (text[i] === '\t') {
-        numChars += tabSize;
+        numChars += tabSize - (numChars % tabSize);
       } else {
         numChars++;
       }
@@ -405,45 +473,34 @@
     return result;
   };
 
-  GrDiffBuilder.prototype._addTabWrappers = function(html) {
-    var htmlStr = this._getTabWrapper(this._prefs.tab_size,
-        this._prefs.show_tabs);
-    return html.replace(GrDiffBuilder.TAB_REGEX, htmlStr);
-  };
+  /**
+   * Takes a string of text (not HTML) and returns a string of HTML with tab
+   * elements in place of tab characters. In each case tab elements are given
+   * the width needed to reach the next tab-stop.
+   *
+   * @param {String} A line of text potentially containing tab characters.
+   * @param {Number} The width for tabs.
+   * @return {String} An HTML string potentially containing tab elements.
+   */
+  GrDiffBuilder.prototype._addTabWrappers = function(line, tabSize) {
+    if (!line.length) { return ''; }
 
-  GrDiffBuilder.prototype._addIntralineHighlights = function(content, html,
-      highlights) {
-    var START_TAG = '<hl class="style-scope gr-diff">';
-    var END_TAG = '</hl>';
+    var result = '';
+    var offset = 0;
+    var split = line.split('\t');
+    var width;
 
-    for (var i = 0; i < highlights.length; i++) {
-      var hl = highlights[i];
-
-      var htmlStartIndex = 0;
-      // Find the index of the HTML string to insert the start tag.
-      for (var j = 0; j < hl.startIndex; j++) {
-        htmlStartIndex = this._advanceChar(html, htmlStartIndex);
-      }
-
-      var htmlEndIndex = 0;
-      if (hl.endIndex !== undefined) {
-        for (var j = 0; j < hl.endIndex; j++) {
-          htmlEndIndex = this._advanceChar(html, htmlEndIndex);
-        }
-      } else {
-        // If endIndex isn't present, continue to the end of the line.
-        htmlEndIndex = html.length;
-      }
-      // The start and end indices could be the same if a highlight is meant
-      // to start at the end of a line and continue onto the next one.
-      // Ignore it.
-      if (htmlStartIndex !== htmlEndIndex) {
-        html = html.slice(0, htmlStartIndex) + START_TAG +
-              html.slice(htmlStartIndex, htmlEndIndex) + END_TAG +
-              html.slice(htmlEndIndex);
-      }
+    for (var i = 0; i < split.length - 1; i++) {
+      offset += split[i].length;
+      width = tabSize - (offset % tabSize);
+      result += split[i] + this._getTabWrapper(width, this._prefs.show_tabs);
+      offset += width;
     }
-    return html;
+    if (split.length) {
+      result += split[split.length - 1];
+    }
+
+    return result;
   };
 
   GrDiffBuilder.prototype._getTabWrapper = function(tabSize, showTabs) {
@@ -480,5 +537,20 @@
     return el;
   };
 
+  GrDiffBuilder.prototype._handleLayerUpdate = function(start, end, side) {
+    this._renderContentByRange(start, end, side);
+  };
+
+  /**
+   * Finds the next DIV.contentText element following the given element, and on
+   * the same side. Will only search within a group.
+   * @param {HTMLElement} content
+   * @param {String} side Either 'left' or 'right'
+   * @return {HTMLElement}
+   */
+  GrDiffBuilder.prototype._getNextContentOnSide = function(content, side) {
+    throw Error('Subclasses must implement _getNextContentOnSide');
+  };
+
   window.GrDiffBuilder = GrDiffBuilder;
 })(window, GrDiffGroup, GrDiffLine);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index cda4dc8..187a5cd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -20,10 +20,13 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
 <script src="../gr-diff/gr-diff-line.js"></script>
 <script src="../gr-diff/gr-diff-group.js"></script>
+<script src="../gr-diff-highlight/gr-annotation.js"></script>
 <script src="gr-diff-builder.js"></script>
 
+<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
 <link rel="import" href="gr-diff-builder.html">
 
 <test-fixture id="basic">
@@ -34,9 +37,23 @@
   </template>
 </test-fixture>
 
+<test-fixture id="div-with-text">
+  <template>
+    <div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
+  </template>
+</test-fixture>
+
+<test-fixture id="mock-diff">
+  <template>
+    <gr-diff-builder view-mode="SIDE_BY_SIDE">
+      <table id="diffTable"></table>
+    </gr-diff-builder>
+  </template>
+</test-fixture>
 
 <script>
   suite('gr-diff-builder tests', function() {
+    var element;
     var builder;
 
     setup(function() {
@@ -104,19 +121,29 @@
           '6789');
     });
 
-    test('text length with tabs', function() {
+    test('text length with tabs and unicode', function() {
       assert.equal(builder._textLength('12345', 4), 5);
       assert.equal(builder._textLength('\t\t12', 4), 10);
+      assert.equal(builder._textLength('abc💢123', 4), 7);
+
+      assert.equal(builder._textLength('abc\t', 8), 8);
+      assert.equal(builder._textLength('abc\t\t', 10), 20);
+      assert.equal(builder._textLength('', 10), 0);
+      assert.equal(builder._textLength('', 10), 0);
+      assert.equal(builder._textLength('abc\tde', 10), 12);
+      assert.equal(builder._textLength('abc\tde\t', 10), 20);
+      assert.equal(builder._textLength('\t\t\t\t\t', 20), 100);
     });
 
     test('tab wrapper insertion', function() {
       var html = 'abc\tdef';
       var wrapper = builder._getTabWrapper(
-          builder._prefs.tab_size,
+          builder._prefs.tab_size - 3,
           builder._prefs.show_tabs);
       assert.ok(wrapper);
       assert.isAbove(wrapper.length, 0);
-      assert.equal(builder._addTabWrappers(html), 'abc' + wrapper + 'def');
+      assert.equal(builder._addTabWrappers(html, builder._prefs.tab_size),
+          'abc' + wrapper + 'def');
       assert.throws(builder._getTabWrapper.bind(
           builder,
           // using \x3c instead of < in string so gjslint can parse
@@ -155,6 +182,10 @@
     });
 
     test('comment thread creation', function() {
+      var l3 = {id: 'l3', line: 3, updated: '2016-08-09 00:42:32.000000000'};
+      var l5 = {id: 'l5', line: 5, updated: '2016-08-09 00:42:32.000000000'};
+      var r5 = {id: 'r5', line: 5, updated: '2016-08-09 00:42:32.000000000'};
+
       builder._comments = {
         meta: {
           changeNum: '42',
@@ -165,13 +196,8 @@
           path: '/path/to/foo',
           projectConfig: {foo: 'bar'},
         },
-        left: [
-          {id: 'l3', line: 3},
-          {id: 'l5', line: 5},
-        ],
-        right: [
-          {id: 'r5', line: 5},
-        ],
+        left: [l3, l5],
+        right: [r5],
       };
 
       function checkThreadProps(threadEl, patchNum, side, comments) {
@@ -187,26 +213,24 @@
       line.beforeNumber = 5;
       line.afterNumber = 5;
       var threadEl = builder._commentThreadForLine(line);
-      checkThreadProps(threadEl, '3', 'REVISION',
-          [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
+      checkThreadProps(threadEl, '3', 'REVISION', [l5, r5]);
 
       threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT);
-      checkThreadProps(threadEl, '3', 'REVISION', [{id: 'r5', line: 5}]);
+      checkThreadProps(threadEl, '3', 'REVISION', [r5]);
 
       threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT);
-      checkThreadProps(threadEl, '3', 'PARENT', [{id: 'l5', line: 5}]);
+      checkThreadProps(threadEl, '3', 'PARENT', [l5]);
 
       builder._comments.meta.patchRange.basePatchNum = '1';
 
       threadEl = builder._commentThreadForLine(line);
-      checkThreadProps(threadEl, '3', 'REVISION',
-          [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
+      checkThreadProps(threadEl, '3', 'REVISION', [l5, r5]);
 
       threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT);
-      checkThreadProps(threadEl, '1', 'REVISION', [{id: 'l5', line: 5}]);
+      checkThreadProps(threadEl, '1', 'REVISION', [l5]);
 
       threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT);
-      checkThreadProps(threadEl, '3', 'REVISION', [{id: 'r5', line: 5}]);
+      checkThreadProps(threadEl, '3', 'REVISION', [r5]);
 
       builder._comments.meta.patchRange.basePatchNum = 'PARENT';
 
@@ -214,15 +238,180 @@
       line.beforeNumber = 5;
       line.afterNumber = 5;
       threadEl = builder._commentThreadForLine(line);
-      checkThreadProps(threadEl, '3', 'PARENT',
-          [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
+      checkThreadProps(threadEl, '3', 'PARENT', [l5, r5]);
 
       line = new GrDiffLine(GrDiffLine.Type.ADD);
       line.beforeNumber = 3;
       line.afterNumber = 5;
       threadEl = builder._commentThreadForLine(line);
-      checkThreadProps(threadEl, '3', 'REVISION',
-          [{id: 'l3', line: 3}, {id: 'r5', line: 5}]);
+      checkThreadProps(threadEl, '3', 'REVISION', [l3, r5]);
+    });
+
+    suite('intraline differences', function() {
+      var el;
+      var str;
+      var annotateElementSpy;
+      var layer;
+
+      function slice(str, start, end) {
+        return Array.from(str).slice(start, end).join('');
+      }
+
+      setup(function() {
+        el = fixture('div-with-text');
+        str = el.textContent;
+        annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
+        layer = document.createElement('gr-diff-builder')
+            ._createIntralineLayer();
+      });
+
+      teardown(function() {
+        annotateElementSpy.restore();
+      });
+
+      test('annotate no highlights', function() {
+        var line = {
+          text: str,
+          highlights: [],
+        };
+
+        layer.annotate(el, line);
+
+        // The content is unchanged.
+        assert.isFalse(annotateElementSpy.called);
+        assert.equal(el.childNodes.length, 1);
+        assert.instanceOf(el.childNodes[0], Text);
+        assert.equal(str, el.childNodes[0].textContent);
+      });
+
+      test('annotate with highlights', function() {
+        var line = {
+          text: str,
+          highlights: [
+            {startIndex: 6, endIndex: 12},
+            {startIndex: 18, endIndex: 22},
+          ],
+        };
+        var str0 = slice(str, 0, 6);
+        var str1 = slice(str, 6, 12);
+        var str2 = slice(str, 12, 18);
+        var str3 = slice(str, 18, 22);
+        var str4 = slice(str, 22);
+
+        layer.annotate(el, line);
+
+        assert.isTrue(annotateElementSpy.called);
+        assert.equal(el.childNodes.length, 5);
+
+        assert.instanceOf(el.childNodes[0], Text);
+        assert.equal(el.childNodes[0].textContent, str0);
+
+        assert.notInstanceOf(el.childNodes[1], Text);
+        assert.equal(el.childNodes[1].textContent, str1);
+
+        assert.instanceOf(el.childNodes[2], Text);
+        assert.equal(el.childNodes[2].textContent, str2);
+
+        assert.notInstanceOf(el.childNodes[3], Text);
+        assert.equal(el.childNodes[3].textContent, str3);
+
+        assert.instanceOf(el.childNodes[4], Text);
+        assert.equal(el.childNodes[4].textContent, str4);
+      });
+
+      test('annotate without endIndex', function() {
+        var line = {
+          text: str,
+          highlights: [
+            {startIndex: 28},
+          ],
+        };
+
+        var str0 = slice(str, 0, 28);
+        var str1 = slice(str, 28);
+
+        layer.annotate(el, line);
+
+        assert.isTrue(annotateElementSpy.called);
+        assert.equal(el.childNodes.length, 2);
+
+        assert.instanceOf(el.childNodes[0], Text);
+        assert.equal(el.childNodes[0].textContent, str0);
+
+        assert.notInstanceOf(el.childNodes[1], Text);
+        assert.equal(el.childNodes[1].textContent, str1);
+      });
+
+      test('annotate ignores empty highlights', function() {
+        var line = {
+          text: str,
+          highlights: [
+            {startIndex: 28, endIndex: 28},
+          ],
+        };
+
+        layer.annotate(el, line);
+
+        assert.isFalse(annotateElementSpy.called);
+        assert.equal(el.childNodes.length, 1);
+      });
+
+      test('annotate handles unicode', function() {
+        // Put some unicode into the string:
+        str = str.replace(/\s/g, '💢');
+        el.textContent = str;
+        var line = {
+          text: str,
+          highlights: [
+            {startIndex: 6, endIndex: 12},
+          ],
+        };
+
+        var str0 = slice(str, 0, 6);
+        var str1 = slice(str, 6, 12);
+        var str2 = slice(str, 12);
+
+        layer.annotate(el, line);
+
+        assert.isTrue(annotateElementSpy.called);
+        assert.equal(el.childNodes.length, 3);
+
+        assert.instanceOf(el.childNodes[0], Text);
+        assert.equal(el.childNodes[0].textContent, str0);
+
+        assert.notInstanceOf(el.childNodes[1], Text);
+        assert.equal(el.childNodes[1].textContent, str1);
+
+        assert.instanceOf(el.childNodes[2], Text);
+        assert.equal(el.childNodes[2].textContent, str2);
+      });
+
+      test('annotate handles unicode w/o endIndex', function() {
+        // Put some unicode into the string:
+        str = str.replace(/\s/g, '💢');
+        el.textContent = str;
+
+        var line = {
+          text: str,
+          highlights: [
+            {startIndex: 6},
+          ],
+        };
+
+        var str0 = slice(str, 0, 6);
+        var str1 = slice(str, 6);
+
+        layer.annotate(el, line);
+
+        assert.isTrue(annotateElementSpy.called);
+        assert.equal(el.childNodes.length, 2);
+
+        assert.instanceOf(el.childNodes[0], Text);
+        assert.equal(el.childNodes[0].textContent, str0);
+
+        assert.notInstanceOf(el.childNodes[1], Text);
+        assert.equal(el.childNodes[1].textContent, str1);
+      });
     });
 
     suite('rendering', function() {
@@ -248,6 +437,10 @@
             ]
           },
         ];
+        stub('gr-reporting', {
+          time: sinon.stub(),
+          timeEnd: sinon.stub(),
+        });
         element = fixture('basic');
         outputEl = element.queryEffectiveChildren('#diffTable');
         element.addEventListener('render', function() {
@@ -265,7 +458,22 @@
           };
           return builder;
         });
-        element.render({ content: content }, {left: [], right: []}, prefs);
+        element.diff = {content: content};
+        element.render({left: [], right: []}, prefs);
+      });
+
+      test('reporting', function(done) {
+        var timeStub = element.$.reporting.time;
+        var timeEndStub = element.$.reporting.timeEnd;
+        flush(function() {
+          assert.isTrue(timeStub.calledWithExactly('Diff Total Render'));
+          assert.isTrue(timeStub.calledWithExactly('Diff Content Render'));
+          assert.isTrue(timeStub.calledWithExactly('Diff Syntax Render'));
+          assert.isTrue(timeEndStub.calledWithExactly('Diff Total Render'));
+          assert.isTrue(timeEndStub.calledWithExactly('Diff Content Render'));
+          assert.isTrue(timeEndStub.calledWithExactly('Diff Syntax Render'));
+          done();
+        });
       });
 
       test('renderSection', function() {
@@ -295,5 +503,145 @@
         assert.strictEqual(sections[1], section[1]);
       });
     });
+
+    suite('mock-diff', function() {
+      var element;
+      var builder;
+      var diff;
+      var prefs;
+
+      setup(function(done) {
+        element = fixture('mock-diff');
+        diff = document.createElement('mock-diff-response').diffResponse;
+        element.diff = diff;
+
+        prefs = {
+          line_length: 80,
+          show_tabs: true,
+          tab_size: 4,
+        };
+
+        element.render({left: [], right: []}, prefs).then(function() {
+          builder = element._builder;
+          done();
+        });
+      });
+
+      test('getContentByLine', function() {
+        var actual;
+
+        actual = builder.getContentByLine(2, 'left');
+        assert.equal(actual.textContent, diff.content[0].ab[1]);
+
+        actual = builder.getContentByLine(2, 'right');
+        assert.equal(actual.textContent, diff.content[0].ab[1]);
+
+        actual = builder.getContentByLine(5, 'left');
+        assert.equal(actual.textContent, diff.content[2].ab[0]);
+
+        actual = builder.getContentByLine(5, 'right');
+        assert.equal(actual.textContent, diff.content[1].b[0]);
+      });
+
+      test('findLinesByRange', function() {
+        var lines = [];
+        var elems = [];
+        var start = 6;
+        var end = 10;
+        var count = end - start + 1;
+
+        builder.findLinesByRange(start, end, 'right', lines, elems);
+
+        assert.equal(lines.length, count);
+        assert.equal(elems.length, count);
+
+        for (var i = 0; i < 5; i++) {
+          assert.instanceOf(lines[i], GrDiffLine);
+          assert.equal(lines[i].afterNumber, start + i);
+          assert.instanceOf(elems[i], HTMLElement);
+          assert.equal(lines[i].text, elems[i].textContent);
+        }
+      });
+
+      test('_renderContentByRange', function() {
+        var spy = sinon.spy(builder, '_createTextEl');
+        var start = 9;
+        var end = 14;
+        var count = end - start + 1;
+
+        builder._renderContentByRange(start, end, 'left');
+
+        assert.equal(spy.callCount, count);
+        spy.getCalls().forEach(function(call, i) {
+          assert.equal(call.args[0].beforeNumber, start + i);
+        });
+
+        spy.restore();
+      });
+
+      test('_getNextContentOnSide side-by-side left', function() {
+        var startElem = builder.getContentByLine(5, 'left',
+            element.$.diffTable);
+        var expectedStartString = diff.content[2].ab[0];
+        var expectedNextString = diff.content[2].ab[1];
+        assert.equal(startElem.textContent, expectedStartString);
+
+        var nextElem = builder._getNextContentOnSide(startElem,
+            'left');
+        assert.equal(nextElem.textContent, expectedNextString);
+      });
+
+      test('_getNextContentOnSide side-by-side right', function() {
+        var startElem = builder.getContentByLine(5, 'right',
+            element.$.diffTable);
+        var expectedStartString = diff.content[1].b[0];
+        var expectedNextString = diff.content[1].b[1];
+        assert.equal(startElem.textContent, expectedStartString);
+
+        var nextElem = builder._getNextContentOnSide(startElem,
+            'right');
+        assert.equal(nextElem.textContent, expectedNextString);
+      });
+
+      test('_getNextContentOnSide unified left', function(done) {
+        // Re-render as unified:
+        element.viewMode = 'UNIFIED_DIFF';
+        element.render({left: [], right: []}, prefs).then(function() {
+          builder = element._builder;
+
+          var startElem = builder.getContentByLine(5, 'left',
+              element.$.diffTable);
+          var expectedStartString = diff.content[2].ab[0];
+          var expectedNextString = diff.content[2].ab[1];
+          assert.equal(startElem.textContent, expectedStartString);
+
+          var nextElem = builder._getNextContentOnSide(startElem,
+              'left');
+          assert.equal(nextElem.textContent, expectedNextString);
+
+          done();
+        });
+      });
+
+      test('_getNextContentOnSide unified right', function(done) {
+        // Re-render as unified:
+        element.viewMode = 'UNIFIED_DIFF';
+        element.render({left: [], right: []}, prefs).then(function() {
+          builder = element._builder;
+
+          var startElem = builder.getContentByLine(5, 'right',
+              element.$.diffTable);
+          var expectedStartString = diff.content[1].b[0];
+          var expectedNextString = diff.content[1].b[1];
+          assert.equal(startElem.textContent, expectedStartString);
+
+          var nextElem = builder._getNextContentOnSide(startElem,
+              'right');
+          assert.equal(nextElem.textContent, expectedNextString);
+
+          done();
+        });
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
index cfbcb07..641dc0f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -128,19 +128,21 @@
     setup(function() {
       stub('gr-rest-api-interface', {
         getLoggedIn: function() { return Promise.resolve(false); },
-        saveDiffDraft: function() { return Promise.resolve({
-          ok: true,
-          text: function() { return Promise.resolve(')]}\'\n' +
-              JSON.stringify({
-                id: '7afa4931_de3d65bd',
-                path: '/path/to/file.txt',
-                line: 5,
-                in_reply_to: 'baf0414d_60047215',
-                updated: '2015-12-21 02:01:10.850000000',
-                message: 'Done'
-              }));
-          },
-        })},
+        saveDiffDraft: function() {
+          return Promise.resolve({
+            ok: true,
+            text: function() { return Promise.resolve(')]}\'\n' +
+                JSON.stringify({
+                  id: '7afa4931_de3d65bd',
+                  path: '/path/to/file.txt',
+                  line: 5,
+                  in_reply_to: 'baf0414d_60047215',
+                  updated: '2015-12-21 02:01:10.850000000',
+                  message: 'Done'
+                }));
+            },
+          });
+        },
         deleteDiffDraft: function() { return Promise.resolve({ok: true}); },
       });
       element = fixture('withComment');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index 3baf200..1b30bde 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -15,7 +15,6 @@
   'use strict';
 
   var STORAGE_DEBOUNCE_INTERVAL = 400;
-  var UPDATE_DEBOUNCE_INTERVAL = 500;
 
   Polymer({
     is: 'gr-diff-comment',
@@ -160,7 +159,7 @@
     _fireUpdate: function() {
       this.debounce('fire-update', function() {
         this.fire('comment-update', this._getEventPayload());
-      }, UPDATE_DEBOUNCE_INTERVAL);
+      });
     },
 
     _draftChanged: function(draft) {
@@ -281,6 +280,7 @@
     },
 
     _fireDiscard: function() {
+      this.cancelDebouncer('fire-update');
       this.fire('comment-discard', this._getEventPayload());
     },
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index de5eb48..fcf8b41 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -194,15 +194,20 @@
       disabled = element.$$('.save').hasAttribute('disabled');
       assert.isTrue(disabled, 'save button should be disabled.');
 
+      var updateStub = sinon.stub();
+      element.addEventListener('comment-update', updateStub);
+
       var numDiscardEvents = 0;
       element.addEventListener('comment-discard', function(e) {
         numDiscardEvents++;
         if (numDiscardEvents == 3) {
+          assert.isFalse(updateStub.called);
           done();
         }
       });
       MockInteractions.tap(element.$$('.cancel'));
       MockInteractions.tap(element.$$('.discard'));
+      element.flushDebouncer('fire-update');
       MockInteractions.pressAndReleaseKeyOn(element.$.editTextarea, 27); // esc
     });
 
@@ -236,21 +241,19 @@
       element._xhrPromise.then(function(draft) {
         assert(fireStub.calledWith('comment-save'),
                'comment-save should be sent');
-        assert.deepEqual(fireStub.lastCall.args, [
-          'comment-save', {
-            comment: {
-              __draft: true,
-              __draftID: 'temp_draft_id',
-              __editing: false,
-              id: 'baf0414d_40572e03',
-              line: 5,
-              message: 'saved!',
-              path: '/path/to/file',
-              updated: '2015-12-08 21:52:36.177000000',
-            },
-            patchNum: 1,
+        assert.deepEqual(fireStub.lastCall.args[1], {
+          comment: {
+            __draft: true,
+            __draftID: 'temp_draft_id',
+            __editing: false,
+            id: 'baf0414d_40572e03',
+            line: 5,
+            message: 'saved!',
+            path: '/path/to/file',
+            updated: '2015-12-08 21:52:36.177000000',
           },
-        ]);
+          patchNum: 1,
+        });
         assert.isFalse(element.disabled,
                        'Element should be enabled when done creating draft.');
         assert.equal(draft.message, 'saved!');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
index 5a41709..491eded 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
@@ -23,7 +23,6 @@
         id="cursorManager"
         scroll="keep-visible"
         cursor-target-class="target-row"
-        fold-offset-top="[[foldOffsetTop]]"
         target="{{diffRow}}"></gr-cursor-manager>
   </template>
   <script src="gr-diff-cursor.js"></script>
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 99a0b5c..dd11f2c 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
@@ -54,11 +54,6 @@
         },
       },
 
-      foldOffsetTop: {
-        type: Number,
-        value: 0,
-      },
-
       /**
        * If set, the cursor will attempt to move to the line number (instead of
        * the first chunk) the next time the diff renders. It is set back to null
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 4c5ee62..5bdd138 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
@@ -25,7 +25,7 @@
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="../gr-diff/gr-diff.html">
 <link rel="import" href="./gr-diff-cursor.html">
-<link rel="import" href="./mock-diff-response_test.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
new file mode 100644
index 0000000..ec21fd1
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
@@ -0,0 +1,209 @@
+// Copyright (C) 2016 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(window) {
+  'use strict';
+
+  // Prevent redefinition.
+  if (window.GrAnnotation) { return; }
+
+  // TODO(wyatta): refactor this to be <MARK> rather than <HL>.
+  var ANNOTATION_TAG = 'HL';
+
+  // Astral code point as per https://mathiasbynens.be/notes/javascript-unicode
+  var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+
+  var GrAnnotation = {
+
+    /**
+     * The DOM API textContent.length calculation is broken when the text
+     * contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode .
+     * @param  {Text} A text node.
+     * @return {Number} The length of the text.
+     */
+    getLength: function(node) {
+      return node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length;
+    },
+
+    /**
+     * Surrounds the element's text at specified range in an ANNOTATION_TAG
+     * element. If the element has child elements, the range is split and
+     * applied as deeply as possible.
+     */
+    annotateElement: function(parent, offset, length, cssClass) {
+      var nodes = [].slice.apply(parent.childNodes);
+      var node;
+      var nodeLength;
+      var subLength;
+
+      for (var i = 0; i < nodes.length; i++) {
+        node = nodes[i];
+        nodeLength = this.getLength(node);
+
+        // If the current node is completely before the offset.
+        if (nodeLength <= offset) {
+          offset -= nodeLength;
+          continue;
+        }
+
+        // Sublength is the annotation length for the current node.
+        subLength = Math.min(length, nodeLength - offset);
+
+        if (node instanceof Text) {
+          this._annotateText(node, offset, subLength, cssClass);
+        } else if (node instanceof HTMLElement) {
+          this.annotateElement(node, offset, subLength, cssClass);
+        }
+
+        // If there is still more to annotate, then shift the indices, otherwise
+        // work is done, so break the loop.
+        if (subLength < length) {
+          length -= subLength;
+          offset = 0;
+        } else {
+          break;
+        }
+      }
+    },
+
+    /**
+     * Wraps node in annotation tag with cssClass, replacing the node in DOM.
+     *
+     * @return {!Element} Wrapped node.
+     */
+    wrapInHighlight: function(node, cssClass) {
+      var hl;
+      if (node.tagName === ANNOTATION_TAG) {
+        hl = node;
+        hl.classList.add(cssClass);
+      } else {
+        hl = document.createElement(ANNOTATION_TAG);
+        hl.className = cssClass;
+        Polymer.dom(node.parentElement).replaceChild(hl, node);
+        Polymer.dom(hl).appendChild(node);
+      }
+      return hl;
+    },
+
+    /**
+     * Splits Text Node and wraps it in hl with cssClass.
+     * Wraps trailing part after split, tailing one if opt_firstPart is true.
+     *
+     * @param {!Node} node
+     * @param {number} offset
+     * @param {string} cssClass
+     * @param {boolean=} opt_firstPart
+     */
+    splitAndWrapInHighlight: function(node, offset, cssClass, opt_firstPart) {
+      if (this.getLength(node) === offset || offset === 0) {
+        return this.wrapInHighlight(node, cssClass);
+      } else {
+        if (opt_firstPart) {
+          this.splitNode(node, offset);
+          // Node points to first part of the Text, second one is sibling.
+        } else {
+          node = this.splitNode(node, offset);
+        }
+        return this.wrapInHighlight(node, cssClass);
+      }
+    },
+
+    /**
+     * Splits Node at offset.
+     * If Node is Element, it's cloned and the node at offset is split too.
+     *
+     * @param {!Node} node
+     * @param {number} offset
+     * @return {!Node} Trailing Node.
+     */
+    splitNode: function(element, offset) {
+      if (element instanceof Text) {
+        return this.splitTextNode(element, offset);
+      }
+      var tail = element.cloneNode(false);
+      element.parentElement.insertBefore(tail, element.nextSibling);
+      // Skip nodes before offset.
+      var node = element.firstChild;
+      while (node &&
+          this.getLength(node) <= offset ||
+          this.getLength(node) === 0) {
+        offset -= this.getLength(node);
+        node = node.nextSibling;
+      }
+      if (this.getLength(node) > offset) {
+        tail.appendChild(this.splitNode(node, offset));
+      }
+      while (node.nextSibling) {
+        tail.appendChild(node.nextSibling);
+      }
+      return tail;
+    },
+
+    /**
+     * Node.prototype.splitText Unicode-valid alternative.
+     *
+     * DOM Api for splitText() is broken for Unicode:
+     * https://mathiasbynens.be/notes/javascript-unicode
+     *
+     * @param {!Text} node
+     * @param {number} offset
+     * @return {!Text} Trailing Text Node.
+     */
+    splitTextNode: function(node, offset) {
+      if (node.textContent.match(REGEX_ASTRAL_SYMBOL)) {
+        // TODO (viktard): Polyfill Array.from for IE10.
+        var head = Array.from(node.textContent);
+        var tail = head.splice(offset);
+        var parent = node.parentNode;
+
+        // Split the content of the original node.
+        node.textContent = head.join('');
+
+        var tailNode = document.createTextNode(tail.join(''));
+        if (parent) {
+          parent.insertBefore(tailNode, node.nextSibling);
+        }
+        return tailNode;
+      } else {
+        return node.splitText(offset);
+      }
+    },
+
+    _annotateText: function(node, offset, length, cssClass) {
+      var nodeLength = this.getLength(node);
+
+      // There are four cases:
+      //  1) Entire node is highlighted.
+      //  2) Highlight is at the start.
+      //  3) Highlight is at the end.
+      //  4) Highlight is in the middle.
+
+      if (offset === 0 && nodeLength === length) {
+        // Case 1.
+        this.wrapInHighlight(node, cssClass);
+      } else if (offset === 0) {
+        // Case 2.
+        this.splitAndWrapInHighlight(node, length, cssClass, true);
+      } else if (offset + length === nodeLength) {
+        // Case 3
+        this.splitAndWrapInHighlight(node, offset, cssClass, false);
+      } else {
+        // Case 4
+        this.splitAndWrapInHighlight(this.splitTextNode(node, offset), length,
+            cssClass, true);
+      }
+    },
+  };
+
+  window.GrAnnotation = GrAnnotation;
+})(window);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
new file mode 100644
index 0000000..27a684d
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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-annotation</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="gr-annotation.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+
+<test-fixture id="basic">
+  <template>
+    <div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
+  </template>
+</test-fixture>
+
+<script>
+  suite('annotation', function() {
+    var str;
+    var parent;
+    var textNode;
+
+    setup(function() {
+      parent = fixture('basic');
+      textNode = parent.childNodes[0];
+      str = textNode.textContent;
+    });
+
+    test('_annotateText Case 1', function() {
+      GrAnnotation._annotateText(textNode, 0, str.length, 'foobar');
+
+      assert.equal(parent.childNodes.length, 1);
+      assert.instanceOf(parent.childNodes[0], HTMLElement);
+      assert.equal(parent.childNodes[0].className, 'foobar');
+      assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
+      assert.equal(parent.childNodes[0].childNodes[0].textContent, str);
+    });
+
+    test('_annotateText Case 2', function() {
+      var length = 12;
+      var substr = str.substr(0, length);
+      var remainder = str.substr(length);
+
+      GrAnnotation._annotateText(textNode, 0, length, 'foobar');
+
+      assert.equal(parent.childNodes.length, 2);
+
+      assert.instanceOf(parent.childNodes[0], HTMLElement);
+      assert.equal(parent.childNodes[0].className, 'foobar');
+      assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
+      assert.equal(parent.childNodes[0].childNodes[0].textContent, substr);
+
+      assert.instanceOf(parent.childNodes[1], Text);
+      assert.equal(parent.childNodes[1].textContent, remainder);
+    });
+
+    test('_annotateText Case 3', function() {
+      var index = 12;
+      var length = str.length - index;
+      var remainder = str.substr(0, index);
+      var substr = str.substr(index);
+
+      GrAnnotation._annotateText(textNode, index, length, 'foobar');
+
+      assert.equal(parent.childNodes.length, 2);
+
+      assert.instanceOf(parent.childNodes[0], Text);
+      assert.equal(parent.childNodes[0].textContent, remainder);
+
+      assert.instanceOf(parent.childNodes[1], HTMLElement);
+      assert.equal(parent.childNodes[1].className, 'foobar');
+      assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
+      assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
+    });
+
+    test('_annotateText Case 4', function() {
+      var index = str.indexOf('dolor');
+      var length = 'dolor '.length;
+
+      var remainderPre = str.substr(0, index);
+      var substr = str.substr(index, length);
+      var remainderPost = str.substr(index + length);
+
+      GrAnnotation._annotateText(textNode, index, length, 'foobar');
+
+      assert.equal(parent.childNodes.length, 3);
+
+      assert.instanceOf(parent.childNodes[0], Text);
+      assert.equal(parent.childNodes[0].textContent, remainderPre);
+
+      assert.instanceOf(parent.childNodes[1], HTMLElement);
+      assert.equal(parent.childNodes[1].className, 'foobar');
+      assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
+      assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
+
+      assert.instanceOf(parent.childNodes[2], Text);
+      assert.equal(parent.childNodes[2].textContent, remainderPost);
+    });
+
+    test('_annotateElement design doc example', function() {
+      var layers = [
+        'amet, ',
+        'inceptos ',
+        'amet, ',
+        'et, suspendisse ince'
+      ];
+
+      // Apply the layers successively.
+      layers.forEach(function(layer, i) {
+        GrAnnotation.annotateElement(
+            parent, str.indexOf(layer), layer.length, 'layer-' + (i + 1));
+      });
+
+      assert.equal(parent.textContent, str);
+
+      // Layer 1:
+      var layer1 = parent.querySelectorAll('.layer-1');
+      assert.equal(layer1.length, 1);
+      assert.equal(layer1[0].textContent, layers[0]);
+      assert.equal(layer1[0].parentElement, parent);
+
+      // Layer 2:
+      var layer2 = parent.querySelectorAll('.layer-2');
+      assert.equal(layer2.length, 1);
+      assert.equal(layer2[0].textContent, layers[1]);
+      assert.equal(layer2[0].parentElement, parent);
+
+      // Layer 3:
+      var layer3 = parent.querySelectorAll('.layer-3');
+      assert.equal(layer3.length, 1);
+      assert.equal(layer3[0].textContent, layers[2]);
+      assert.equal(layer3[0].parentElement, layer1[0]);
+
+      // Layer 4:
+      var layer4 = parent.querySelectorAll('.layer-4');
+      assert.equal(layer4.length, 3);
+
+      assert.equal(layer4[0].textContent, 'et, ');
+      assert.equal(layer4[0].parentElement, layer3[0]);
+
+      assert.equal(layer4[1].textContent, 'suspendisse ');
+      assert.equal(layer4[1].parentElement, parent);
+
+      assert.equal(layer4[2].textContent, 'ince');
+      assert.equal(layer4[2].parentElement, layer2[0]);
+
+      assert.equal(layer4[0].textContent +
+          layer4[1].textContent +
+          layer4[2].textContent,
+          layers[3]);
+    });
+
+    test('splitTextNode', function() {
+      var helloString = 'hello';
+      var asciiString = 'ASCII';
+      var unicodeString = 'Unic💢de';
+
+      var node;
+      var tail;
+
+      // Non-unicode path:
+      node = document.createTextNode(helloString + asciiString);
+      tail = GrAnnotation.splitTextNode(node, helloString.length);
+      assert(node.textContent, helloString);
+      assert(tail.textContent, asciiString);
+
+      // Unicdoe path:
+      node = document.createTextNode(helloString + unicodeString);
+      tail = GrAnnotation.splitTextNode(node, helloString.length);
+      assert(node.textContent, helloString);
+      assert(tail.textContent, unicodeString);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
index bc3b23f..54294a1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
@@ -24,11 +24,11 @@
         position: relative;
       }
       .contentWrapper ::content .range {
-        background-color: #ffd500 !important;
+        background-color: rgba(255,213,0,0.5);
         display: inline;
       }
       .contentWrapper ::content .rangeHighlight {
-        background-color: #ff0 !important;
+        background-color: rgba(255,255,0,0.5);
         display: inline;
       }
     </style>
@@ -36,5 +36,6 @@
       <content></content>
     </div>
   </template>
+  <script src="gr-annotation.js"></script>
   <script src="gr-diff-highlight.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index a5968da..bfe103b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -14,38 +14,26 @@
 (function() {
   'use strict';
 
-  // Astral code point as per https://mathiasbynens.be/notes/javascript-unicode
-  var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
-  var RANGE_HIGHLIGHT = 'range';
-  var HOVER_HIGHLIGHT = 'rangeHighlight';
-
   Polymer({
     is: 'gr-diff-highlight',
 
     properties: {
       comments: Object,
-      enabled: {
-        type: Boolean,
-        observer: '_enabledChanged',
-      },
       loggedIn: Boolean,
       _cachedDiffBuilder: Object,
-      _enabledListeners: {
-        type: Object,
-        value: function() {
-          return {
-            'comment-discard': '_handleCommentDiscard',
-            'comment-mouse-out': '_handleCommentMouseOut',
-            'comment-mouse-over': '_handleCommentMouseOver',
-            'create-comment': '_createComment',
-            'render': '_handleRender',
-            'show-context': '_handleShowContext',
-            'thread-discard': '_handleThreadDiscard',
-          };
-        },
-      },
+      isAttached: Boolean,
     },
 
+    listeners: {
+      'comment-mouse-out': '_handleCommentMouseOut',
+      'comment-mouse-over': '_handleCommentMouseOver',
+      'create-comment': '_createComment',
+    },
+
+    observers: [
+      '_enableSelectionObserver(loggedIn, isAttached)',
+    ],
+
     get diffBuilder() {
       if (!this._cachedDiffBuilder) {
         this._cachedDiffBuilder =
@@ -54,45 +42,18 @@
       return this._cachedDiffBuilder;
     },
 
-    detached: function() {
-      this.enabled = false;
-    },
-
-    _enabledChanged: function() {
-      if (this.enabled) {
+    _enableSelectionObserver: function(loggedIn, isAttached) {
+      if (loggedIn && isAttached) {
         this.listen(document, 'selectionchange', '_handleSelectionChange');
       } else {
         this.unlisten(document, 'selectionchange', '_handleSelectionChange');
       }
-      for (var eventName in this._enabledListeners) {
-        var methodName = this._enabledListeners[eventName];
-        if (this.enabled) {
-          this.listen(this, eventName, methodName);
-        } else {
-          this.unlisten(this, eventName, methodName);
-        }
-      }
     },
 
     isRangeSelected: function() {
       return !!this.$$('gr-selection-action-box');
     },
 
-    _handleThreadDiscard: function(e) {
-      var comment = e.detail.lastComment;
-      // Comment Element was removed from DOM already.
-      if (comment.range) {
-        this._renderCommentRange(comment, e.target);
-      }
-    },
-
-    _handleCommentDiscard: function(e) {
-      var comment = e.detail.comment;
-      if (comment.range) {
-        this._renderCommentRange(comment, e.target);
-      }
-    },
-
     _handleSelectionChange: function() {
       // Can't use up or down events to handle selection started and/or ended in
       // in comment threads or outside of diff.
@@ -101,45 +62,36 @@
       this.debounce('selectionChange', this._handleSelection, 200);
     },
 
-    _handleRender: function() {
-      this._applyAllHighlights();
-    },
-
-    _handleShowContext: function() {
-      // TODO (viktard): Re-render expanded sections only.
-      this._applyAllHighlights();
-    },
-
     _handleCommentMouseOver: function(e) {
       var comment = e.detail.comment;
-      var range = comment.range;
-      if (!range) {
-        return;
-      }
+      if (!comment.range) { return; }
       var lineEl = this.diffBuilder.getLineElByChild(e.target);
       var side = this.diffBuilder.getSideByLineEl(lineEl);
-      this._applyRangedHighlight(
-          HOVER_HIGHLIGHT, range.start_line, range.start_character,
-          range.end_line, range.end_character, side);
+      var index = this._indexOfComment(side, comment);
+      if (index !== undefined) {
+        this.set(['comments', side, index, '__hovering'], true);
+      }
     },
 
     _handleCommentMouseOut: function(e) {
       var comment = e.detail.comment;
-      var range = comment.range;
-      if (!range) {
-        return;
-      }
+      if (!comment.range) { return; }
       var lineEl = this.diffBuilder.getLineElByChild(e.target);
       var side = this.diffBuilder.getSideByLineEl(lineEl);
-      var contentEls = this.diffBuilder.getContentsByLineRange(
-          range.start_line, range.end_line, side);
-      contentEls.forEach(function(content) {
-        Polymer.dom(content).querySelectorAll('.' + HOVER_HIGHLIGHT).forEach(
-            function(el) {
-              el.classList.remove(HOVER_HIGHLIGHT);
-              el.classList.add(RANGE_HIGHLIGHT);
-            });
-      }, this);
+      var index = this._indexOfComment(side, comment);
+      if (index !== undefined) {
+        this.set(['comments', side, index, '__hovering'], false);
+      }
+    },
+
+    _indexOfComment: function(side, comment) {
+      var idProp = comment.id ? 'id' : '__draftID';
+      for (var i = 0; i < this.comments[side].length; i++) {
+        if (comment[idProp] &&
+            this.comments[side][i][idProp] === comment[idProp]) {
+          return i;
+        }
+      }
     },
 
     /**
@@ -173,18 +125,19 @@
       if (!line) {
         return;
       }
-      var content = this.diffBuilder.getContentByLineEl(lineEl);
-      if (!content) {
+      var contentText = this.diffBuilder.getContentByLineEl(lineEl);
+      if (!contentText) {
         return;
       }
-      if (!content.contains(node)) {
-        node = content;
+      var contentTd = contentText.parentElement;
+      if (!contentTd.contains(node)) {
+        node = contentText;
         column = 0;
       } else {
-        var thread = content.querySelector('gr-diff-comment-thread');
+        var thread = contentTd.querySelector('gr-diff-comment-thread');
         if (thread && thread.contains(node)) {
-          column = this._getLength(content);
-          node = content;
+          column = this._getLength(contentText);
+          node = contentText;
         } else {
           column = this._convertOffsetToColumn(node, offset);
         }
@@ -247,26 +200,8 @@
       }
     },
 
-    _renderCommentRange: function(comment, el) {
-      var lineEl = this.diffBuilder.getLineElByChild(el);
-      if (!lineEl) {
-        return;
-      }
-      var side = this.diffBuilder.getSideByLineEl(lineEl);
-      this._rerenderByLines(
-          comment.range.start_line, comment.range.end_line, side);
-    },
-
     _createComment: function(e) {
       this._removeActionBox();
-      var side = e.detail.side;
-      var range = e.detail.range;
-      if (!range) {
-        return;
-      }
-      this._applyRangedHighlight(
-          RANGE_HIGHLIGHT, range.startLine, range.startChar,
-          range.endLine, range.endChar, side);
     },
 
     _removeActionBoxDebounced: function() {
@@ -322,339 +257,18 @@
     },
 
     /**
-     * Get length of a node. Traverses diff content siblings if required.
+     * Get length of a node. If the node is a content node, then only give the
+     * length of its .contentText child.
      *
      * @param {!Node} node
      * @return {number}
      */
     _getLength: function(node) {
       if (node instanceof Element && node.classList.contains('content')) {
-        node = node.firstChild;
-        var length = 0;
-        while (node) {
-          if (node instanceof Text || node.tagName == 'HL') {
-            length += this._getLength(node);
-          }
-          node = node.nextSibling;
-        }
-        return length;
+        return this._getLength(node.querySelector('.contentText'));
       } else {
-        // DOM API for textContent.length is broken for Unicode:
-        // https://mathiasbynens.be/notes/javascript-unicode
-        return node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length;
+        return GrAnnotation.getLength(node);
       }
     },
-
-    /**
-     * Wraps node in hl tag with cssClass, replacing the node in DOM.
-     *
-     * @return {!Element} Wrapped node.
-     */
-    _wrapInHighlight: function(node, cssClass) {
-      var hl;
-      if (node.tagName === 'HL') {
-        hl = node;
-        hl.classList.add(cssClass);
-      } else {
-        hl = document.createElement('hl');
-        hl.className = cssClass;
-        Polymer.dom(node.parentElement).replaceChild(hl, node);
-        hl.appendChild(node);
-      }
-      return hl;
-    },
-
-    /**
-     * Node.prototype.splitText Unicode-valid alternative.
-     *
-     * @param {!Text} node
-     * @param {number} offset
-     * @return {!Text} Trailing Text Node.
-     */
-    _splitTextNode: function(node, offset) {
-      if (node.textContent.match(REGEX_ASTRAL_SYMBOL)) {
-        // DOM Api for splitText() is broken for Unicode:
-        // https://mathiasbynens.be/notes/javascript-unicode
-        // TODO (viktard): Polyfill Array.from for IE10.
-        var head = Array.from(node.textContent);
-        var tail = head.splice(offset);
-        var parent = node.parentElement;
-        var headNode = document.createTextNode(head.join(''));
-        parent.replaceChild(headNode, node);
-        var tailNode = document.createTextNode(tail.join(''));
-        parent.insertBefore(tailNode, headNode.nextSibling);
-        return tailNode;
-      } else {
-        return node.splitText(offset);
-      }
-    },
-
-    /**
-     * Split Node at offset.
-     * If Node is Element, it's cloned and the node at offset is split too.
-     *
-     * @param {!Node} node
-     * @param {number} offset
-     * @return {!Node} Trailing Node.
-     */
-    _splitNode: function(element, offset) {
-      if (element instanceof Text) {
-        return this._splitTextNode(element, offset);
-      }
-      var tail = element.cloneNode(false);
-      element.parentElement.insertBefore(tail, element.nextSibling);
-      // Skip nodes before offset.
-      var node = element.firstChild;
-      while (node &&
-          this._getLength(node) <= offset ||
-          this._getLength(node) === 0) {
-        offset -= this._getLength(node);
-        node = node.nextSibling;
-      }
-      if (this._getLength(node) > offset) {
-        tail.appendChild(this._splitNode(node, offset));
-      }
-      while (node.nextSibling) {
-        tail.appendChild(node.nextSibling);
-      }
-      return tail;
-    },
-
-    /**
-     * Split Text Node and wrap it in hl with cssClass.
-     * Wraps trailing part after split, tailing one if opt_firstPart is true.
-     *
-     * @param {!Node} node
-     * @param {number} offset
-     * @param {string} cssClass
-     * @param {boolean=} opt_firstPart
-     */
-    _splitAndWrapInHighlight: function(node, offset, cssClass, opt_firstPart) {
-      if (this._getLength(node) === offset || offset === 0) {
-        return this._wrapInHighlight(node, cssClass);
-      } else {
-        if (opt_firstPart) {
-          this._splitNode(node, offset);
-          // Node points to first part of the Text, second one is sibling.
-        } else {
-          node = this._splitNode(node, offset);
-        }
-        return this._wrapInHighlight(node, cssClass);
-      }
-    },
-
-    /**
-     * Creates hl tag with cssClass for starting side of range highlight.
-     *
-     * @param {!Element} startContent Range start diff content aka td.content.
-     * @param {!Element} endContent Range end diff content aka td.content.
-     * @param {number} startOffset Range start within start content.
-     * @param {number} endOffset Range end within end content.
-     * @param {string} cssClass
-     * @return {!Element} Range start node.
-     */
-    _normalizeStart: function(
-        startContent, endContent, startOffset, endOffset, cssClass) {
-      var isOneLine = startContent === endContent;
-      var startNode = startContent.firstChild;
-      var length = endOffset - startOffset;
-
-      if (!startNode) {
-        return startNode;
-      }
-
-      // Skip nodes before startOffset.
-      var nodeLength = this._getLength(startNode);
-      while (startNode && (nodeLength <= startOffset || nodeLength === 0)) {
-        startOffset -= nodeLength;
-        startNode = startNode.nextSibling;
-        nodeLength = startNode && this._getLength(startNode);
-      }
-      if (!startNode) { return null; }
-
-      // Split Text node.
-      if (startNode instanceof Text) {
-        startNode =
-            this._splitAndWrapInHighlight(startNode, startOffset, cssClass);
-        // Edge case: single line, text node wraps the highlight.
-        if (isOneLine && this._getLength(startNode) > length) {
-          var extra = this._splitTextNode(startNode.firstChild, length);
-          startContent.insertBefore(extra, startNode.nextSibling);
-          startContent.normalize();
-        }
-      } else if (startNode.tagName == 'HL') {
-        if (!startNode.classList.contains(cssClass)) {
-          // Edge case: single line, <hl> wraps the highlight.
-          // Should leave wrapping HL's content after the highlight.
-          if (isOneLine && startOffset + length < this._getLength(startNode)) {
-            this._splitNode(startNode, startOffset + length);
-          }
-          startNode =
-              this._splitAndWrapInHighlight(startNode, startOffset, cssClass);
-        }
-      } else {
-        startNode = null;
-      }
-      return startNode;
-    },
-
-    /**
-     * Creates hl tag with cssClass for ending side of range highlight.
-     *
-     * @param {!Element} startContent Range start diff content aka td.content.
-     * @param {!Element} endContent Range end diff content aka td.content.
-     * @param {number} startOffset Range start within start content.
-     * @param {number} endOffset Range end within end content.
-     * @param {string} cssClass
-     * @return {!Element} Range start node.
-     */
-    _normalizeEnd: function(
-        startContent, endContent, startOffset, endOffset, cssClass) {
-      var endNode = endContent.firstChild;
-
-      if (!endNode) {
-        return endNode;
-      }
-
-      // Find the node where endOffset points at.
-      var nodeLength = this._getLength(endNode);
-      while (endNode && (nodeLength < endOffset || nodeLength === 0)) {
-        endOffset -= nodeLength;
-        endNode = endNode.nextSibling;
-        nodeLength = endNode && this._getLength(endNode);
-      }
-      if (!endNode) { return null; }
-
-      if (endNode instanceof Text) {
-        endNode =
-            this._splitAndWrapInHighlight(endNode, endOffset, cssClass, true);
-      } else if (endNode.tagName == 'HL') {
-        if (!endNode.classList.contains(cssClass)) {
-          // Split text inside HL.
-          var hl = endNode;
-          endNode = this._splitAndWrapInHighlight(
-              endNode, endOffset, cssClass, true);
-          if (hl.textContent.length === 0) {
-            hl.remove();
-          }
-        }
-      } else {
-        endNode = null;
-      }
-      return endNode;
-    },
-
-    /**
-     * Applies highlight to first and last lines in range.
-     *
-     * @param {!Element} startContent Range start diff content aka td.content.
-     * @param {!Element} endContent Range end diff content aka td.content.
-     * @param {number} startOffset Range start within start content.
-     * @param {number} endOffset Range end within end content.
-     * @param {string} cssClass
-     */
-    _highlightSides: function(
-        startContent, endContent, startOffset, endOffset, cssClass) {
-      var isOneLine = startContent === endContent;
-      var startNode = this._normalizeStart(
-          startContent, endContent, startOffset, endOffset, cssClass);
-      var endNode = this._normalizeEnd(
-          startContent, endContent, startOffset, endOffset, cssClass);
-
-      // Grow starting highlight until endNode or end of line.
-      if (startNode && startNode != endNode) {
-        var growStartHl = function(node) {
-          if (node instanceof Text || node.tagName === 'SPAN') {
-            startNode.appendChild(node);
-          } else if (node.tagName === 'HL') {
-            this._traverseContentSiblings(node.firstChild, growStartHl);
-            node.remove();
-          }
-          return node == endNode;
-        }.bind(this);
-        this._traverseContentSiblings(startNode.nextSibling, growStartHl);
-        startNode.normalize();
-      }
-
-      if (!isOneLine && endNode) {
-        var growEndHl = function(node) {
-          if (node instanceof Text || node.tagName === 'SPAN') {
-            endNode.insertBefore(node, endNode.firstChild);
-          } else if (node.tagName === 'HL') {
-            this._traverseContentSiblings(node.firstChild, growEndHl);
-            node.remove();
-          }
-        }.bind(this);
-        // Prepend text up to line start to the ending highlight.
-        this._traverseContentSiblings(
-          endNode.previousSibling, growEndHl, {left: true});
-        endNode.normalize();
-      }
-    },
-
-    /**
-     * @param {string} cssClass
-     * @param {number} startLine Range start code line number.
-     * @param {number} startCol Range start column number.
-     * @param {number} endLine Range end line number.
-     * @param {number} endCol Range end column number.
-     * @param {string=} opt_side Side selector (right or left).
-     */
-    _applyRangedHighlight: function(
-        cssClass, startLine, startCol, endLine, endCol, opt_side) {
-      var startEl = this.diffBuilder.getContentByLine(startLine, opt_side);
-      var endEl = this.diffBuilder.getContentByLine(endLine, opt_side);
-      this._highlightSides(startEl, endEl, startCol, endCol, cssClass);
-      if (endLine - startLine > 1) {
-        // There is at least one line in between.
-        var contents = this.diffBuilder.getContentsByLineRange(
-            startLine + 1, endLine - 1, opt_side);
-        // Wrap contents in highlight.
-        contents.forEach(function(content) {
-          if (content.textContent.length === 0) {
-            return;
-          }
-          var threadEl =
-                this.diffBuilder.getCommentThreadByContentEl(content);
-          if (threadEl) {
-            threadEl.remove();
-          }
-          var text = document.createTextNode(content.textContent);
-          while (content.firstChild) {
-            content.removeChild(content.firstChild);
-          }
-          content.appendChild(text);
-          if (threadEl) {
-            content.appendChild(threadEl);
-          }
-          this._wrapInHighlight(text, cssClass);
-        }, this);
-      }
-    },
-
-    _applyAllHighlights: function() {
-      var rangedLeft =
-          this.comments.left.filter(function(item) { return !!item.range; });
-      var rangedRight =
-          this.comments.right.filter(function(item) { return !!item.range; });
-      rangedLeft.forEach(function(item) {
-        var range = item.range;
-        this._applyRangedHighlight(
-            RANGE_HIGHLIGHT, range.start_line, range.start_character,
-            range.end_line, range.end_character, 'left');
-      }, this);
-      rangedRight.forEach(function(item) {
-        var range = item.range;
-        this._applyRangedHighlight(
-            RANGE_HIGHLIGHT, range.start_line, range.start_character,
-            range.end_line, range.end_character, 'right');
-      }, this);
-    },
-
-    _rerenderByLines: function(startLine, endLine, opt_side) {
-      this.async(function() {
-        this.diffBuilder.renderLineRange(startLine, endLine, opt_side);
-      }, 1);
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index 8771cc1..5f84e4f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -32,9 +32,9 @@
         <tbody class="section both">
           <tr class="diff-row side-by-side" left-type="both" right-type="both">
             <td class="left lineNum" data-value="138">138</td>
-            <td class="content both darkHighlight">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</td>
+            <td class="content both darkHighlight"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
             <td class="right lineNum" data-value="119">119</td>
-            <td class="content both darkHighlight">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</td>
+            <td class="content both darkHighlight"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
           </tr>
         </tbody>
 
@@ -42,21 +42,21 @@
           <tr class="diff-row side-by-side" left-type="remove" right-type="add">
             <td class="left lineNum" data-value="140">140</td>
             <!-- Next tag is formatted to eliminate zero-length text nodes. -->
-            <td class="content remove lightHighlight">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab withIndicator" style="tab-size:8;"></span></hl>udiam, <hl>quid</hl> sit, <span class="tab withIndicator" style="tab-size:8;"></span>quod <hl>Epicurum</hl><gr-diff-comment-thread>
+            <td class="content remove lightHighlight"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab withIndicator" style="tab-size:8;"></span></hl>udiam, <hl>quid</hl> sit, <span class="tab withIndicator" style="tab-size:8;"></span>quod <hl>Epicurum</hl></div><gr-diff-comment-thread>
                 [Yet another random diff thread content here]
               </gr-diff-comment-thread></td>
             <td class="right lineNum" data-value="120">120</td>
             <!-- Next tag is formatted to eliminate zero-length text nodes. -->
-            <td class="content add lightHighlight">nacti , <hl>,</hl> sumus <hl><span class="tab withIndicator" style="tab-size:8;"></span></hl> otiosum,  <span class="tab withIndicator" style="tab-size:8;"></span> audiam,  sit, quod</td>
+            <td class="content add lightHighlight"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab withIndicator" style="tab-size:8;"></span></hl> otiosum,  <span class="tab withIndicator" style="tab-size:8;"></span> audiam,  sit, quod</div></td>
           </tr>
         </tbody>
 
         <tbody class="section both">
           <tr class="diff-row side-by-side" left-type="both" right-type="both">
             <td class="left lineNum" data-value="141"></td>
-            <td class="content both darkHighlight">nam et<hl><span class="tab withIndicator" style="tab-size:8;">	</span></hl>complectitur<span class="tab withIndicator" style="tab-size:8;">	</span>verbis, quod vult, et dicit plane, quod intellegam;</td>
+            <td class="content both darkHighlight"><div class="contentText">nam et<hl><span class="tab withIndicator" style="tab-size:8;">	</span></hl>complectitur<span class="tab withIndicator" style="tab-size:8;">	</span>verbis, quod vult, et dicit plane, quod intellegam;</div></td>
             <td class="right lineNum" data-value="130"></td>
-            <td class="content both darkHighlight">nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;</td>
+            <td class="content both darkHighlight"><div class="contentText">nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;</div></td>
           </tr>
         </tbody>
 
@@ -86,16 +86,16 @@
             <td class="left"></td>
             <td class="blank darkHighlight"></td>
             <td class="right lineNum" data-value="146"></td>
-            <td class="content add darkHighlight">[17] Quid igitur est? inquit; audire enim cupio, quid non probes. Principio, inquam,</td>
+            <td class="content add darkHighlight"><div class="contentText">[17] Quid igitur est? inquit; audire enim cupio, quid non probes. Principio, inquam,</div></td>
           </tr>
         </tbody>
 
         <tbody class="section both">
           <tr class="diff-row side-by-side" left-type="both" right-type="both">
             <td class="left lineNum" data-value="165"></td>
-            <td class="content both darkHighlight">in physicis, quibus maxime gloriatur, primum totus est alienus. Democritea dicit</td>
+            <td class="content both darkHighlight"><div class="contentText">in physicis, quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td>
             <td class="right lineNum" data-value="147"></td>
-            <td class="content both darkHighlight">in physicis, <hl><span class="tab withIndicator" style="tab-size:8;">	</span></hl> quibus maxime gloriatur, primum totus est alienus. Democritea dicit</td>
+            <td class="content both darkHighlight"><div class="contentText">in physicis, <hl><span class="tab withIndicator" style="tab-size:8;">	</span></hl> quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td>
           </tr>
         </tbody>
 
@@ -128,50 +128,31 @@
       sandbox.restore();
     });
 
-    test('_enabledListeners', function() {
-      var listeners = element._enabledListeners;
-      for (var eventName in listeners) {
-        sandbox.stub(element, listeners[eventName]);
-      }
-      // Enable all the listeners.
-      element.enabled = true;
-      for (var eventName in listeners) {
-        var methodName = listeners[eventName];
-        var stub = element[methodName];
-        element.fire(eventName);
-        assert.isTrue(stub.called);
-        stub.reset();
-      }
-      // Disable all the listeners.
-      element.enabled = false;
-      for (var eventName in listeners) {
-        var methodName = listeners[eventName];
-        var stub = element[methodName];
-        element.fire(eventName);
-        assert.isFalse(stub.called);
-      }
-    });
+    suite('selectionchange event handling', function() {
+      var emulateSelection = function() {
+        document.dispatchEvent(new CustomEvent('selectionchange'));
+        element.flushDebouncer('selectionChange');
+        element.flushDebouncer('removeActionBox');
+      };
 
-    test('does not listen to selectionchange when disabled', function() {
-      sandbox.stub(element, '_handleSelection');
-      sandbox.stub(element, '_removeActionBox');
-      element.enabled = false;
-      document.dispatchEvent(new CustomEvent('selectionchange'));
-      element.flushDebouncer('selectionChange');
-      assert.isFalse(element._handleSelection.called);
-      element.flushDebouncer('removeActionBox');
-      assert.isFalse(element._removeActionBox.called);
-    });
+      setup(function() {
+        sandbox.stub(element, '_handleSelection');
+        sandbox.stub(element, '_removeActionBox');
+      });
 
-    test('listens to selectionchange when enabled', function() {
-      sandbox.stub(element, '_handleSelection');
-      sandbox.stub(element, '_removeActionBox');
-      element.enabled = true;
-      document.dispatchEvent(new CustomEvent('selectionchange'));
-      element.flushDebouncer('selectionChange');
-      assert.isTrue(element._handleSelection.called);
-      element.flushDebouncer('removeActionBox');
-      assert.isTrue(element._removeActionBox.called);
+      test('enabled if logged in', function() {
+        element.loggedIn = true;
+        emulateSelection();
+        assert.isTrue(element._handleSelection.called);
+        assert.isTrue(element._removeActionBox.called);
+      });
+
+      test('ignored if logged out', function() {
+        element.loggedIn = false;
+        emulateSelection();
+        assert.isFalse(element._handleSelection.called);
+        assert.isFalse(element._removeActionBox.called);
+      });
     });
 
     suite('comment events', function() {
@@ -185,7 +166,6 @@
           renderLineRange: sandbox.stub(),
         };
         element._cachedDiffBuilder = builder;
-        element.enabled = true;
       });
 
       test('ignores thread discard for line comment', function(done) {
@@ -204,42 +184,17 @@
         });
       });
 
-      test('renders lines in comment range on thread discard', function(done) {
-        element.fire('thread-discard', {
-          lastComment: {
-            range: {
-              start_line: 10,
-              end_line: 24,
-            },
-          },
-        });
-        flush(function() {
-          assert.isTrue(
-              builder.renderLineRange.calledWithExactly(10, 24, 'other-side'));
-          done();
-        });
-      });
-
-      test('renders lines in comment range on comment discard', function(done) {
-        element.fire('comment-discard', {
-          comment: {
-            range: {
-              start_line: 10,
-              end_line: 24,
-            },
-          },
-        });
-        flush(function() {
-          assert.isTrue(
-              builder.renderLineRange.calledWithExactly(10, 24, 'other-side'));
-          done();
-        });
-      });
-
       test('comment-mouse-over from line comments is ignored', function() {
-        sandbox.stub(element, '_applyRangedHighlight');
+        sandbox.stub(element, 'set');
         element.fire('comment-mouse-over', {comment: {}});
-        assert.isFalse(element._applyRangedHighlight.called);
+        assert.isFalse(element.set.called);
+      });
+
+      test('comment-mouse-over from ranged comment causes set', function() {
+        sandbox.stub(element, 'set');
+        sandbox.stub(element, '_indexOfComment').returns(0);
+        element.fire('comment-mouse-over', {comment: {range: {}}});
+        assert.isTrue(element.set.called);
       });
 
       test('comment-mouse-out from line comments is ignored', function() {
@@ -247,56 +202,7 @@
         assert.isFalse(builder.getContentsByLineRange.called);
       });
 
-      test('on comment-mouse-out highlight classes are removed', function() {
-        var testEl = fixture('highlighted');
-        builder.getContentsByLineRange.returns([testEl]);
-        element.fire('comment-mouse-out', {
-          comment: {
-            range: {
-              start_line: 3,
-              start_character: 14,
-              end_line: 10,
-              end_character: 24,
-            }
-          }});
-        assert.isTrue(builder.getContentsByLineRange.calledWithExactly(
-            3, 10, 'other-side'));
-        assert.equal(0, testEl.querySelectorAll('.rangeHighlight').length);
-        assert.equal(2, testEl.querySelectorAll('.range').length);
-      });
-
-      test('on comment-mouse-over range is highlighted', function() {
-        sandbox.stub(element, '_applyRangedHighlight');
-        element.fire('comment-mouse-over', {
-          comment: {
-            range: {
-              start_line: 3,
-              start_character: 14,
-              end_line: 10,
-              end_character: 24,
-            },
-          }});
-        assert.isTrue(element._applyRangedHighlight.calledWithExactly(
-            'rangeHighlight', 3, 14, 10, 24, 'other-side'));
-      });
-
-      test('on create-comment range is highlighted', function() {
-        sandbox.stub(element, '_applyRangedHighlight');
-        element.fire('create-comment', {
-          range: {
-            startLine: 3,
-            startChar: 14,
-            endLine: 10,
-            endChar: 24,
-          },
-          side: 'some-side',
-        });
-        assert.isTrue(element._applyRangedHighlight.calledWithExactly(
-            'range', 3, 14, 10, 24, 'some-side'));
-      });
-
       test('on create-comment action box is removed', function() {
-        sandbox.stub(element, '_applyRangedHighlight');
         sandbox.stub(element, '_removeActionBox');
         element.fire('create-comment', {
           comment: {
@@ -307,332 +213,27 @@
       });
     });
 
-    test('apply multiline highlight', function() {
-      var diff = element.querySelector('#diffTable');
-      var startContent =
-          diff.querySelector('.left.lineNum[data-value="138"] ~ .content');
-      var betweenContent =
-          diff.querySelector('.left.lineNum[data-value="140"] ~ .content');
-      var endContent =
-          diff.querySelector('.left.lineNum[data-value="141"] ~ .content');
-      var commentThread =
-          diff.querySelector('gr-diff-comment-thread');
-      var builder = {
-        getCommentThreadByContentEl: sandbox.stub().returns(commentThread),
-        getContentByLine: sandbox.stub().returns({}),
-        getContentsByLineRange: sandbox.stub().returns([betweenContent]),
-        getLineElByChild: sandbox.stub().returns(
-            {getAttribute: sandbox.stub()}),
-      };
-      element._cachedDiffBuilder = builder;
-      element.enabled = true;
-      builder.getContentByLine.withArgs(138, 'left').returns(
-          startContent);
-      builder.getContentByLine.withArgs(141, 'left').returns(
-          endContent);
-      element._applyRangedHighlight('some', 138, 4, 141, 28, 'left');
-      assert.instanceOf(startContent.childNodes[0], Text);
-      assert.equal(startContent.childNodes[0].textContent, '[14]');
-      assert.instanceOf(startContent.childNodes[1], Element);
-      assert.equal(startContent.childNodes[1].textContent,
-          ' Nam cum ad me in Cumanum salutandi causa uterque venisset,');
-      assert.equal(startContent.childNodes[1].tagName, 'HL');
-      assert.equal(startContent.childNodes[1].className, 'some');
-
-      assert.instanceOf(betweenContent.firstChild, Element);
-      assert.equal(betweenContent.firstChild.tagName, 'HL');
-      assert.equal(betweenContent.firstChild.className, 'some');
-      assert.equal(betweenContent.childNodes.length, 2);
-      assert.equal(betweenContent.firstChild.childNodes.length, 1);
-      assert.equal(betweenContent.firstChild.textContent,
-          'na💢ti te, inquit, sumus aliquando otiosum, certe a udiam, ' +
-          'quid sit, quod Epicurum');
-
-      assert.isNull(diff.querySelector('.right + .content .some'),
-          'Highlight should be applied only to the left side content.');
-
-      assert.strictEqual(betweenContent.querySelector('gr-diff-comment-thread'),
-          commentThread, 'Comment threads should be preserved.');
-
-      assert.instanceOf(endContent.childNodes[0], Element);
-      assert.equal(endContent.childNodes[0].textContent,
-          'nam et\tcomplectitur\tverbis, ');
-      assert.equal(endContent.childNodes[0].tagName, 'HL');
-      assert.equal(endContent.childNodes[0].className, 'some');
-      assert.instanceOf(endContent.childNodes[1], Text);
-      assert.equal(endContent.childNodes[1].textContent,
-          'quod vult, et dicit plane, quod intellegam;');
-      var endHl = endContent.querySelector('hl.some');
-      assert.equal(endHl.childNodes.length, 5);
-      var tabs = endHl.querySelectorAll('span.tab');
-      assert.equal(tabs.length, 2);
-      assert.equal(tabs[0].previousSibling.textContent, 'nam et');
-      assert.equal(tabs[1].previousSibling.textContent, 'complectitur');
-      assert.equal(tabs[1].nextSibling.textContent, 'verbis, ');
-    });
-
-    test('multiline highlight w/ start at end of 1st line', function() {
-      var diff = element.querySelector('#diffTable');
-      var startContent =
-          diff.querySelector('.left.lineNum[data-value="138"] ~ .content');
-      var betweenContent =
-          diff.querySelector('.left.lineNum[data-value="140"] ~ .content');
-      var endContent =
-          diff.querySelector('.left.lineNum[data-value="141"] ~ .content');
-      var commentThread =
-          diff.querySelector('gr-diff-comment-thread');
-      var builder = {
-        getCommentThreadByContentEl: sandbox.stub().returns(commentThread),
-        getContentByLine: sandbox.stub().returns({}),
-        getContentsByLineRange: sandbox.stub().returns([betweenContent]),
-        getLineElByChild: sandbox.stub().returns(
-            {getAttribute: sandbox.stub()}),
-      };
-      element._cachedDiffBuilder = builder;
-      element.enabled = true;
-      builder.getContentByLine.withArgs(138, 'left').returns(
-          startContent);
-      builder.getContentByLine.withArgs(141, 'left').returns(
-          endContent);
-
-      var expectedStartContentNodes = startContent.childNodes.length;
-
-      // The following should not cause an error.
-      element._applyRangedHighlight(
-          'some', 138, startContent.textContent.length, 141, 28, 'left');
-
-      assert.equal(startContent.childNodes.length, expectedStartContentNodes,
-          'Should not add a highlight to the start content');
-    });
-
-    suite('single line ranges', function() {
-      var diff;
-      var content;
-      var commentThread;
-      var builder;
-
-      setup(function() {
-        diff = element.querySelector('#diffTable');
-        content =
-            diff.querySelector('.left.lineNum[data-value="140"] ~ .content');
-        commentThread = diff.querySelector('gr-diff-comment-thread');
-        builder = {
-          getCommentThreadByContentEl: sandbox.stub().returns(commentThread),
-          getContentByLine: sandbox.stub().returns(content),
-          getContentsByLineRange: sandbox.stub().returns([]),
-          getLineElByChild: sandbox.stub().returns(
-              {getAttribute: sandbox.stub()}),
-        };
-        element._cachedDiffBuilder = builder;
-        element.enabled = true;
-      });
-
-      test('whole line range', function() {
-        element._applyRangedHighlight('some', 140, 0, 140, 81, 'left');
-        assert.instanceOf(content.firstChild, Element);
-        assert.equal(content.firstChild.tagName, 'HL');
-        assert.equal(content.firstChild.className, 'some');
-        assert.equal(content.childNodes.length, 2);
-        assert.equal(content.firstChild.childNodes.length, 5);
-        assert.equal(content.firstChild.textContent,
-            'na💢ti te, inquit, sumus aliquando otiosum, certe a udiam, ' +
-            'quid sit, quod Epicurum');
-        var tabs = content.querySelectorAll('span.tab');
-        assert.equal(tabs.length, 2);
-        assert.strictEqual(tabs[1].previousSibling, tabs[0].nextSibling);
-        assert.equal(tabs[0].previousSibling.textContent,
-            'na💢ti te, inquit, sumus aliquando otiosum, certe a ');
-        assert.equal(tabs[1].previousSibling.textContent,
-            'udiam, quid sit, ');
-        assert.equal(tabs[1].nextSibling.textContent, 'quod Epicurum');
-      });
-
-      test('merging multiple other hls', function() {
-        element._applyRangedHighlight('some', 140, 1, 140, 80, 'left');
-        assert.instanceOf(content.firstChild, Text);
-        assert.equal(content.childNodes.length, 4);
-        var hl = content.querySelector('hl.some');
-        assert.strictEqual(content.firstChild, hl.previousSibling);
-        assert.equal(hl.childNodes.length, 5);
-        assert.equal(content.querySelectorAll('span.tab').length, 2);
-        assert.equal(hl.textContent,
-            'a💢ti te, inquit, sumus aliquando otiosum, certe a udiam, ' +
-            'quid sit, quod Epicuru');
-      });
-
-      test('hl inside Text node', function() {
-        // Before: na💢ti
-        //  After: n<hl class="some">a💢t</hl>i
-        element._applyRangedHighlight('some', 140, 1, 140, 4, 'left');
-        var hl = content.querySelector('hl.some');
-        assert.equal(hl.outerHTML, '<hl class="some">a💢t</hl>');
-      });
-
-      test('hl ending over different hl', function() {
-        // Before: na💢ti <hl>te, inquit</hl>,
-        //  After: na💢<hl class="some">ti te</hl><hl class="foo">, inquit</hl>,
-        element._applyRangedHighlight('some', 140, 3, 140, 8, 'left');
-        var hl = content.querySelector('hl.some');
-        assert.equal(hl.outerHTML, '<hl class="some">ti te</hl>');
-        assert.equal(hl.nextSibling.outerHTML,
-            '<hl class="foo">, inquit</hl>');
-      });
-
-      test('hl starting inside different hl', function() {
-        // Before: na💢ti <hl>te, inquit</hl>, sumus
-        //  After: na💢ti <hl class="foo">te, in</hl><hl class="some">quit, ...
-        element._applyRangedHighlight('some', 140, 12, 140, 21, 'left');
-        var hl = content.querySelector('hl.some');
-        assert.equal(hl.textContent, 'quit, sum');
-        assert.equal(
-            hl.previousSibling.outerHTML, '<hl class="foo">te, in</hl>');
-      });
-
-      test('hl inside different hl', function() {
-        // Before: na💢ti <hl class="foo">te, inquit</hl>, sumus
-        //  After: <hl class="foo">t</hl><hl="some">e, i</hl><hl class="foo">n..
-        element._applyRangedHighlight('some', 140, 7, 140, 12, 'left');
-        var hl = content.querySelector('hl.some');
-        assert.equal(hl.textContent, 'e, in');
-        assert.equal(hl.previousSibling.outerHTML, '<hl class="foo">t</hl>');
-        assert.equal(hl.nextSibling.outerHTML, '<hl class="foo">quit</hl>');
-      });
-
-      test('hl starts and ends in different hls', function() {
-        element._applyRangedHighlight('some', 140, 8, 140, 27, 'left');
-        var hl = content.querySelector('hl.some');
-        assert.equal(hl.textContent, ', inquit, sumus ali');
-        assert.equal(hl.previousSibling.outerHTML, '<hl class="foo">te</hl>');
-        assert.equal(hl.nextSibling.outerHTML, '<hl class="bar">quando</hl>');
-      });
-
-      test('hl over different hl', function() {
-        element._applyRangedHighlight('some', 140, 2, 140, 21, 'left');
-        var hl = content.querySelector('hl.some');
-        assert.equal(hl.outerHTML, '<hl class="some">💢ti te, inquit, sum</hl>');
-        assert.notOk(content.querySelector('.foo'));
-      });
-
-      test('hl starting and ending in boundaries', function() {
-        element._applyRangedHighlight('some', 140, 6, 140, 33, 'left');
-        var hl = content.querySelector('hl.some');
-        assert.equal(hl.textContent, 'te, inquit, sumus aliquando');
-        assert.notOk(content.querySelector('.bar'));
-      });
-
-      test('overlapping hls', function() {
-        element._applyRangedHighlight('some', 140, 1, 140, 3, 'left');
-        element._applyRangedHighlight('some', 140, 2, 140, 4, 'left');
-        assert.equal(content.querySelectorAll('hl.some').length, 1);
-        var hl = content.querySelector('hl.some');
-        assert.equal(hl.outerHTML, '<hl class="some">a💢t</hl>');
-      });
-
-      test('growing hl right including another hl', function() {
-        element._applyRangedHighlight('some', 140, 1, 140, 4, 'left');
-        element._applyRangedHighlight('some', 140, 3, 140, 10, 'left');
-        assert.equal(content.querySelectorAll('hl.some').length, 1);
-        var hl = content.querySelector('hl.some');
-        assert.equal(hl.outerHTML, '<hl class="some">a💢ti te, </hl>');
-        assert.equal(hl.nextSibling.outerHTML, '<hl class="foo">inquit</hl>');
-      });
-
-      test('growing hl left to start of line', function() {
-        element._applyRangedHighlight('some', 140, 2, 140, 5, 'left');
-        element._applyRangedHighlight('some', 140, 0, 140, 3, 'left');
-        assert.equal(content.querySelectorAll('hl.some').length, 1);
-        var hl = content.querySelector('hl.some');
-        assert.equal(hl.outerHTML, '<hl class="some">na💢ti</hl>');
-        assert.strictEqual(content.firstChild, hl);
-      });
-
-      test('splitting hl containing a tab', function() {
-        element._applyRangedHighlight('some', 140, 63, 140, 72, 'left');
-        assert.equal(content.querySelector('hl.some').textContent, 'sit, quod');
-        element._applyRangedHighlight('another', 140, 66, 140, 81, 'left');
-        assert.equal(content.querySelector('hl.another').textContent,
-            ', quod Epicurum');
-      });
-    });
-
-    test('_applyAllHighlights', function() {
-      element.comments = {
-        left: [
-          {
-            range: {
-              start_line: 3,
-              start_character: 14,
-              end_line: 10,
-              end_character: 24,
-            },
-          },
-        ],
-        right: [
-          {
-            range: {
-              start_line: 320,
-              start_character: 200,
-              end_line: 1024,
-              end_character: 768,
-            },
-          },
-        ],
-      };
-      sandbox.stub(element, '_applyRangedHighlight');
-      element._applyAllHighlights();
-      sinon.assert.calledWith(element._applyRangedHighlight,
-          'range', 3, 14, 10, 24, 'left');
-      sinon.assert.calledWith(element._applyRangedHighlight,
-          'range', 320, 200, 1024, 768, 'right');
-    });
-
-    test('apply comment ranges on render', function() {
-      element.enabled = true;
-      sandbox.stub(element, '_applyAllHighlights');
-      element.fire('render');
-      assert.isTrue(element._applyAllHighlights.called);
-    });
-
-    test('apply comment ranges on context expand', function() {
-      element.enabled = true;
-      sandbox.stub(element, '_applyAllHighlights');
-      element.fire('show-context');
-      assert.isTrue(element._applyAllHighlights.called);
-    });
-
-    test('ignores render when disabled', function() {
-      element.enabled = false;
-      sandbox.stub(element, '_applyAllHighlights');
-      element.fire('render');
-      assert.isFalse(element._applyAllHighlights.called);
-    });
-
-    test('ignores context expand when disabled', function() {
-      element.enabled = false;
-      sandbox.stub(element, '_applyAllHighlights');
-      element.fire('show-context');
-      assert.isFalse(element._applyAllHighlights.called);
-    });
-
     suite('selection', function() {
       var diff;
       var builder;
       var contentStubs;
 
       var stubContent = function(line, side, opt_child) {
-        var content = diff.querySelector(
+        var contentTd = diff.querySelector(
             '.' + side + '.lineNum[data-value="' + line + '"] ~ .content');
+        var contentText = contentTd.querySelector('.contentText');
         var lineEl = diff.querySelector(
             '.' + side + '.lineNum[data-value="' + line + '"]');
         contentStubs.push({
           lineEl: lineEl,
-          content: content,
+          contentTd: contentTd,
+          contentText: contentText,
         });
-        builder.getContentByLineEl.withArgs(lineEl).returns(content);
+        builder.getContentByLineEl.withArgs(lineEl).returns(contentText);
         builder.getLineNumberByChild.withArgs(lineEl).returns(line);
-        builder.getContentByLine.withArgs(line, side).returns(content);
+        builder.getContentByLine.withArgs(line, side).returns(contentText);
         builder.getSideByLineEl.withArgs(lineEl).returns(side);
-        return content;
+        return contentText;
       };
 
       var emulateSelection = function(
@@ -657,7 +258,7 @@
 
       var getLineElByChild = function(node) {
         var stubs = contentStubs.find(function(stub) {
-          return stub.content.contains(node);
+          return stub.contentTd.contains(node);
         });
         return stubs && stubs.lineEl;
       };
@@ -676,7 +277,6 @@
           getSideByLineEl: sandbox.stub(),
         };
         element._cachedDiffBuilder = builder;
-        element.enabled = true;
       });
 
       teardown(function() {
@@ -775,9 +375,11 @@
       });
 
       test('starts outside of diff', function() {
-        var content = stubContent(140, 'left');
-        emulateSelection(content.previousElementSibling.firstChild, 2,
-            content.firstChild, 2);
+        var contentText = stubContent(140, 'left');
+        var contentTd = contentText.parentElement;
+
+        emulateSelection(contentTd.previousElementSibling.firstChild, 2,
+            contentText.firstChild, 2);
         assert.isFalse(element.isRangeSelected());
       });
 
@@ -797,7 +399,8 @@
 
       test('starts in comment thread element', function() {
         var startContent = stubContent(140, 'left');
-        var comment = startContent.querySelector('gr-diff-comment-thread');
+        var comment = startContent.parentElement.querySelector(
+            'gr-diff-comment-thread');
         var endContent = stubContent(141, 'left');
         emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
         assert.isTrue(element.isRangeSelected());
@@ -812,7 +415,8 @@
 
       test('ends in comment thread element', function() {
         var content = stubContent(140, 'left');
-        var comment = content.querySelector('gr-diff-comment-thread');
+        var comment = content.parentElement.querySelector(
+            'gr-diff-comment-thread');
         emulateSelection(content.firstChild, 4, comment.firstChild, 1);
         assert.isTrue(element.isRangeSelected());
         assert.deepEqual(getActionRange(), {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index ff48213..cbf63d6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -106,10 +106,9 @@
             on-tap="_handleShowTabsTap">
       </div>
       <div class="pref">
-        <label for="enableRangedCommentsInput">
-          <span class="beta">(beta)</span> Enable ranged comments</label>
-        <input is="iron-input" type="checkbox" id="enableRangedCommentsInput"
-            on-tap="_handleEnableRangedComments">
+        <label for="syntaxHighlightInput">Syntax highlighting</label>
+        <input is="iron-input" type="checkbox" id="syntaxHighlightInput"
+            on-tap="_handleSyntaxHighlightTap">
       </div>
     </div>
     <div class="actions">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
index 066405f..4103b2e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
@@ -61,13 +61,13 @@
       this._newPrefs = Object.assign({}, prefs);
       this.$.contextSelect.value = prefs.context;
       this.$.showTabsInput.checked = prefs.show_tabs;
+      this.$.syntaxHighlightInput.checked = prefs.syntax_highlighting;
     },
 
     _localPrefsChanged: function(changeRecord) {
       var localPrefs = changeRecord.base || {};
       // TODO(viktard): This is not supported in IE. Implement a polyfill.
       this._newLocalPrefs = Object.assign({}, localPrefs);
-      this.$.enableRangedCommentsInput.checked = localPrefs.ranged_comments;
     },
 
     _handleContextSelectChange: function(e) {
@@ -79,9 +79,9 @@
       this.set('_newPrefs.show_tabs', Polymer.dom(e).rootTarget.checked);
     },
 
-    _handleEnableRangedComments: function(e) {
-      this.set(
-          '_newLocalPrefs.ranged_comments', Polymer.dom(e).rootTarget.checked);
+    _handleSyntaxHighlightTap: function(e) {
+      this.set('_newPrefs.syntax_highlighting',
+          Polymer.dom(e).rootTarget.checked);
     },
 
     _handleSave: function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
index ca19669..0c40d9f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
@@ -44,6 +44,7 @@
         line_length: 100,
         show_tabs: true,
         tab_size: 8,
+        syntax_highlighting: true,
       };
       assert.deepEqual(element.prefs, element._newPrefs);
 
@@ -52,11 +53,13 @@
       element.$.columnsInput.bindValue = 80;
       element.$.tabSizeInput.bindValue = 4;
       MockInteractions.tap(element.$.showTabsInput);
+      MockInteractions.tap(element.$.syntaxHighlightInput);
 
       assert.equal(element._newPrefs.context, 50);
       assert.equal(element._newPrefs.line_length, 80);
       assert.equal(element._newPrefs.tab_size, 4);
       assert.isFalse(element._newPrefs.show_tabs);
+      assert.isFalse(element._newPrefs.syntax_highlighting);
     });
 
     test('events', function(done) {
@@ -72,22 +75,5 @@
       MockInteractions.tap(element.$$('gr-button[primary]'));
       MockInteractions.tap(element.$$('gr-button:not([primary])'));
     });
-
-    test('ranged comments as a local preference', function() {
-      element.localPrefs = {
-        ranged_comments: true,
-      };
-      var inputEl = element.$.enableRangedCommentsInput;
-      assert.isTrue(inputEl.checked, 'Binding to localPrefs');
-      MockInteractions.tap(inputEl);
-      assert.isFalse(inputEl.checked, 'Reacting to range_comments UI event.');
-      var saveStub = sinon.stub();
-      element.addEventListener('save', saveStub);
-      MockInteractions.tap(element.$$('gr-button[primary]'));
-      assert.isFalse(element.localPrefs.ranged_comments,
-          'Updating localPrefs on save.');
-      assert.isTrue(saveStub.called);
-      element.removeEventListener('save', saveStub);
-    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index f0fc649..2dd4c91 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -32,6 +32,16 @@
     REMOVED: 'edit_a',
   };
 
+  /**
+   * The maximum size for an addition or removal chunk before it is broken down
+   * into a series of chunks that are this size at most.
+   *
+   * Note: The value of 70 is chosen so that it is larger than the default
+   * _asyncThreshold of 64, but feel free to tune this constant to your
+   * performance needs.
+   */
+  var MAX_GROUP_SIZE = 70;
+
   Polymer({
     is: 'gr-diff-processor',
 
@@ -59,7 +69,31 @@
         value: function() { return {left: {}, right: {}}; },
       },
 
+      /**
+       * The maximum number of lines to process synchronously.
+       */
+      _asyncThreshold: {
+        type: Number,
+        value: 64,
+      },
+
       _nextStepHandle: Number,
+      _isScrolling: Boolean,
+    },
+
+    attached: function() {
+      this.listen(window, 'scroll', '_handleWindowScroll');
+    },
+
+    detached: function() {
+      this.unlisten(window, 'scroll', '_handleWindowScroll');
+    },
+
+    _handleWindowScroll: function() {
+      this._isScrolling = true;
+      this.debounce('resetIsScrolling', function() {
+        this._isScrolling = false;
+      }, 50);
     },
 
     /**
@@ -80,7 +114,13 @@
 
         content = this._splitCommonGroupsWithComments(content);
 
+        var currentBatch = 0;
         var nextStep = function() {
+
+          if (this._isScrolling) {
+            this.async(nextStep, 100);
+            return;
+          }
           // If we are done, resolve the promise.
           if (state.sectionIndex >= content.length) {
             resolve(this.groups);
@@ -92,13 +132,19 @@
           var result = this._processNext(state, content);
           result.groups.forEach(function(group) {
             this.push('groups', group);
+            currentBatch += group.lines.length;
           }, this);
           state.lineNums.left += result.lineDelta.left;
           state.lineNums.right += result.lineDelta.right;
 
           // Increment the index and recurse.
           state.sectionIndex++;
-          this._nextStepHandle = this.async(nextStep, 1);
+          if (currentBatch >= this._asyncThreshold) {
+            currentBatch = 0;
+            this._nextStepHandle = this.async(nextStep, 1);
+          } else {
+            nextStep.call(this);
+          }
         };
 
         nextStep.call(this);
@@ -136,8 +182,7 @@
         var sectionEnd = null;
         if (state.sectionIndex === 0) {
           sectionEnd = 'first';
-        }
-        else if (state.sectionIndex === content.length - 1) {
+        } else if (state.sectionIndex === content.length - 1) {
           sectionEnd = 'last';
         }
 
@@ -177,11 +222,11 @@
     /**
      * Take rows of a shared diff section and produce an array of corresponding
      * (potentially collapsed) groups.
-     * @param  {Array<String>} rows
-     * @param  {Number} context
-     * @param  {Number} startLineNumLeft
-     * @param  {Number} startLineNumRight
-     * @param  {String} opt_sectionEnd String representing whether this is the
+     * @param {Array<String>} rows
+     * @param {Number} context
+     * @param {Number} startLineNumLeft
+     * @param {Number} startLineNumRight
+     * @param {String} opt_sectionEnd String representing whether this is the
      *     first section or the last section or neither. Use the values 'first',
      *     'last' and null respectively.
      * @return {Array<GrDiffGroup>}
@@ -240,10 +285,10 @@
     /**
      * Take the rows of a delta diff section and produce the corresponding
      * group.
-     * @param  {Array<String>} rowsAdded
-     * @param  {Array<String>} rowsRemoved
-     * @param  {Number} startLineNumLeft
-     * @param  {Number} startLineNumRight
+     * @param {Array<String>} rowsAdded
+     * @param {Array<String>} rowsRemoved
+     * @param {Number} startLineNumLeft
+     * @param {Number} startLineNumRight
      * @return {GrDiffGroup}
      */
     _deltaGroupFromRows: function(rowsAdded, rowsRemoved, startLineNumLeft,
@@ -301,7 +346,7 @@
      * In order to show comments out of the bounds of the selected context,
      * treat them as separate chunks within the model so that the content (and
      * context surrounding it) renders correctly.
-     * @param  {Object} content The diff content object.
+     * @param {Object} content The diff content object.
      * @return {Object} A new diff content object with regions split up.
      */
     _splitCommonGroupsWithComments: function(content) {
@@ -314,13 +359,17 @@
 
         // If it isn't a common group, append it as-is and update line numbers.
         if (!content[i].ab) {
-          result.push(content[i]);
           if (content[i].a) {
             leftLineNum += content[i].a.length;
           }
           if (content[i].b) {
             rightLineNum += content[i].b.length;
           }
+
+          this._breakdownGroup(content[i]).forEach(function(group) {
+            result.push(group);
+          });
+
           continue;
         }
 
@@ -420,5 +469,48 @@
       }
       return normalized;
     },
+
+    /**
+     * If a group is an addition or a removal, break it down into smaller groups
+     * of that type using the MAX_GROUP_SIZE. If the group is a shared section
+     * or a delta it is returned as the single element of the result array.
+     * @param {!Object} A raw chunk from a diff response.
+     * @return {!Array<!Array<!Object>>}
+     */
+    _breakdownGroup: function(group) {
+      var key = null;
+      if (group.a && !group.b) {
+        key = 'a';
+      } else if (group.b && !group.a) {
+        key = 'b';
+      }
+
+      if (!key) { return [group]; }
+
+      return this._breakdown(group[key], MAX_GROUP_SIZE)
+        .map(function(subgroupLines) {
+          var subGroup = {};
+          subGroup[key] = subgroupLines;
+          return subGroup;
+        });
+    },
+
+    /**
+     * Given an array and a size, return an array of arrays where no inner array
+     * is larger than that size, preserving the original order.
+     * @param {!Array<T>} array
+     * @param {number} size
+     * @return {!Array<!Array<T>>}
+     * @template T
+     */
+    _breakdown: function(array, size) {
+      if (!array.length) { return []; }
+      if (array.length < size) { return [array]; }
+
+      var head = array.slice(0, array.length - size);
+      var tail = array.slice(array.length - size);
+
+      return this._breakdown(head, size).concat([tail]);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 120c980..4f8c532 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -40,6 +40,15 @@
         'fugit assum per.';
 
     var element;
+    var sandbox;
+
+    setup(function() {
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(function() {
+      sandbox.restore();
+    });
 
     suite('not logged in', function() {
 
@@ -209,7 +218,7 @@
       });
 
       test('insert context groups', function(done) {
-        content = [
+        var content = [
           {a: ['all work and no play make andybons a dull boy']},
           {ab: []},
           {b: ['elgoog elgoog elgoog']},
@@ -409,6 +418,23 @@
         ]);
       });
 
+      test('scrolling pauses rendering', function() {
+        var contentRow = {
+          ab: [
+            '<!DOCTYPE html>',
+            '<meta charset="utf-8">',
+          ]
+        };
+        var content = _.times(200, _.constant(contentRow));
+        sandbox.stub(element, 'async');
+        element._isScrolling = true;
+        element.process(content);
+        assert.equal(element.groups.length, 1);
+        element._isScrolling = false;
+        element.process(content);
+        assert.equal(element.groups.length, 33);
+      });
+
       suite('gr-diff-processor helpers', function() {
         var rows;
 
@@ -510,6 +536,60 @@
           assert.notOk(result[result.length - 1].afterNumber);
         });
       });
+
+      suite('_breakdown*', function() {
+        test('_breakdownGroup ignores shared groups', function() {
+          sandbox.stub(element, '_breakdown');
+          var chunk = {ab: ['blah', 'blah', 'blah']};
+          var result = element._breakdownGroup(chunk);
+          assert.deepEqual(result, [chunk]);
+          assert.isFalse(element._breakdown.called);
+        });
+
+        test('_breakdownGroup breaks down additions', function() {
+          sandbox.spy(element, '_breakdown');
+          var chunk = {b: ['blah', 'blah', 'blah']};
+          var result = element._breakdownGroup(chunk);
+          assert.deepEqual(result, [chunk]);
+          assert.isTrue(element._breakdown.called);
+        });
+
+        test('_breakdown common case', function() {
+          var array = 'Lorem ipsum dolor sit amet, suspendisse inceptos'
+              .split(' ');
+          var size = 3;
+
+          var result = element._breakdown(array, size);
+
+          result.forEach(function(subResult) {
+            assert.isAtMost(subResult.length, size);
+          });
+          var flattened = result
+              .reduce(function(a, b) { return a.concat(b); }, []);
+          assert.deepEqual(flattened, array);
+        });
+
+        test('_breakdown smaller than size', function() {
+          var array = 'Lorem ipsum dolor sit amet, suspendisse inceptos'
+              .split(' ');
+          var size = 10;
+          var expected = [array];
+
+          var result = element._breakdown(array, size);
+
+          assert.deepEqual(result, expected);
+        });
+
+        test('_breakdown empty', function() {
+          var array = [];
+          var size = 10;
+          var expected = [];
+
+          var result = element._breakdown(array, size);
+
+          assert.deepEqual(result, expected);
+        });
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 6160c9d..7d0b7ea 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -26,6 +26,10 @@
       'down': '_handleDown',
     },
 
+    attached: function() {
+      this.classList.add('selected-right');
+    },
+
     get diffBuilder() {
       if (!this._cachedDiffBuilder) {
         this._cachedDiffBuilder =
@@ -40,8 +44,15 @@
         return;
       }
       var side = this.diffBuilder.getSideByLineEl(lineEl);
-      this.classList.remove('selected-right', 'selected-left');
-      this.classList.add('selected-' + side);
+      var targetClass = 'selected-' + side;
+      var alternateClass = 'selected-' + (side === 'left' ? 'right' : 'left');
+
+      if (this.classList.contains(alternateClass)) {
+        this.classList.remove(alternateClass);
+      }
+      if (!this.classList.contains(targetClass)) {
+        this.classList.add(targetClass);
+      }
     },
 
     _handleCopy: function(e) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 0e9db86..2573ad1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -172,6 +172,7 @@
             path="[[_path]]"
             change-num="[[_changeNum]]"
             patch-range="[[_patchRange]]"
+            files-weblinks="[[_filesWeblinks]]"
             available-patches="[[_computeAvailablePatches(_change.revisions)]]">
         </gr-patch-range-select>
         <div>
@@ -199,15 +200,16 @@
             on-save="_handlePrefsSave"
             on-cancel="_handlePrefsCancel"></gr-diff-preferences>
       </gr-overlay>
-      <gr-diff id="diff"
+      <gr-diff
+          id="diff"
           project="[[_change.project]]"
           commit="[[_change.current_revision]]"
           is-image-diff="{{_isImageDiff}}"
+          files-weblinks="{{_filesWeblinks}}"
           change-num="[[_changeNum]]"
           patch-range="[[_patchRange]]"
           path="[[_path]]"
           prefs="[[_prefs]]"
-          has-ranged-comments="[[_localPrefs.ranged_comments]]"
           project-config="[[_projectConfig]]"
           view-mode="[[_diffMode]]"
           on-line-selected="_onLineSelected">
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 f7a73cf..d6a3bc0 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
@@ -81,9 +81,10 @@
       _userPrefs: Object,
       _diffMode: {
         type: String,
-        computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)'
+        computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)',
       },
       _isImageDiff: Boolean,
+      _filesWeblinks: Object,
     },
 
     behaviors: [
@@ -229,7 +230,13 @@
           }
           break;
         case 65:  // 'a'
-          if (!this._loggedIn) { return; }
+          if (e.shiftKey) { // Hide left diff.
+            e.preventDefault();
+            this.$.diff.toggleLeftDiff();
+            break;
+          }
+
+          if (!this._loggedIn) { break; }
 
           this.set('changeViewState.showReplyDialog', true);
           /* falls through */ // required by JSHint
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 6595f13..0a4d6b6 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
@@ -47,6 +47,13 @@
       element = fixture('basic');
     });
 
+    test('toggle left diff with a hotkey', function() {
+      var toggleLeftDiffStub = sinon.stub(element.$.diff, 'toggleLeftDiff');
+      MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift');  // 'a'
+      assert.isTrue(toggleLeftDiffStub.calledOnce);
+      toggleLeftDiffStub.restore();
+    });
+
     test('keyboard shortcuts', function() {
       element._changeNum = '42';
       element._patchRange = {
@@ -379,7 +386,7 @@
 
       // Set the actual value of the select, and simulate the change event.
       select.value = newMode;
-      element.fire('change', {}, { node: select });
+      element.fire('change', {}, {node: select});
 
       // Make sure the handler was called and the state is still coherent.
       assert.equal(element._getDiffViewMode(), newMode);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
index 638d7f5..2dc495a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
@@ -23,6 +23,11 @@
     this.adds = [];
     this.removes = [];
 
+    this.lineRange = {
+      left: {start: null, end: null},
+      right: {start: null, end: null},
+    };
+
     if (opt_lines) {
       opt_lines.forEach(this.addLine, this);
     }
@@ -51,6 +56,7 @@
     } else if (line.type === GrDiffLine.Type.REMOVE) {
       this.removes.push(line);
     }
+    this._updateRange(line);
   };
 
   GrDiffGroup.prototype.getSideBySidePairs = function() {
@@ -78,5 +84,33 @@
     return pairs;
   };
 
+  GrDiffGroup.prototype._updateRange = function(line) {
+    if (line.beforeNumber === 'FILE' || line.afterNumber === 'FILE') { return; }
+
+    if (line.type === GrDiffLine.Type.ADD ||
+        line.type === GrDiffLine.Type.BOTH) {
+      if (this.lineRange.right.start === null ||
+          line.afterNumber < this.lineRange.right.start) {
+        this.lineRange.right.start = line.afterNumber;
+      }
+      if (this.lineRange.right.end === null ||
+          line.afterNumber > this.lineRange.right.end) {
+        this.lineRange.right.end = line.afterNumber;
+      }
+    }
+
+    if (line.type === GrDiffLine.Type.REMOVE ||
+        line.type === GrDiffLine.Type.BOTH) {
+      if (this.lineRange.left.start === null ||
+          line.beforeNumber < this.lineRange.left.start) {
+        this.lineRange.left.start = line.beforeNumber;
+      }
+      if (this.lineRange.left.end === null ||
+          line.beforeNumber > this.lineRange.left.end) {
+        this.lineRange.left.end = line.beforeNumber;
+      }
+    }
+  };
+
   window.GrDiffGroup = GrDiffGroup;
 })(window, GrDiffLine);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
index d3063e7..563825e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
@@ -30,12 +30,19 @@
       var l1 = new GrDiffLine(GrDiffLine.Type.ADD);
       var l2 = new GrDiffLine(GrDiffLine.Type.ADD);
       var l3 = new GrDiffLine(GrDiffLine.Type.REMOVE);
+      l1.afterNumber = 128;
+      l2.afterNumber = 129;
+      l3.beforeNumber = 64;
       group.addLine(l1);
       group.addLine(l2);
       group.addLine(l3);
       assert.deepEqual(group.lines, [l1, l2, l3]);
       assert.deepEqual(group.adds, [l1, l2]);
       assert.deepEqual(group.removes, [l3]);
+      assert.deepEqual(group.lineRange, {
+        left: {start: 64, end: 64},
+        right: {start: 128, end: 129},
+      });
 
       var pairs = group.getSideBySidePairs();
       assert.deepEqual(pairs, [
@@ -57,14 +64,28 @@
 
     test('group/header line pairs', function() {
       var l1 = new GrDiffLine(GrDiffLine.Type.BOTH);
+      l1.beforeNumber = 64;
+      l1.afterNumber = 128;
+
       var l2 = new GrDiffLine(GrDiffLine.Type.BOTH);
+      l2.beforeNumber = 65;
+      l2.afterNumber = 129;
+
       var l3 = new GrDiffLine(GrDiffLine.Type.BOTH);
+      l3.beforeNumber = 66;
+      l3.afterNumber = 130;
+
       var group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]);
 
       assert.deepEqual(group.lines, [l1, l2, l3]);
       assert.deepEqual(group.adds, []);
       assert.deepEqual(group.removes, []);
 
+      assert.deepEqual(group.lineRange, {
+        left: {start: 64, end: 66},
+        right: {start: 128, end: 130},
+      });
+
       var pairs = group.getSideBySidePairs();
       assert.deepEqual(pairs, [
         {left: l1, right: l1},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index e0d003e..46612a0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -21,6 +21,7 @@
 <link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
 <link rel="import" href="../gr-diff-highlight/gr-diff-highlight.html">
 <link rel="import" href="../gr-diff-selection/gr-diff-selection.html">
+<link rel="import" href="../gr-syntax-themes/gr-theme-default.html">
 
 <dom-module id="gr-diff">
   <template>
@@ -31,6 +32,12 @@
         --light-add-highlight-color: #efe;
         --dark-add-highlight-color: #d4ffd4;
       }
+      :host.no-left .sideBySide ::content .left,
+      :host.no-left .sideBySide ::content .left + td,
+      :host.no-left .sideBySide ::content .right:not([data-value]),
+      :host.no-left .sideBySide ::content .right:not([data-value]) + td {
+        display: none;
+      }
       .diffContainer {
         border-bottom: 1px solid #eee;
         border-top: 1px solid #eee;
@@ -42,6 +49,14 @@
       table {
         border-collapse: collapse;
         border-right: 1px solid #ddd;
+        table-layout: fixed;
+      }
+      table tbody {
+        -webkit-transform: translateZ(0);
+        -moz-transform: translateZ(0);
+        -ms-transform: translateZ(0);
+        -o-transform: translateZ(0);
+        transform: translateZ(0);
       }
       .lineNum {
         background-color: #eee;
@@ -76,6 +91,13 @@
         vertical-align: top;
         white-space: pre;
       }
+      .contentText:empty:before {
+        /**
+         * Insert glyph to prevent empty diff content from collapsing.
+         * "\200B" is a 'ZERO WIDTH SPACE' (U+200B)
+         */
+        content: "\200B";
+      }
       .contextLineNum:before,
       .lineNum:before {
         display: inline-block;
@@ -88,9 +110,6 @@
       .canComment .lineNum[data-value] {
         cursor: pointer;
       }
-      .canComment .lineNum[data-value]:hover:before {
-        background-color: #ccc;
-      }
       .canComment .lineNum[data-value="FILE"]:before {
         content: 'File';
       }
@@ -101,14 +120,14 @@
         max-width: var(--content-width, 80ch);
         min-width: var(--content-width, 80ch);
       }
-      .content.add hl,
+      .content.add .intraline,
       .content.add.darkHighlight {
         background-color: var(--dark-add-highlight-color);
       }
       .content.add.lightHighlight {
         background-color: var(--light-add-highlight-color);
       }
-      .content.remove hl,
+      .content.remove .intraline,
       .content.remove.darkHighlight {
         background-color: var(--dark-remove-highlight-color);
       }
@@ -133,23 +152,25 @@
       }
       .tab {
         display: inline-block;
+        position: relative;
       }
-      .tab.withIndicator:before {
-        color: #C62828;
-        /* >> character */
-        content: '\00BB';
+      .tab.withIndicator {
+        color: #D68E47;
+        text-decoration: line-through;
       }
     </style>
+    <style include="gr-theme-default"></style>
     <div class$="[[_computeContainerClass(_loggedIn, viewMode)]]"
         on-tap="_handleTap">
       <gr-diff-selection>
         <gr-diff-highlight
             id="highlights"
             logged-in="[[_loggedIn]]"
-            enabled="[[hasRangedComments]]"
-            comments="[[_comments]]">
+            comments="{{_comments}}">
           <gr-diff-builder
               id="diffBuilder"
+              comments="[[_comments]]"
+              diff="[[_diff]]"
               view-mode="[[viewMode]]"
               is-image-diff="[[isImageDiff]]"
               base-image="[[_baseImage]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index d4cced0..dbcbb38 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -51,7 +51,11 @@
         computed: '_computeIsImageDiff(_diff)',
         notify: true,
       },
-      hasRangedComments: Boolean,
+      filesWeblinks: {
+        type: Object,
+        value: function() { return {}; },
+        notify: true,
+      },
 
       _loggedIn: {
         type: Boolean,
@@ -133,6 +137,10 @@
       return this.$.highlights.isRangeSelected();
     },
 
+    toggleLeftDiff: function() {
+      this.toggleClass('no-left');
+    },
+
     _getCommentThreads: function() {
       return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
     },
@@ -162,7 +170,9 @@
         this.$.diffBuilder.showContext(e.detail.groups, e.detail.section);
       } else if (el.classList.contains('lineNum')) {
         this.addDraftAtLine(el);
-      } if (el.classList.contains('content')) {
+      } else if (el.tagName === 'HL' ||
+          el.classList.contains('content') ||
+          el.classList.contains('contentText')) {
         var target = this.$.diffBuilder.getLineElByChild(el);
         if (target) { this._selectLine(target); }
       }
@@ -180,7 +190,8 @@
       var diffSide = e.detail.side;
       var line = range.endLine;
       var lineEl = this.$.diffBuilder.getLineElByNumber(line, diffSide);
-      var contentEl = this.$.diffBuilder.getContentByLineEl(lineEl);
+      var contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
+      var contentEl = contentText.parentElement;
       var patchNum = this._getPatchNumByLineAndContent(lineEl, contentEl);
       var side = this._getSideByLineAndContent(lineEl, contentEl);
       var threadEl = this._getOrCreateThreadAtLine(contentEl, patchNum, side);
@@ -189,8 +200,8 @@
     },
 
     _addDraft: function(lineEl, opt_lineNum) {
-      var line = opt_lineNum || lineEl.getAttribute('data-value');
-      var contentEl = this.$.diffBuilder.getContentByLineEl(lineEl);
+      var contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
+      var contentEl = contentText.parentElement;
       var patchNum = this._getPatchNumByLineAndContent(lineEl, contentEl);
       var side = this._getSideByLineAndContent(lineEl, contentEl);
       var threadEl = this._getOrCreateThreadAtLine(contentEl, patchNum, side);
@@ -334,7 +345,7 @@
     },
 
     _render: function() {
-      this.$.diffBuilder.render(this._diff, this._comments, this.prefs);
+      this.$.diffBuilder.render(this._comments, this.prefs);
     },
 
     _clearDiffContent: function() {
@@ -351,7 +362,13 @@
           this.patchRange.basePatchNum,
           this.patchRange.patchNum,
           this.path,
-          this._handleGetDiffError.bind(this));
+          this._handleGetDiffError.bind(this)).then(function(diff) {
+               this.filesWeblinks = {
+                 meta_a: diff.meta_a && diff.meta_a.web_links,
+                 meta_b: diff.meta_b && diff.meta_b.web_links,
+               };
+               return diff;
+             }.bind(this));
     },
 
     _getDiffComments: function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 236cf35..c33eadb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -44,6 +44,13 @@
         element = fixture('basic');
       });
 
+      test('toggleLeftDiff', function() {
+        element.toggleLeftDiff();
+        assert.isTrue(element.classList.contains('no-left'));
+        element.toggleLeftDiff();
+        assert.isFalse(element.classList.contains('no-left'));
+      });
+
       test('get drafts', function(done) {
         element.patchRange = {basePatchNum: 0, patchNum: 0};
 
@@ -56,6 +63,27 @@
         });
       });
 
+      test('loads files weblinks', function(done) {
+        var diffStub = sinon.stub(element.$.restAPI, 'getDiff').returns(
+            Promise.resolve({
+              meta_a: {
+                web_links: 'foo',
+              },
+              meta_b: {
+                web_links: 'bar',
+              },
+            }));
+        element.patchRange = {};
+        element._getDiff().then(function() {
+          assert.deepEqual(element.filesWeblinks, {
+            meta_a: 'foo',
+            meta_b: 'bar',
+          });
+          done();
+        });
+        diffStub.restore();
+      });
+
       test('remove comment', function() {
         element._comments = {
           meta: {
@@ -293,6 +321,8 @@
           element._handleTap(e);
           assert.isTrue(selectStub.called);
           assert.equal(selectStub.lastCall.args[0], lineEl);
+          selectStub.restore();
+          getLineStub.restore();
           done();
         });
         content.click();
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
index b0ee0b73..c496703 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -38,6 +38,12 @@
         </template>
       </select>
     </span>
+    <span is="dom-if" if="[[filesWeblinks.meta_a]]">
+      <template is="dom-repeat" items="[[filesWeblinks.meta_a]]" as="weblink">
+        <a target="_blank"
+           href$="[[weblink.url]]">[[weblink.name]]</a>
+      </template>
+    </span>
     &rarr;
     <span class="patchRange">
       <select id="rightPatchSelect" on-change="_handlePatchChange">
@@ -47,6 +53,12 @@
               disabled$="[[_computeRightDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
         </template>
       </select>
+      <span is="dom-if" if="[[filesWeblinks.meta_b]]">
+        <template is="dom-repeat" items="[[filesWeblinks.meta_b]]" as="weblink">
+          <a target="_blank"
+             href$="[[weblink.url]]">[[weblink.name]]</a>
+        </template>
+      </span>
     </span>
   </template>
   <script src="gr-patch-range-select.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index 3439ecd..24d36c4 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -20,6 +20,7 @@
     properties: {
       availablePatches: Array,
       changeNum: String,
+      filesWeblinks: Object,
       patchRange: Object,
       path: String,
     },
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index a7d909e..c7e1196 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -89,5 +89,28 @@
       rightSelectEl.value = '3';
       element.fire('change', {}, {node: leftSelectEl});
     });
+
+    test('filesWeblinks', function() {
+      element.filesWeblinks = {
+        meta_a: [
+          {
+            name: 'foo',
+            url: 'f.oo',
+          }
+        ],
+        meta_b: [
+          {
+            name: 'bar',
+            url: 'ba.r',
+          }
+        ],
+      };
+      flushAsynchronousOperations();
+      var domApi = Polymer.dom(element.root);
+      assert.equal(
+          domApi.querySelector('a[href="f.oo"]').textContent, 'foo');
+      assert.equal(
+          domApi.querySelector('a[href="ba.r"]').textContent, 'bar');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
new file mode 100644
index 0000000..113e37f
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
@@ -0,0 +1,21 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<dom-module id="gr-ranged-comment-layer">
+  <script src="../gr-diff-highlight/gr-annotation.js"></script>
+  <script src="gr-ranged-comment-layer.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
new file mode 100644
index 0000000..7496e59
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -0,0 +1,185 @@
+// Copyright (C) 2016 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';
+
+  var HOVER_PATH_PATTERN = /^comments\.(left|right)\.\#(\d+)\.__hovering$/;
+  var SPLICE_PATH_PATTERN = /^comments\.(left|right)\.splices$/;
+
+  var RANGE_HIGHLIGHT = 'range';
+  var HOVER_HIGHLIGHT = 'rangeHighlight';
+
+  Polymer({
+    is: 'gr-ranged-comment-layer',
+
+    properties: {
+      comments: Object,
+      _listeners: {
+        type: Array,
+        value: function() { return []; },
+      },
+      _commentMap: {
+        type: Object,
+        value: function() { return {left: [], right: []}; },
+      }
+    },
+
+    observers: [
+      '_handleCommentChange(comments.*)',
+    ],
+
+    /**
+     * Layer method to add annotations to a line.
+     * @param {HTMLElement} el The DIV.contentText element to apply the
+     *     annotation to.
+     * @param {GrDiffLine} line The line object.
+     */
+    annotate: function(el, line) {
+      var ranges = [];
+      if (line.type === GrDiffLine.Type.REMOVE || (
+          line.type === GrDiffLine.Type.BOTH &&
+          el.getAttribute('data-side') !== 'right')) {
+        ranges = ranges.concat(this._getRangesForLine(line, 'left'));
+      }
+      if (line.type === GrDiffLine.Type.ADD || (
+          line.type === GrDiffLine.Type.BOTH &&
+          el.getAttribute('data-side') !== 'left')) {
+        ranges = ranges.concat(this._getRangesForLine(line, 'right'));
+      }
+
+      ranges.forEach(function(range) {
+        GrAnnotation.annotateElement(el, range.start,
+            range.end - range.start,
+            range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT);
+      });
+    },
+
+    /**
+     * Register a listener for layer updates.
+     * @param {Function<Number, Number, String>} fn The update handler function.
+     *     Should accept as arguments the line numbers for the start and end of
+     *     the update and the side as a string.
+     */
+    addListener: function(fn) {
+      this._listeners.push(fn);
+    },
+
+    /**
+     * Notify Layer listeners of changes to annotations.
+     * @param {Number} start The line where the update starts.
+     * @param {Number} end The line where the update ends.
+     * @param {String} side The side of the update. ('left' or 'right')
+     */
+    _notifyUpdateRange: function(start, end, side) {
+      this._listeners.forEach(function(listener) {
+        listener(start, end, side);
+      });
+    },
+
+    /**
+     * Handle change in the comments by updating the comment maps and by
+     * emitting appropriate update notifications.
+     * @param {Object} record The change record.
+     */
+    _handleCommentChange: function(record) {
+      if (!record.path) { return; }
+
+      // If the entire set of comments was changed.
+      if (record.path === 'comments') {
+        this._commentMap.left = this._computeCommentMap(this.comments.left);
+        this._commentMap.right = this._computeCommentMap(this.comments.right);
+        return;
+      }
+
+      // If the change only changed the `hovering` property of a comment.
+      var match = record.path.match(HOVER_PATH_PATTERN);
+      if (match) {
+        var side = match[1];
+        var index = match[2];
+        var comment = this.comments[side][index];
+        if (comment && comment.range) {
+          this._commentMap[side] = this._computeCommentMap(this.comments[side]);
+          this._notifyUpdateRange(
+              comment.range.start_line, comment.range.end_line, side);
+        }
+        return;
+      }
+
+      // If comments were spliced in or out.
+      match = record.path.match(SPLICE_PATH_PATTERN);
+      if (match) {
+        var side = match[1];
+        this._commentMap[side] = this._computeCommentMap(this.comments[side]);
+        this._handleCommentSplice(record.value, side);
+      }
+    },
+
+    /**
+     * Take a list of comments and return a sparse list mapping line numbers to
+     * partial ranges. Uses an end-character-index of -1 to indicate the end of
+     * the line.
+     * @param {Array<Object>} commentList The list of comments.
+     * @return {Object} The sparse list.
+     */
+    _computeCommentMap: function(commentList) {
+      var result = {};
+      commentList.forEach(function(comment) {
+        if (!comment.range) { return; }
+        var range = comment.range;
+        for (var line = range.start_line; line <= range.end_line; line++) {
+          if (!result[line]) { result[line] = []; }
+          result[line].push({
+            comment: comment,
+            start: line === range.start_line ? range.start_character : 0,
+            end: line === range.end_line ? range.end_character : -1,
+          });
+        }
+      });
+      return result;
+    },
+
+    /**
+     * Translate a splice record into range update notifications.
+     */
+    _handleCommentSplice: function(record, side) {
+      if (!record || !record.indexSplices) { return; }
+      record.indexSplices.forEach(function(splice) {
+        var ranges = splice.removed.length ?
+          splice.removed.map(function(c) { return c.range; }) :
+          [splice.object[splice.index].range];
+        ranges.forEach(function(range) {
+          if (!range) { return; }
+          this._notifyUpdateRange(range.start_line, range.end_line, side);
+        }.bind(this));
+      }.bind(this));
+    },
+
+    _getRangesForLine: function(line, side) {
+      var lineNum = side === 'left' ? line.beforeNumber : line.afterNumber;
+      var ranges = this.get(['_commentMap', side, lineNum]) || [];
+      return ranges
+          .map(function(range) {
+            return {
+              start: range.start,
+              end: range.end === -1 ? line.text.length : range.end,
+              hovering: !!range.comment.__hovering,
+            };
+          })
+          .sort(function(a, b) {
+            // Sort the ranges so that hovering highlights are on top.
+            return a.hovering && !b.hovering ? 1 : 0;
+          });
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
new file mode 100644
index 0000000..68b7528
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -0,0 +1,328 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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-ranged-comment-layer</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../gr-diff/gr-diff-line.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-ranged-comment-layer.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-ranged-comment-layer></gr-ranged-comment-layer>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-ranged-comment-layer', function() {
+    var element;
+    var sandbox;
+
+    setup(function() {
+      var initialComments = {
+        left: [
+          {
+            id: '12345',
+            line: 39,
+            message: 'range comment',
+            range: {
+              end_character: 9,
+              end_line: 39,
+              start_character: 6,
+              start_line: 36,
+            },
+          }, {
+            id: '23456',
+            line: 100,
+            message: 'non range comment',
+          },
+        ],
+        right: [
+          {
+            id: '34567',
+            line: 10,
+            message: 'range comment',
+            range: {
+              end_character: 22,
+              end_line: 12,
+              start_character: 10,
+              start_line: 10,
+            },
+          }, {
+            id: '45678',
+            line: 100,
+            message: 'single line range comment',
+            range: {
+              end_character: 15,
+              end_line: 100,
+              start_character: 5,
+              start_line: 100,
+            },
+          },
+        ],
+      };
+
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+      element.comments = initialComments;
+    });
+
+    teardown(function() {
+      sandbox.restore();
+    });
+
+    suite('annotate', function() {
+      var sandbox;
+      var el;
+      var line;
+      var annotateElementStub;
+
+      setup(function() {
+        sandbox = sinon.sandbox.create();
+        annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+        el = document.createElement('div');
+        el.setAttribute('data-side', 'left');
+        line = new GrDiffLine(GrDiffLine.Type.BOTH);
+        line.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,';
+      });
+
+      teardown(function() {
+        sandbox.restore();
+      });
+
+      test('type=Remove no-comment', function() {
+        line.type = GrDiffLine.Type.REMOVE;
+        line.beforeNumber = 40;
+
+        element.annotate(el, line);
+
+        assert.isFalse(annotateElementStub.called);
+      });
+
+      test('type=Remove has-comment', function() {
+        line.type = GrDiffLine.Type.REMOVE;
+        line.beforeNumber = 36;
+        var expectedStart = 6;
+        var expectedLength = line.text.length - expectedStart;
+
+        element.annotate(el, line);
+
+        assert.isTrue(annotateElementStub.called);
+        var lastCall = annotateElementStub.lastCall;
+        assert.equal(lastCall.args[0], el);
+        assert.equal(lastCall.args[1], expectedStart);
+        assert.equal(lastCall.args[2], expectedLength);
+        assert.equal(lastCall.args[3], 'range');
+      });
+
+      test('type=Remove has-comment hovering', function() {
+        line.type = GrDiffLine.Type.REMOVE;
+        line.beforeNumber = 36;
+        element.set(['comments', 'left', 0, '__hovering'], true);
+
+        var expectedStart = 6;
+        var expectedLength = line.text.length - expectedStart;
+
+        element.annotate(el, line);
+
+        assert.isTrue(annotateElementStub.called);
+        var lastCall = annotateElementStub.lastCall;
+        assert.equal(lastCall.args[0], el);
+        assert.equal(lastCall.args[1], expectedStart);
+        assert.equal(lastCall.args[2], expectedLength);
+        assert.equal(lastCall.args[3], 'rangeHighlight');
+      });
+
+      test('type=Both has-comment', function() {
+        line.type = GrDiffLine.Type.BOTH;
+        line.beforeNumber = 36;
+
+        var expectedStart = 6;
+        var expectedLength = line.text.length - expectedStart;
+
+        element.annotate(el, line);
+
+        assert.isTrue(annotateElementStub.called);
+        var lastCall = annotateElementStub.lastCall;
+        assert.equal(lastCall.args[0], el);
+        assert.equal(lastCall.args[1], expectedStart);
+        assert.equal(lastCall.args[2], expectedLength);
+        assert.equal(lastCall.args[3], 'range');
+      });
+
+      test('type=Both has-comment off side', function() {
+        line.type = GrDiffLine.Type.BOTH;
+        line.beforeNumber = 36;
+        el.setAttribute('data-side', 'right');
+
+        var expectedStart = 6;
+        var expectedLength = line.text.length - expectedStart;
+
+        element.annotate(el, line);
+
+        assert.isFalse(annotateElementStub.called);
+      });
+
+      test('type=Add has-comment', function() {
+        line.type = GrDiffLine.Type.ADD;
+        line.afterNumber = 12;
+        el.setAttribute('data-side', 'right');
+
+        var expectedStart = 0;
+        var expectedLength = 22;
+
+        element.annotate(el, line);
+
+        assert.isTrue(annotateElementStub.called);
+        var lastCall = annotateElementStub.lastCall;
+        assert.equal(lastCall.args[0], el);
+        assert.equal(lastCall.args[1], expectedStart);
+        assert.equal(lastCall.args[2], expectedLength);
+        assert.equal(lastCall.args[3], 'range');
+      });
+    });
+
+    test('_handleCommentChange overwrite', function() {
+      var handlerSpy = sandbox.spy(element, '_handleCommentChange');
+      var mapSpy = sandbox.spy(element, '_computeCommentMap');
+
+      element.set('comments', {left: [], right: []});
+
+      assert.isTrue(handlerSpy.called);
+      assert.equal(mapSpy.callCount, 2);
+
+      assert.equal(Object.keys(element._commentMap.left).length, 0);
+      assert.equal(Object.keys(element._commentMap.right).length, 0);
+    });
+
+    test('_handleCommentChange hovering', function() {
+      var handlerSpy = sandbox.spy(element, '_handleCommentChange');
+      var mapSpy = sandbox.spy(element, '_computeCommentMap');
+      var notifyStub = sinon.stub();
+      element.addListener(notifyStub);
+
+      element.set(['comments', 'right', 0, '__hovering'], true);
+
+      assert.isTrue(handlerSpy.called);
+      assert.isTrue(mapSpy.called);
+
+      assert.isTrue(notifyStub.called);
+      var lastCall = notifyStub.lastCall;
+      assert.equal(lastCall.args[0], 10);
+      assert.equal(lastCall.args[1], 12);
+      assert.equal(lastCall.args[2], 'right');
+    });
+
+    test('_handleCommentChange splice out', function() {
+      var handlerSpy = sandbox.spy(element, '_handleCommentChange');
+      var mapSpy = sandbox.spy(element, '_computeCommentMap');
+      var notifyStub = sinon.stub();
+      element.addListener(notifyStub);
+
+      element.splice('comments.right', 0, 1);
+
+      assert.isTrue(handlerSpy.called);
+      assert.isTrue(mapSpy.called);
+
+      assert.isTrue(notifyStub.called);
+      var lastCall = notifyStub.lastCall;
+      assert.equal(lastCall.args[0], 10);
+      assert.equal(lastCall.args[1], 12);
+      assert.equal(lastCall.args[2], 'right');
+    });
+
+    test('_handleCommentChange splice in', function() {
+      var handlerSpy = sandbox.spy(element, '_handleCommentChange');
+      var mapSpy = sandbox.spy(element, '_computeCommentMap');
+      var notifyStub = sinon.stub();
+      element.addListener(notifyStub);
+
+      element.splice('comments.left', element.comments.left.length, 0, {
+        id: '56123',
+        line: 250,
+        message: 'new range comment',
+        range: {
+          end_character: 15,
+          end_line: 275,
+          start_character: 5,
+          start_line: 250,
+        },
+      });
+
+      assert.isTrue(handlerSpy.called);
+      assert.isTrue(mapSpy.called);
+
+      assert.isTrue(notifyStub.called);
+      var lastCall = notifyStub.lastCall;
+      assert.equal(lastCall.args[0], 250);
+      assert.equal(lastCall.args[1], 275);
+      assert.equal(lastCall.args[2], 'left');
+    });
+
+    test('_computeCommentMap creates maps correctly', function() {
+      // There is only one ranged comment on the left, but it spans ll.36-39.
+      var leftKeys = [];
+      for (var i = 36; i <= 39; i++) { leftKeys.push('' + i); }
+      assert.deepEqual(Object.keys(element._commentMap.left).sort(),
+          leftKeys.sort());
+
+      assert.equal(element._commentMap.left[36].length, 1);
+      assert.equal(element._commentMap.left[36][0].start, 6);
+      assert.equal(element._commentMap.left[36][0].end, -1);
+
+      assert.equal(element._commentMap.left[37].length, 1);
+      assert.equal(element._commentMap.left[37][0].start, 0);
+      assert.equal(element._commentMap.left[37][0].end, -1);
+
+      assert.equal(element._commentMap.left[38].length, 1);
+      assert.equal(element._commentMap.left[38][0].start, 0);
+      assert.equal(element._commentMap.left[38][0].end, -1);
+
+      assert.equal(element._commentMap.left[39].length, 1);
+      assert.equal(element._commentMap.left[39][0].start, 0);
+      assert.equal(element._commentMap.left[39][0].end, 9);
+
+      // The right has two ranged comments, one spanning ll.10-12 and the other
+      // on line 100.
+      var rightKeys = [];
+      for (i = 10; i <= 12; i++) { rightKeys.push('' + i); }
+      rightKeys.push('100');
+      assert.deepEqual(Object.keys(element._commentMap.right).sort(),
+          rightKeys.sort());
+
+      assert.equal(element._commentMap.right[10].length, 1);
+      assert.equal(element._commentMap.right[10][0].start, 10);
+      assert.equal(element._commentMap.right[10][0].end, -1);
+
+      assert.equal(element._commentMap.right[11].length, 1);
+      assert.equal(element._commentMap.right[11][0].start, 0);
+      assert.equal(element._commentMap.right[11][0].end, -1);
+
+      assert.equal(element._commentMap.right[12].length, 1);
+      assert.equal(element._commentMap.right[12][0].start, 0);
+      assert.equal(element._commentMap.right[12][0].end, 22);
+
+      assert.equal(element._commentMap.right[100].length, 1);
+      assert.equal(element._commentMap.right[100][0].start, 5);
+      assert.equal(element._commentMap.right[100][0].end, 15);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
new file mode 100644
index 0000000..c5c9377
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
@@ -0,0 +1,21 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<dom-module id="gr-syntax-layer">
+  <script src="../gr-diff/gr-diff-line.js"></script>
+  <script src="../gr-diff-highlight/gr-annotation.js"></script>
+  <script src="gr-syntax-layer.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
new file mode 100644
index 0000000..478bcc8
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -0,0 +1,402 @@
+// Copyright (C) 2016 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';
+
+  var LANGUAGE_MAP = {
+    'application/dart': 'dart',
+    'application/json': 'json',
+    'application/typescript': 'typescript',
+    'text/css': 'css',
+    'text/html': 'html',
+    'text/javascript': 'js',
+    'text/x-c': 'cpp',
+    'text/x-c++src': 'cpp',
+    'text/x-clojure': 'clojure',
+    'text/x-common-lisp': 'lisp',
+    'text/x-csharp': 'csharp',
+    'text/x-csrc': 'cpp',
+    'text/x-d': 'd',
+    'text/x-go': 'go',
+    'text/x-haskell': 'haskell',
+    'text/x-java': 'java',
+    'text/x-lua': 'lua',
+    'text/x-markdown': 'markdown',
+    'text/x-objectivec': 'objectivec',
+    'text/x-ocaml': 'ocaml',
+    'text/x-perl': 'perl',
+    'text/x-protobuf': 'protobuf',
+    'text/x-python': 'python',
+    'text/x-ruby': 'ruby',
+    'text/x-rustsrc': 'rust',
+    'text/x-scala': 'scala',
+    'text/x-sh': 'bash',
+    'text/x-sql': 'sql',
+    'text/x-swift': 'swift',
+    'text/x-yaml': 'yaml',
+  };
+  var ASYNC_DELAY = 10;
+  var HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
+
+  var CLASS_WHITELIST = {
+    'gr-diff gr-syntax gr-syntax-literal': true,
+    'gr-diff gr-syntax gr-syntax-keyword': true,
+    'gr-diff gr-syntax gr-syntax-selector-tag': true,
+    'gr-diff gr-syntax gr-syntax-built_in': true,
+    'gr-diff gr-syntax gr-syntax-type': true,
+    'gr-diff gr-syntax gr-syntax-selector-pseudo': true,
+    'gr-diff gr-syntax gr-syntax-template-variable': true,
+    'gr-diff gr-syntax gr-syntax-number': true,
+    'gr-diff gr-syntax gr-syntax-regexp': true,
+    'gr-diff gr-syntax gr-syntax-variable': true,
+    'gr-diff gr-syntax gr-syntax-selector-attr': true,
+    'gr-diff gr-syntax gr-syntax-template-tag': true,
+    'gr-diff gr-syntax gr-syntax-string': true,
+    'gr-diff gr-syntax gr-syntax-selector-id': true,
+    'gr-diff gr-syntax gr-syntax-title': true,
+    'gr-diff gr-syntax gr-syntax-params': true,
+    'gr-diff gr-syntax gr-syntax-comment': true,
+    'gr-diff gr-syntax gr-syntax-meta': true,
+    'gr-diff gr-syntax gr-syntax-meta-keyword': true,
+    'gr-diff gr-syntax gr-syntax-tag': true,
+    'gr-diff gr-syntax gr-syntax-name': true,
+    'gr-diff gr-syntax gr-syntax-attr': true,
+    'gr-diff gr-syntax gr-syntax-attribute': true,
+    'gr-diff gr-syntax gr-syntax-emphasis': true,
+    'gr-diff gr-syntax gr-syntax-strong': true,
+    'gr-diff gr-syntax gr-syntax-link': true,
+    'gr-diff gr-syntax gr-syntax-selector-class': true,
+  };
+
+  Polymer({
+    is: 'gr-syntax-layer',
+
+    properties: {
+      diff: {
+        type: Object,
+        observer: '_diffChanged',
+      },
+      enabled: {
+        type: Boolean,
+        value: true,
+      },
+      _baseRanges: {
+        type: Array,
+        value: function() { return []; },
+      },
+      _revisionRanges: {
+        type: Array,
+        value: function() { return []; },
+      },
+      _baseLanguage: String,
+      _revisionLanguage: String,
+      _listeners: {
+        type: Array,
+        value: function() { return []; },
+      },
+      _processHandle: Number,
+    },
+
+    addListener: function(fn) {
+      this.push('_listeners', fn);
+    },
+
+    /**
+     * Annotation layer method to add syntax annotations to the given element
+     * for the given line.
+     * @param {!HTMLElement} el
+     * @param {!GrDiffLine} line
+     */
+    annotate: function(el, line) {
+      if (!this.enabled) { return; }
+
+      // Determine the side.
+      var side;
+      if (line.type === GrDiffLine.Type.REMOVE || (
+          line.type === GrDiffLine.Type.BOTH &&
+          el.getAttribute('data-side') !== 'right')) {
+        side = 'left';
+      } else if (line.type === GrDiffLine.Type.ADD || (
+          el.getAttribute('data-side') !== 'left')) {
+        side = 'right';
+      }
+
+      // Find the relevant syntax ranges, if any.
+      var ranges = [];
+      if (side === 'left' && this._baseRanges.length >= line.beforeNumber) {
+        ranges = this._baseRanges[line.beforeNumber - 1] || [];
+      } else if (side === 'right' &&
+          this._revisionRanges.length >= line.afterNumber) {
+        ranges = this._revisionRanges[line.afterNumber - 1] || [];
+      }
+
+      // Apply the ranges to the element.
+      ranges.forEach(function(range) {
+        GrAnnotation.annotateElement(
+            el, range.start, range.length, range.className);
+      });
+    },
+
+    /**
+     * Start processing symtax for the loaded diff and notify layer listeners
+     * as syntax info comes online.
+     * @return {Promise}
+     */
+    process: function() {
+      // Discard existing ranges.
+      this._baseRanges = [];
+      this._revisionRanges = [];
+
+      if (!this.enabled || !this.diff.content.length) {
+        return Promise.resolve();
+      }
+
+      this.cancel();
+
+      if (this.diff.meta_a) {
+        this._baseLanguage = LANGUAGE_MAP[this.diff.meta_a.content_type];
+      }
+      if (this.diff.meta_b) {
+        this._revisionLanguage = LANGUAGE_MAP[this.diff.meta_b.content_type];
+      }
+      if (!this._baseLanguage && !this._revisionLanguage) {
+        return Promise.resolve();
+      }
+
+      var state = {
+        sectionIndex: 0,
+        lineIndex: 0,
+        baseContext: undefined,
+        revisionContext: undefined,
+        lineNums: {left: 1, right: 1},
+        lastNotify: {left: 1, right: 1},
+      };
+
+      return this._loadHLJS().then(function() {
+        return new Promise(function(resolve) {
+          var nextStep = function() {
+            this._processHandle = null;
+            this._processNextLine(state);
+
+            // Move to the next line in the section.
+            state.lineIndex++;
+
+            // If the section has been exhausted, move to the next one.
+            if (this._isSectionDone(state)) {
+              state.lineIndex = 0;
+              state.sectionIndex++;
+            }
+
+            // If all sections have been exhausted, finish.
+            if (state.sectionIndex >= this.diff.content.length) {
+              resolve();
+              this._notify(state);
+              return;
+            }
+
+            if (state.sectionIndex !== 0 && state.lineIndex % 100 === 0) {
+              this._notify(state);
+              this._processHandle = this.async(nextStep, ASYNC_DELAY);
+            } else {
+              nextStep.call(this);
+            }
+          };
+
+          this._processHandle = this.async(nextStep, 1);
+        }.bind(this));
+      }.bind(this));
+    },
+
+    /**
+     * Cancel any asynchronous syntax processing jobs.
+     */
+    cancel: function() {
+      if (this._processHandle) {
+        this.cancelAsync(this._processHandle);
+        this._processHandle = null;
+      }
+    },
+
+    _diffChanged: function() {
+      this.cancel();
+      this._baseRanges = [];
+      this._revisionRanges = [];
+    },
+
+    /**
+     * Take a string of HTML with the (potentially nested) syntax markers
+     * Highlight.js emits and emit a list of text ranges and classes for the
+     * markers.
+     * @param {string} str The string of HTML.
+     * @return {!Array<!Object>} The list of ranges.
+     */
+    _rangesFromString: function(str) {
+      var div = document.createElement('div');
+      div.innerHTML = str;
+      return this._rangesFromElement(div, 0);
+    },
+
+    _rangesFromElement: function(elem, offset) {
+      var result = [];
+      for (var i = 0; i < elem.childNodes.length; i++) {
+        var node = elem.childNodes[i];
+        var nodeLength = GrAnnotation.getLength(node);
+        // Note: HLJS may emit a span with class undefined when it thinks there
+        // may be a syntax error.
+        if (node.tagName === 'SPAN' && node.className !== 'undefined' &&
+            CLASS_WHITELIST.hasOwnProperty(node.className)) {
+          result.push({
+            start: offset,
+            length: nodeLength,
+            className: node.className,
+          });
+          if (node.children.length) {
+            result = result.concat(this._rangesFromElement(node, offset));
+          }
+        }
+        offset += nodeLength;
+      }
+      return result;
+    },
+
+    /**
+     * For a given state, process the syntax for the next line (or pair of
+     * lines).
+     * @param {!Object} state The processing state for the layer.
+     */
+    _processNextLine: function(state) {
+      var baseLine = undefined;
+      var revisionLine = undefined;
+      var hljs = this._getHighlightLib();
+
+      var section = this.diff.content[state.sectionIndex];
+      if (section.ab) {
+        baseLine = section.ab[state.lineIndex];
+        revisionLine = section.ab[state.lineIndex];
+        state.lineNums.left++;
+        state.lineNums.right++;
+      } else {
+        if (section.a && section.a.length > state.lineIndex) {
+          baseLine = section.a[state.lineIndex];
+          state.lineNums.left++;
+        }
+        if (section.b && section.b.length > state.lineIndex) {
+          revisionLine = section.b[state.lineIndex];
+          state.lineNums.right++;
+        }
+      }
+
+      // To store the result of the syntax highlighter.
+      var result;
+
+      if (this._baseLanguage && baseLine !== undefined) {
+        result = hljs.highlight(this._baseLanguage, baseLine, true,
+            state.baseContext);
+        this.push('_baseRanges', this._rangesFromString(result.value));
+        state.baseContext = result.top;
+      }
+
+      if (this._revisionLanguage && revisionLine !== undefined) {
+        result = hljs.highlight(this._revisionLanguage, revisionLine, true,
+            state.revisionContext);
+        this.push('_revisionRanges', this._rangesFromString(result.value));
+        state.revisionContext = result.top;
+      }
+    },
+
+    /**
+     * Tells whether the state has exhausted its current section.
+     * @param {!Object} state
+     * @return {boolean}
+     */
+    _isSectionDone: function(state) {
+      var section = this.diff.content[state.sectionIndex];
+      if (section.ab) {
+        return state.lineIndex >= section.ab.length;
+      } else {
+        return (!section.a || state.lineIndex >= section.a.length) &&
+            (!section.b || state.lineIndex >= section.b.length);
+      }
+    },
+
+    /**
+     * For a given state, notify layer listeners of any processed line ranges
+     * that have not yet been notified.
+     * @param {!Object} state
+     */
+    _notify: function(state) {
+      if (state.lineNums.left - state.lastNotify.left) {
+        this._notifyRange(
+          state.lastNotify.left,
+          state.lineNums.left,
+          'left');
+        state.lastNotify.left = state.lineNums.left;
+      }
+      if (state.lineNums.right - state.lastNotify.right) {
+        this._notifyRange(
+          state.lastNotify.right,
+          state.lineNums.right,
+          'right');
+        state.lastNotify.right = state.lineNums.right;
+      }
+    },
+
+    _notifyRange: function(start, end, side) {
+      this._listeners.forEach(function(fn) {
+        fn(start, end, side);
+      });
+    },
+
+    _getHighlightLib: function() {
+      return window.hljs;
+    },
+
+    _isHighlightLibLoaded: function() {
+      return !!this._getHighlightLib();
+    },
+
+    _configureHighlightLib: function() {
+      this._getHighlightLib().configure(
+          {classPrefix: 'gr-diff gr-syntax gr-syntax-'});
+    },
+
+    _getLibRoot: function() {
+      if (this._cachedLibRoot) { return this._cachedLibRoot; }
+
+      return this._cachedLibRoot = document.head
+          .querySelector('link[rel=import][href$="gr-app.html"]')
+          .href
+          .match(/(.+\/)elements\/gr-app\.html/)[1];
+    },
+    _cachedLibRoot: null,
+
+    /**
+     * Load and configure the HighlightJS library. If the library is already
+     * loaded, then do nothing and resolve.
+     * @return {Promise}
+     */
+    _loadHLJS: function() {
+      if (this._isHighlightLibLoaded()) { return Promise.resolve(); }
+      return new Promise(function(resolve) {
+        var script = document.createElement('script');
+        script.src = this._getLibRoot() + HLJS_PATH;
+        script.onload = function() {
+          this._configureHighlightLib();
+          resolve();
+        }.bind(this);
+        Polymer.dom(this.root).appendChild(script);
+      }.bind(this));
+    }
+  });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
new file mode 100644
index 0000000..5106671
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -0,0 +1,399 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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-syntax-layer</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
+<link rel="import" href="gr-syntax-layer.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-syntax-layer></gr-syntax-layer>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-syntax-layer tests', function() {
+    var sandbox;
+    var diff;
+    var element;
+
+    function getMockHLJS() {
+      var html = '<span class="gr-diff gr-syntax gr-syntax-string">' +
+          'ipsum</span>';
+      return {
+        configure: function() {},
+        highlight: function(lang, line, ignore, state) {
+          return {
+            value: line.replace(/ipsum/, html),
+            top: state === undefined ? 1 : state + 1,
+          };
+        },
+      };
+    }
+
+    setup(function() {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+      var mock = document.createElement('mock-diff-response');
+      diff = mock.diffResponse;
+      element.diff = diff;
+    });
+
+    teardown(function() {
+      sandbox.restore();
+    });
+
+    test('annotate without range does nothing', function() {
+      var annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+      var el = document.createElement('div');
+      el.textContent = 'Etiam dui, blandit wisi.';
+      var line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+      line.beforeNumber = 12;
+
+      element.annotate(el, line);
+
+      assert.isFalse(annotationSpy.called);
+    });
+
+    test('annotate with range applies it', function() {
+      var str = 'Etiam dui, blandit wisi.';
+      var start = 6;
+      var length = 3;
+      var className = 'foobar';
+
+      var annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+      var el = document.createElement('div');
+      el.textContent = str;
+      var line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+      line.beforeNumber = 12;
+      element._baseRanges[11] = [{
+        start: start,
+        length: length,
+        className: className,
+      }];
+
+      element.annotate(el, line);
+
+      assert.isTrue(annotationSpy.called);
+      assert.equal(annotationSpy.lastCall.args[0], el);
+      assert.equal(annotationSpy.lastCall.args[1], start);
+      assert.equal(annotationSpy.lastCall.args[2], length);
+      assert.equal(annotationSpy.lastCall.args[3], className);
+      assert.isOk(el.querySelector('hl.' + className));
+    });
+
+    test('annotate with range but disabled does nothing', function() {
+      var str = 'Etiam dui, blandit wisi.';
+      var start = 6;
+      var length = 3;
+      var className = 'foobar';
+
+      var annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+      var el = document.createElement('div');
+      el.textContent = str;
+      var line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+      line.beforeNumber = 12;
+      element._baseRanges[11] = [{
+        start: start,
+        length: length,
+        className: className,
+      }];
+      element.enabled = false;
+
+      element.annotate(el, line);
+
+      assert.isFalse(annotationSpy.called);
+    });
+
+    test('process on empty diff does nothing', function(done) {
+      element.diff = {
+        meta_a: {content_type: 'application/json'},
+        meta_b: {content_type: 'application/json'},
+        content: [],
+      };
+      var processNextSpy = sandbox.spy(element, '_processNextLine');
+
+      var processPromise = element.process();
+
+      processPromise.then(function() {
+        assert.isFalse(processNextSpy.called);
+        assert.equal(element._baseRanges.length, 0);
+        assert.equal(element._revisionRanges.length, 0);
+        done();
+      });
+    });
+
+    test('process for unsupported languages does nothing', function(done) {
+      element.diff = {
+        meta_a: {content_type: 'text/x+objective-cobol-plus-plus'},
+        meta_b: {content_type: 'application/not-a-real-language'},
+        content: [],
+      };
+      var processNextSpy = sandbox.spy(element, '_processNextLine');
+
+      var processPromise = element.process();
+
+      processPromise.then(function() {
+        assert.isFalse(processNextSpy.called);
+        assert.equal(element._baseRanges.length, 0);
+        assert.equal(element._revisionRanges.length, 0);
+        done();
+      });
+    });
+
+    test('process while disabled does nothing', function(done) {
+      var processNextSpy = sandbox.spy(element, '_processNextLine');
+      element.enabled = false;
+      var loadHLJSSpy = sandbox.spy(element, '_loadHLJS');
+
+      var processPromise = element.process();
+
+      processPromise.then(function() {
+        assert.isFalse(processNextSpy.called);
+        assert.equal(element._baseRanges.length, 0);
+        assert.equal(element._revisionRanges.length, 0);
+        assert.isFalse(loadHLJSSpy.called);
+        done();
+      });
+    });
+
+    test('process highlight ipsum', function(done) {
+      element.diff.meta_a.content_type = 'application/json';
+      element.diff.meta_b.content_type = 'application/json';
+
+      var mockHLJS = getMockHLJS();
+      var highlightSpy = sinon.spy(mockHLJS, 'highlight');
+      sandbox.stub(element, '_getHighlightLib',
+          function() { return mockHLJS; });
+      var processNextSpy = sandbox.spy(element, '_processNextLine');
+      var processPromise = element.process();
+
+      processPromise.then(function() {
+        var linesA = diff.meta_a.lines;
+        var linesB = diff.meta_b.lines;
+
+        assert.isTrue(processNextSpy.called);
+        assert.equal(element._baseRanges.length, linesA);
+        assert.equal(element._revisionRanges.length, linesB);
+
+        assert.equal(highlightSpy.callCount, linesA + linesB);
+
+        // The first line of both sides have a range.
+        [element._baseRanges[0], element._revisionRanges[0]]
+            .forEach(function(range) {
+              assert.equal(range.length, 1);
+              assert.equal(range[0].className,
+                  'gr-diff gr-syntax gr-syntax-string');
+              assert.equal(range[0].start, 'lorem '.length);
+              assert.equal(range[0].length, 'ipsum'.length);
+            });
+
+        // There are no ranges from ll.1-12 on the left and ll.1-11 on the
+        // right.
+        element._baseRanges.slice(1, 12)
+            .concat(element._revisionRanges.slice(1, 11))
+            .forEach(function(range) {
+              assert.equal(range.length, 0);
+            });
+
+        // There should be another pair of ranges on l.13 for the left and
+        // l.12 for the right.
+        [element._baseRanges[13], element._revisionRanges[12]]
+            .forEach(function(range) {
+              assert.equal(range.length, 1);
+              assert.equal(range[0].className,
+                  'gr-diff gr-syntax gr-syntax-string');
+              assert.equal(range[0].start, 32);
+              assert.equal(range[0].length, 'ipsum'.length);
+            });
+
+        // The next group should have a similar instance on either side.
+
+        var range = element._baseRanges[15];
+        assert.equal(range.length, 1);
+        assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
+        assert.equal(range[0].start, 34);
+        assert.equal(range[0].length, 'ipsum'.length);
+
+        range = element._revisionRanges[14];
+        assert.equal(range.length, 1);
+        assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
+        assert.equal(range[0].start, 35);
+        assert.equal(range[0].length, 'ipsum'.length);
+
+        done();
+      });
+    });
+
+    test('_diffChanged calls cancel', function() {
+      var cancelSpy = sandbox.spy(element, '_diffChanged');
+      element.diff = {content: []};
+      assert.isTrue(cancelSpy.called);
+    });
+
+    test('_rangesFromElement no ranges', function() {
+      var elem = document.createElement('span');
+      elem.textContent = 'Etiam dui, blandit wisi.';
+      var offset = 100;
+
+      var result = element._rangesFromElement(elem, offset);
+
+      assert.equal(result.length, 0);
+    });
+
+    test('_rangesFromElement single range', function() {
+      var str0 = 'Etiam ';
+      var str1 = 'dui, blandit';
+      var str2 = ' wisi.';
+      var className = 'gr-diff gr-syntax gr-syntax-string';
+      var offset = 100;
+
+      var elem = document.createElement('span');
+      elem.appendChild(document.createTextNode(str0));
+      var span = document.createElement('span');
+      span.textContent = str1;
+      span.className = className;
+      elem.appendChild(span);
+      elem.appendChild(document.createTextNode(str2));
+
+      var result = element._rangesFromElement(elem, offset);
+
+      assert.equal(result.length, 1);
+      assert.equal(result[0].start, str0.length + offset);
+      assert.equal(result[0].length, str1.length);
+      assert.equal(result[0].className, className);
+    });
+
+    test('_rangesFromElement non-whitelist', function() {
+      var str0 = 'Etiam ';
+      var str1 = 'dui, blandit';
+      var str2 = ' wisi.';
+      var className = 'not-in-the-whitelist';
+      var offset = 100;
+
+      var elem = document.createElement('span');
+      elem.appendChild(document.createTextNode(str0));
+      var span = document.createElement('span');
+      span.textContent = str1;
+      span.className = className;
+      elem.appendChild(span);
+      elem.appendChild(document.createTextNode(str2));
+
+      var result = element._rangesFromElement(elem, offset);
+
+      assert.equal(result.length, 0);
+    });
+
+    test('_rangesFromElement milti range', function() {
+      var str0 = 'Etiam ';
+      var str1 = 'dui,';
+      var str2 = ' blandit';
+      var str3 = ' wisi.';
+      var className = 'gr-diff gr-syntax gr-syntax-string';
+      var offset = 100;
+
+      var elem = document.createElement('span');
+      elem.appendChild(document.createTextNode(str0));
+      var span = document.createElement('span');
+      span.textContent = str1;
+      span.className = className;
+      elem.appendChild(span);
+      elem.appendChild(document.createTextNode(str2));
+      span = document.createElement('span');
+      span.textContent = str3;
+      span.className = className;
+      elem.appendChild(span);
+
+      var result = element._rangesFromElement(elem, offset);
+
+      assert.equal(result.length, 2);
+
+      assert.equal(result[0].start, str0.length + offset);
+      assert.equal(result[0].length, str1.length);
+      assert.equal(result[0].className, className);
+
+      assert.equal(result[1].start,
+          str0.length + str1.length + str2.length + offset);
+      assert.equal(result[1].length, str3.length);
+      assert.equal(result[1].className, className);
+    });
+
+    test('_rangesFromElement nested range', function() {
+      var str0 = 'Etiam ';
+      var str1 = 'dui,';
+      var str2 = ' blandit';
+      var str3 = ' wisi.';
+      var className = 'gr-diff gr-syntax gr-syntax-string';
+      var offset = 100;
+
+      var elem = document.createElement('span');
+      elem.appendChild(document.createTextNode(str0));
+      var span1 = document.createElement('span');
+      span1.textContent = str1;
+      span1.className = className;
+      elem.appendChild(span1);
+      var span2 = document.createElement('span');
+      span2.textContent = str2;
+      span2.className = className;
+      span1.appendChild(span2);
+      elem.appendChild(document.createTextNode(str3));
+
+      var result = element._rangesFromElement(elem, offset);
+
+      assert.equal(result.length, 2);
+
+      assert.equal(result[0].start, str0.length + offset);
+      assert.equal(result[0].length, str1.length + str2.length);
+      assert.equal(result[0].className, className);
+
+      assert.equal(result[1].start, str0.length + str1.length + offset);
+      assert.equal(result[1].length, str2.length);
+      assert.equal(result[1].className, className);
+    });
+
+    test('_isSectionDone', function() {
+      var state = {sectionIndex: 0, lineIndex: 0};
+      assert.isFalse(element._isSectionDone(state));
+
+      state = {sectionIndex: 0, lineIndex: 2};
+      assert.isFalse(element._isSectionDone(state));
+
+      state = {sectionIndex: 0, lineIndex: 4};
+      assert.isTrue(element._isSectionDone(state));
+
+      state = {sectionIndex: 1, lineIndex: 2};
+      assert.isFalse(element._isSectionDone(state));
+
+      state = {sectionIndex: 1, lineIndex: 3};
+      assert.isTrue(element._isSectionDone(state));
+
+      state = {sectionIndex: 3, lineIndex: 0};
+      assert.isFalse(element._isSectionDone(state));
+
+      state = {sectionIndex: 3, lineIndex: 3};
+      assert.isFalse(element._isSectionDone(state));
+
+      state = {sectionIndex: 3, lineIndex: 4};
+      assert.isTrue(element._isSectionDone(state));
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
new file mode 100644
index 0000000..e2abc52
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
@@ -0,0 +1,97 @@
+<!--
+Copyright (C) 2016 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.
+-->
+<dom-module id="gr-theme-default">
+  <template>
+    <style>
+      /**
+       * @overview Highlight.js emits the following classes that do not have
+       * styles here:
+       *    subst, symbol, class, function, doctag, meta-string, section,
+       *    builtin-name, bulletm, code, formula, quote, addition, deletion
+       * @see {@link http://highlightjs.readthedocs.io/en/latest/css-classes-reference.html}
+       */
+
+      .gr-syntax-literal,
+      .gr-syntax-keyword,
+      .gr-syntax-selector-tag {
+        font-weight: bold;
+        color: #00f;
+      }
+      .gr-syntax-built_in {
+        color: #555;
+      }
+      .gr-syntax-type,
+      .gr-syntax-selector-pseudo,
+      .gr-syntax-template-variable {
+        color: #ff00e7;
+      }
+      .gr-syntax-number {
+        color: violet;
+      }
+      .gr-syntax-regexp,
+      .gr-syntax-variable,
+      .gr-syntax-selector-attr,
+      .gr-syntax-template-tag {
+        color: #FA8602;
+      }
+      .gr-syntax-string,
+      .gr-syntax-selector-id {
+        color: #018846;
+      }
+      .gr-syntax-title {
+        color: teal;
+      }
+      .gr-syntax-params {
+        color: red;
+      }
+      .gr-syntax-comment {
+        color: #af72a9;
+        font-style: italic;
+      }
+      .gr-syntax-meta {
+        color: #0091AD;
+      }
+      .gr-syntax-meta-keyword {
+        color: #00426b;
+        font-weight: bold;
+      }
+      .gr-syntax-tag {
+        color: #DB7C00;
+      }
+      .gr-syntax-name { /* XML/HTML Tag Name */
+        color: brown;
+      }
+      .gr-syntax-attr { /* XML/HTML Attribute */
+        color: #8C7250;
+      }
+      .gr-syntax-attribute { /* CSS Property */
+        color: #299596;
+      }
+      .gr-syntax-emphasis {
+        font-style: italic;
+      }
+      .gr-syntax-strong {
+        font-weight: bold;
+      }
+      .gr-syntax-link {
+        color: blue;
+      }
+      .gr-syntax-selector-class {
+        color: #1F71FF;
+      }
+    </style>
+  </template>
+</dom-module>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index b156721..25dfaff 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -22,6 +22,7 @@
 <link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
 <link rel="import" href="./core/gr-main-header/gr-main-header.html">
 <link rel="import" href="./core/gr-router/gr-router.html">
+<link rel="import" href="./core/gr-reporting/gr-reporting.html">
 
 <link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
 <link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
@@ -122,18 +123,11 @@
     <footer role="contentinfo">
       Powered by <a href="https://www.gerritcodereview.com/" target="_blank">Gerrit Code Review</a>
       ([[_version]])
-      <span hidden$="[[!_serverConfig.gerrit.report_bug_url]]">
-        |
-        <a href$="[[_serverConfig.gerrit.report_bug_url]]" target="_blank">
-          <span hidden$="[[!_serverConfig.gerrit.report_bug_text]]">
-            [[_serverConfig.gerrit.report_bug_text]]
-          </span>
-          <span hidden$="[[_serverConfig.gerrit.report_bug_text]]">Report Bug</span>
-        </a>
-      </span>
       |
-      <a class="feedback" href="http://goo.gl/forms/ETHmIH2Kga" target="_blank">
-        PolyGerrit Feedback
+      <a class="feedback"
+          href="https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit%20Issue"
+          target="_blank">
+        Report PolyGerrit Bug
       </a>
     </footer>
     <gr-overlay id="keyboardShortcuts" with-backdrop>
@@ -143,6 +137,7 @@
     </gr-overlay>
     <gr-error-manager></gr-error-manager>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-reporting id="reporting"></gr-reporting>
   </template>
   <script src="gr-app.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 0833a72..f24ecfd 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -72,6 +72,7 @@
     },
 
     ready: function() {
+      this.$.reporting.appStarted();
       this._viewState = {
         changeView: {
           changeNum: null,
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
new file mode 100644
index 0000000..ca26ed0
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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-app</title>
+
+<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="gr-app.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-app></gr-app>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-app tests', function() {
+    var sandbox;
+    var element;
+
+    setup(function(done) {
+      sandbox = sinon.sandbox.create();
+      stub('gr-reporting', {
+        appStarted: sandbox.stub(),
+      });
+      element = fixture('basic');
+      flush(done);
+    });
+    teardown(function() {
+      sandbox.restore();
+    });
+
+    test('reporting', function() {
+      assert.isTrue(element.$.reporting.appStarted.calledOnce);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
index b3ec96c..9e1472d 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -56,7 +56,7 @@
         username: 'user username',
         registered: '2000-01-01 00:00:00.000000000',
       };
-      config = {auth: {editable_account_fields: []}},
+      config = {auth: {editable_account_fields: []}};
 
       stub('gr-rest-api-interface', {
         getAccount: function() { return Promise.resolve(account); },
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
index fdb3ab4..030b81c 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
@@ -91,7 +91,6 @@
     });
 
     test('delete email', function() {
-      var deleteSpy = sinon.spy(element, '_handleDeleteButton');
       var buttons = element.$$('table').querySelectorAll('gr-button');
 
       assert.isFalse(element.hasUnsavedChanges);
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
index 44bffe5..36c9abf 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
@@ -48,7 +48,7 @@
       });
 
       element = fixture('basic');
-      element.loadData().then(function() { flush(done) });
+      element.loadData().then(function() { flush(done); });
     });
 
     test('loads data', function() {
@@ -111,7 +111,6 @@
 
     setup(function(done) {
       account = {username: 'user name'};
-      password = 'the password';
 
       stub('gr-rest-api-interface', {
         getAccount: function() { return Promise.resolve(account); },
@@ -122,7 +121,7 @@
       });
 
       element = fixture('basic');
-      element.loadData().then(function() { flush(done) });
+      element.loadData().then(function() { flush(done); });
     });
 
     test('loads data', function() {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index f4c5184..4f1cb87 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -242,6 +242,16 @@
                   on-change="_handleShowTabsChanged">
             </span>
           </section>
+          <section>
+            <span class="title">Syntax Highlighting</span>
+            <span class="value">
+              <input
+                  id="syntaxHighlighting"
+                  type="checkbox"
+                  checked$="[[_diffPrefs.syntax_highlighting]]"
+                  on-change="_handleSyntaxHighlightingChanged">
+            </span>
+          </section>
           <gr-button
               id="saveDiffPrefs"
               on-tap="_handleSaveDiffPreferences"
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 3037409..6c62408 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -203,6 +203,11 @@
       this.set('_diffPrefs.show_tabs', this.$.showTabs.checked);
     },
 
+    _handleSyntaxHighlightingChanged: function() {
+      this.set('_diffPrefs.syntax_highlighting',
+          this.$.syntaxHighlighting.checked);
+    },
+
     _handleSaveDiffPreferences: function() {
       return this.$.restAPI.saveDiffPreferences(this._diffPrefs)
           .then(function() {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index 1935838..4e98b43 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -66,7 +66,7 @@
 
     function stubAddAccountEmail(statusCode) {
       return sinon.stub(element.$.restAPI, 'addAccountEmail',
-          function() { return Promise.resolve({ status: statusCode }); });
+          function() { return Promise.resolve({status: statusCode}); });
     }
 
     setup(function(done) {
@@ -103,8 +103,7 @@
         theme: 'DEFAULT',
         ignore_whitespace: 'IGNORE_NONE'
       };
-      config = {auth: {editable_account_fields: []}},
-      watchedProjects = [];
+      config = {auth: {editable_account_fields: []}};
 
       stub('gr-rest-api-interface', {
         getLoggedIn: function() { return Promise.resolve(true); },
@@ -114,7 +113,7 @@
           return Promise.resolve(diffPreferences);
         },
         getWatchedProjects: function() {
-          return Promise.resolve(watchedProjects);
+          return Promise.resolve([]);
         },
         getAccountEmails: function() { return Promise.resolve(); },
         getConfig: function() { return Promise.resolve(config); },
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
index 6a3cbb0..66576a3 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
@@ -73,7 +73,7 @@
 
       element = fixture('basic');
 
-      element.loadData().then(function() { flush(done) });
+      element.loadData().then(function() { flush(done); });
     });
 
     test('renders', function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
index f5d7ee7..45bf8fe 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -38,7 +38,7 @@
 
     _handleRemoveTap: function(e) {
       e.preventDefault();
-      this.fire('remove', {account: this.account}, {bubbles: false});
+      this.fire('remove', {account: this.account});
     },
 
     _getHasAvatars: function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
index b7f9715..f136907 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -23,6 +23,9 @@
       :host {
         display: inline;
       }
+      :host::after {
+        content: var(--account-label-suffix);
+      }
       gr-avatar {
         height: 1.3em;
         width: 1.3em;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index c7438df..cda2492 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -22,6 +22,13 @@
     <style>
       input {
         font-size: 1em;
+        height: 100%;
+        width: 100%;
+      }
+      input.borderless,
+      input.borderless:focus {
+        border: none;
+        outline: none;
       }
       #suggestions {
         background-color: #fff;
@@ -42,12 +49,14 @@
     </style>
     <input
         id="input"
+        class$="[[_computeClass(borderless)]]"
         is="iron-input"
         disabled$="[[disabled]]"
         bind-value="{{text}}"
         placeholder="[[placeholder]]"
         on-keydown="_handleInputKeydown"
-        on-focus="_updateSuggestions" />
+        on-focus="_updateSuggestions"
+        autocomplete="off" />
     <div
         id="suggestions"
         hidden$="[[_computeSuggestionsHidden(_suggestions)]]">
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 6ab5fa2..6fc09ca 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -14,6 +14,8 @@
 (function() {
   'use strict';
 
+  var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
+
   Polymer({
     is: 'gr-autocomplete',
 
@@ -59,11 +61,14 @@
         value: 1,
       },
 
+      borderless: Boolean,
       disabled: Boolean,
 
       text: {
         type: String,
+        value: '',
         observer: '_updateSuggestions',
+        notify: true,
       },
 
       placeholder: String,
@@ -75,6 +80,15 @@
 
       value: Object,
 
+      /**
+       * Multi mode appends autocompleted entries to the value.
+       * If false, autocompleted entries replace value.
+       */
+      multi: {
+        type: Boolean,
+        value: false,
+      },
+
       _suggestions: {
         type: Array,
         value: function() { return []; },
@@ -86,6 +100,7 @@
         type: Boolean,
         value: false,
       },
+
     },
 
     attached: function() {
@@ -96,6 +111,10 @@
       this.unlisten(document.body, 'click', '_handleBodyClick');
     },
 
+    get focusStart() {
+      return this.$.input;
+    },
+
     focus: function() {
       this.$.input.focus();
     },
@@ -115,15 +134,19 @@
     },
 
     _updateSuggestions: function() {
-      if (this._disableSuggestions) { return; }
-
+      if (!this.text || this._disableSuggestions) { return; }
       if (this.text.length < this.threshold) {
         this._suggestions = [];
         this.value = null;
         return;
       }
+      var text = this.text;
 
-      this.query(this.text).then(function(suggestions) {
+      this.query(text).then(function(suggestions) {
+        if (text !== this.text) {
+          // Late response.
+          return;
+        }
         this._suggestions = suggestions;
         this.$.cursor.moveToStart();
         if (this._index === -1) {
@@ -136,6 +159,10 @@
       return !suggestions.length;
     },
 
+    _computeClass: function(borderless) {
+      return borderless ? 'borderless' : '';
+    },
+
     _getSuggestionElems: function() {
       Polymer.dom.flush();
       return this.$.suggestions.querySelectorAll('li');
@@ -155,6 +182,7 @@
           e.preventDefault();
           this._cancel();
           break;
+        case 9: // Tab
         case 13: // Enter
           e.preventDefault();
           this._commit();
@@ -170,7 +198,16 @@
 
     _updateValue: function(suggestions, index) {
       if (!suggestions.length || index === -1) { return; }
-      this.value = suggestions[index].value;
+      var completed = suggestions[index].value;
+      if (this.multi) {
+        // Append the completed text to the end of the string.
+        // Allow spaces within quoted terms.
+        var tokens = this.text.match(TOKENIZE_REGEX);
+        tokens[tokens.length - 1] = completed;
+        this.value = tokens.join(' ');
+      } else {
+        this.value = completed;
+      }
     },
 
     _handleBodyClick: function(e) {
@@ -189,14 +226,24 @@
     },
 
     _commit: function() {
-      this._updateValue(this._suggestions, this._index);
+      // Allow values that are not in suggestion list iff suggestions are empty.
+      if (this._suggestions.length > 0) {
+        this._updateValue(this._suggestions, this._index);
+      } else {
+        this.value = this.text;
+      }
 
       var value = this.value;
 
-      if (!this.clearOnCommit && this._suggestions[this._index]) {
-        this.setText(this._suggestions[this._index].name);
+      // Value and text are mirrors of each other in multi mode.
+      if (this.multi) {
+        this.setText(this.value);
       } else {
-        this.clear();
+        if (!this.clearOnCommit && this._suggestions[this._index]) {
+          this.setText(this._suggestions[this._index].name);
+        } else {
+          this.clear();
+        }
       }
 
       this.fire('commit', {value: value});
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 755dc93..f8b16b7 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -208,5 +208,38 @@
 
       assert.isTrue(queryStub.called);
     });
+
+    test('_computeClass respects border property', function() {
+      assert.equal(element._computeClass(), '');
+      assert.equal(element._computeClass(false), '');
+      assert.equal(element._computeClass(true), 'borderless');
+    });
+
+    test('undefined or empty text results in no suggestions', function() {
+      sinon.spy(element, '_updateSuggestions');
+      element.text = undefined;
+      assert(element._updateSuggestions.calledOnce);
+      assert.equal(element._suggestions.length, 0);
+    });
+
+    test('multi completes only the last part of the query', function(done) {
+      var promise;
+      var queryStub = sinon.stub()
+          .returns(promise = Promise.resolve([{name: 'suggestion', value: 0}]));
+      element.query = queryStub;
+      element.text = 'blah blah';
+      element.multi = true;
+
+      promise.then(function() {
+        var commitHandler = sinon.spy();
+        element.addEventListener('commit', commitHandler);
+
+        MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
+
+        assert.isTrue(commitHandler.called);
+        assert.equal(element.text, 'blah 0');
+        done();
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index c815ffd..cc0da66 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -22,7 +22,7 @@
   <template strip-whitespace>
     <style>
       :host {
-        background-color: #fff;
+        background-color: #f5f5f5;
         border: 1px solid #d1d2d3;
         border-radius: 2px;
         box-sizing: border-box;
@@ -30,10 +30,10 @@
         cursor: pointer;
         display: inline-block;
         font-family: var(--font-family);
-        font-size: 13px;
+        font-size: 12px;
         font-weight: bold;
         outline-width: 0;
-        padding: .3em .65em;
+        padding: .4em .85em;
         position: relative;
         text-align: center;
         -moz-user-select: none;
@@ -44,10 +44,17 @@
       :host([hidden]) {
         display: none;
       }
+      :host([primary]),
+      :host([secondary]) {
+        color: #fff;
+      }
       :host([primary]) {
         background-color: #4d90fe;
         border-color: #3079ed;
-        color: #fff;
+      }
+      :host([secondary]) {
+        background-color: #d14836;
+        border-color: transparent;
       }
       :host([small]) {
         font-size: 12px;
@@ -74,25 +81,44 @@
       :host([loading][disabled]) {
         cursor: wait;
       }
-      :host(:focus),
-      :host(:hover) {
-        border-color: #666;
+      :host(:focus:not([primary]:not[secondary])),
+      :host(:hover:not([primary]:not[secondary])) {
+        background-color: #f8f8f8;
+        border-color: #aaa;
       }
       :host(:active) {
         border-color: #d1d2d3;
         color: #aaa;
       }
+      :host([primary]:focus),
+      :host([secondary]:focus),
+      :host([primary]:active),
+      :host([secondary]:active) {
+        color: #fff;
+      }
       :host([primary]:focus) {
-        border-color: #fff;
         box-shadow: 0 0 1px #00f;
       }
       :host([primary]:hover) {
+        background-color: #4d90fe;
         border-color: #00F;
       }
+      :host([primary]:active),
+      :host([secondary]:active) {
+        box-shadow: none;
+      }
       :host([primary]:active) {
         border-color: #0c2188;
-        box-shadow: none;
-        color: #fff;
+      }
+      :host([secondary]:focus) {
+        box-shadow: 0 0 1px #f00;
+      }
+      :host([secondary]:hover) {
+        background-color: #c53727;
+        border: 1px solid #b0281a;
+      }
+      :host([secondary]:active) {
+        border-color: #941c0c;
       }
       :host([primary][loading]),
       :host([primary][disabled]) {
@@ -100,6 +126,7 @@
         border-color: transparent;
         color: #fff;
       }
+
     </style>
     <content></content>
   </template>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index 0d3ea3d..7b3bc23 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -63,15 +63,6 @@
         type: String,
         value: ScrollBehavior.NEVER,
       },
-
-      /**
-       * When using the 'keep-visible' scroll behavior, set an offset to the top
-       * of the window for what is considered above the upper fold.
-       */
-      foldOffsetTop: {
-        type: Number,
-        value: 0,
-      },
     },
 
     detached: function() {
@@ -214,7 +205,7 @@
       }
 
       if (this.scroll === ScrollBehavior.KEEP_VISIBLE &&
-          top > window.pageYOffset + this.foldOffsetTop &&
+          top > window.pageYOffset &&
           top < window.pageYOffset + window.innerHeight) { return; }
 
       // Scroll the element to the middle of the window. Dividing by a third
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
index f6507e1..5b49dcc 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
@@ -48,7 +48,7 @@
       <div class="editButtons">
         <gr-button primary
             on-tap="_handleSave"
-            disabled="[[disabled]]">Save</gr-button>
+            disabled="[[_saveDisabled]]">Save</gr-button>
         <gr-button
             on-tap="_handleCancel"
             disabled="[[disabled]]">Cancel</gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
index 3eb7405..77e2272 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
@@ -31,19 +31,24 @@
 
     properties: {
       content: {
-        type: String,
         notify: true,
+        type: String,
       },
       disabled: {
-        type: Boolean,
         reflectToAttribute: true,
-      },
-      editing: {
         type: Boolean,
         value: false,
-        observer: '_editingChanged',
       },
-
+      editing: {
+        observer: '_editingChanged',
+        type: Boolean,
+        value: false,
+      },
+      _saveDisabled: {
+        computed: '_computeSaveDisabled(disabled, content, _newContent)',
+        type: Boolean,
+        value: true,
+      },
       _newContent: String,
     },
 
@@ -56,6 +61,10 @@
       this._newContent = this.content;
     },
 
+    _computeSaveDisabled: function(disabled, content, newContent) {
+      return disabled || (content === newContent);
+    },
+
     _handleSave: function(e) {
       e.preventDefault();
       this.fire('editable-content-save', {content: this._newContent});
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
index bfeb4be..999f171 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
@@ -54,11 +54,35 @@
       MockInteractions.tap(element.$$('gr-button:not([primary])'));
     });
 
-    test('editing content updates', function() {
+    test('enabling editing updates edit field contents', function() {
       element.content = 'current content';
       element._newContent = 'stale content';
       element.editing = true;
       assert.equal(element._newContent, 'current content');
     });
+
+    test('disabling editing does not update edit field contents', function() {
+      element.content = 'current content';
+      element.editing = true;
+      element._newContent = 'stale content';
+      element.editing = false;
+      assert.equal(element._newContent, 'stale content');
+    });
+
+    suite('editing', function() {
+      setup(function() {
+        element.content = 'current content';
+        element.editing = true;
+      });
+
+      test('save button is disabled initially', function() {
+        assert.isTrue(element.$$('gr-button[primary]').disabled);
+      });
+
+      test('save button is enabled when content changes', function() {
+        element._newContent = 'new content';
+        assert.isFalse(element.$$('gr-button[primary]').disabled);
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
index 33180d8..76a9c77 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
@@ -24,6 +24,11 @@
       }
       label {
         color: #777;
+        display: inline-block;
+        max-width: 8em;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
       }
       label.editable {
         cursor: pointer;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
index 0121308..eb604f7 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -32,6 +32,7 @@
         type: String,
         notify: true,
         value: null,
+        observer: '_updateTitle',
       },
       placeholder: {
         type: String,
@@ -100,5 +101,9 @@
       }
       return classes.join(' ');
     },
+
+    _updateTitle: function(value) {
+      this.setAttribute('title', (value && value.length) ? value : null);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index 3030870..4919a5a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -39,6 +39,14 @@
     var element;
     var changeActions;
 
+    // Because deepEqual doesn’t behave in Safari.
+    function assertArraysEqual(actual, expected) {
+      assert.equal(actual.length, expected.length);
+      for (var i = 0; i < actual.length; i++) {
+        assert.equal(actual[i], expected[i]);
+      }
+    }
+
     setup(function() {
       element = fixture('basic');
       var plugin;
@@ -61,14 +69,6 @@
       });
     });
 
-    // Because deepEqual doesn’t behave in Safari.
-    function assertArraysEqual(actual, expected) {
-      assert.equal(actual.length, expected.length);
-      for (var i = 0; i < actual.length; i++) {
-        assert.equal(actual[i], expected[i]);
-      }
-    }
-
     test('add/remove primary action keys', function() {
       element.primaryActionKeys = [];
       changeActions.addPrimaryActionKey('foo');
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
index bb37085..4dfcf48 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -20,6 +20,7 @@
     SHOW_CHANGE: 'showchange',
     SUBMIT_CHANGE: 'submitchange',
     COMMENT: 'comment',
+    REVERT: 'revert',
   };
 
   var Element = {
@@ -148,6 +149,17 @@
       });
     },
 
+    modifyRevertMsg: function(change, msg) {
+      this._getEventCallbacks(EventType.REVERT).forEach(function(callback) {
+        try {
+          msg = callback(change, msg);
+        } catch (err) {
+          console.error(err);
+        }
+      });
+      return msg;
+    },
+
     _getEventCallbacks: function(type) {
       return this._eventCallbacks[type] || [];
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index c12d653..46a555a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -102,6 +102,25 @@
       element.handleEvent(element.EventType.COMMENT, {node: testCommentNode});
     });
 
+    test('revert event', function(done) {
+      function appendToRevertMsg(c, msg) {
+        return msg + '\ninfo';
+      }
+      done();
+
+      assert.equal(element.modifyRevertMsg(null, 'test'), 'test');
+      assert.equal(errorStub.callCount, 0);
+
+      plugin.on(element.EventType.REVERT, throwErrFn);
+      plugin.on(element.EventType.REVERT, appendToRevertMsg);
+      assert.equal(element.modifyRevertMsg(null, 'test'), 'test\ninfo');
+      assert.isTrue(errorStub.calledOnce);
+
+      plugin.on(element.EventType.REVERT, appendToRevertMsg);
+      assert.equal(element.modifyRevertMsg(null, 'test'), 'test\ninfo\ninfo');
+      assert.isTrue(errorStub.calledTwice);
+    });
+
     test('labelchange event', function(done) {
       var testChange = {_number: 42};
       plugin.on(element.EventType.LABEL_CHANGE, throwErrFn);
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
index fa32039..da28e49 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -61,7 +61,7 @@
      */
     _awaitOpen: function(fn) {
       var iters = 0;
-      function step() {
+      var step = function() {
         this.async(function() {
           if (this.style.display !== 'none') {
             fn.call(this);
@@ -69,7 +69,7 @@
             step.call(this);
           }
         }.bind(this), AWAIT_STEP);
-      };
+      }.bind(this);
       step.call(this);
     },
   });
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 25a432b..0b07217 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
@@ -250,6 +250,11 @@
       });
     },
 
+    refreshCredentials: function() {
+      this._cache = {};
+      return this.getLoggedIn();
+    },
+
     getPreferences: function() {
       return this.getLoggedIn().then(function(loggedIn) {
         if (loggedIn) {
@@ -462,8 +467,26 @@
       });
     },
 
-    getSuggestedProjects: function(inputVal, opt_errFn, opt_ctx) {
-      return this.fetchJSON('/projects/', opt_errFn, opt_ctx, { p: inputVal, });
+    getSuggestedGroups: function(inputVal, opt_n, opt_errFn, opt_ctx) {
+      return this.fetchJSON('/groups/', opt_errFn, opt_ctx, {
+        s: inputVal,
+        n: opt_n,
+      });
+    },
+
+    getSuggestedProjects: function(inputVal, opt_n, opt_errFn, opt_ctx) {
+      return this.fetchJSON('/projects/', opt_errFn, opt_ctx, {
+        p: inputVal,
+        n: opt_n,
+      });
+    },
+
+    getSuggestedAccounts: function(inputVal, opt_n, opt_errFn, opt_ctx) {
+      return this.fetchJSON('/accounts/', opt_errFn, opt_ctx, {
+        q: inputVal,
+        n: opt_n,
+        suggest: null,
+      });
     },
 
     addChangeReviewer: function(changeNum, reviewerID) {
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 a8932bf..8dda2ce 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
@@ -33,29 +33,37 @@
 <script>
   suite('gr-rest-api-interface tests', function() {
     var element;
+    var sandbox;
 
     setup(function() {
+      sandbox = sinon.sandbox.create();
       element = fixture('basic');
     });
 
+    teardown(function() {
+      sandbox.restore();
+    });
+
     test('JSON prefix is properly removed', function(done) {
       var testJSON = ')]}\'\n{"hello": "bonjour"}';
 
-      var fetchStub = sinon.stub(window, 'fetch', function() {
-        return Promise.resolve({ok: true, text: function() {
-          return Promise.resolve(testJSON);
-        }});
+      sandbox.stub(window, 'fetch', function() {
+        return Promise.resolve({
+          ok: true,
+          text: function() {
+            return Promise.resolve(testJSON);
+          },
+        });
       });
       element.fetchJSON('/dummy/url').then(function(obj) {
         assert.deepEqual(obj, {hello: 'bonjour'});
-        fetchStub.restore();
         done();
       });
     });
 
     test('cached results', function(done) {
       var n = 0;
-      var fetchJSONStub = sinon.stub(element, 'fetchJSON', function() {
+      sandbox.stub(element, 'fetchJSON', function() {
         return Promise.resolve(++n);
       });
       var promises = [];
@@ -67,7 +75,6 @@
         assert.deepEqual(results, [1, 1, 1]);
         element._fetchSharedCacheURL('/foo').then(function(foo) {
           assert.equal(foo, 1);
-          fetchJSONStub.restore();
           done();
         });
       });
@@ -105,22 +112,23 @@
 
     test('request callbacks can be canceled', function(done) {
       var cancelCalled = false;
-      var fetchStub = sinon.stub(window, 'fetch', function() {
-        return Promise.resolve({body: {
-          cancel: function() { cancelCalled = true; }
-        }});
+      sandbox.stub(window, 'fetch', function() {
+        return Promise.resolve({
+          body: {
+            cancel: function() { cancelCalled = true; }
+          },
+        });
       });
       element.fetchJSON('/dummy/url', null, function() { return true; }).then(
         function(obj) {
           assert.isUndefined(obj);
           assert.isTrue(cancelCalled);
-          fetchStub.restore();
           done();
         });
     });
 
     test('parent diff comments are properly grouped', function(done) {
-      var fetchJSONStub = sinon.stub(element, 'fetchJSON', function() {
+      sandbox.stub(element, 'fetchJSON', function() {
         return Promise.resolve({
           '/COMMIT_MSG': [],
           'sieve.go': [
@@ -147,13 +155,12 @@
             message: 'this isn’t quite right',
             path: 'sieve.go',
           });
-          fetchJSONStub.restore();
           done();
         });
     });
 
     test('differing patch diff comments are properly grouped', function(done) {
-      var fetchJSONStub = sinon.stub(element, 'fetchJSON', function(url) {
+      sandbox.stub(element, 'fetchJSON', function(url) {
         if (url == '/changes/42/revisions/1') {
           return Promise.resolve({
             '/COMMIT_MSG': [],
@@ -201,7 +208,6 @@
             message: '¯\\_(ツ)_/¯',
             path: 'sieve.go',
           });
-          fetchJSONStub.restore();
           done();
         });
     });
@@ -235,22 +241,21 @@
 
     test('rebase always enabled', function(done) {
       var resolveFetchJSON;
-      var fetchJSONStub = sinon.stub(element, 'fetchJSON').returns(
+      sandbox.stub(element, 'fetchJSON').returns(
           new Promise(function(resolve) {
             resolveFetchJSON = resolve;
           }));
       element.getChangeRevisionActions('42', '1337').then(
           function(response) {
             assert.isTrue(response.rebase.enabled);
-            fetchJSONStub.restore();
             done();
           });
       resolveFetchJSON({rebase: {}});
     });
 
     test('server error', function(done) {
-      var getResponseObjectStub = sinon.stub(element, 'getResponseObject');
-      var fetchStub = sinon.stub(window, 'fetch', function() {
+      var getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
+      sandbox.stub(window, 'fetch', function() {
         return Promise.resolve({ok: false});
       });
       var serverErrorEventPromise = new Promise(function(resolve) {
@@ -261,12 +266,37 @@
           function(response) {
             assert.isUndefined(response);
             assert.isTrue(getResponseObjectStub.notCalled);
-            getResponseObjectStub.restore();
-            fetchStub.restore();
             serverErrorEventPromise.then(function() {
               done();
             });
           });
     });
+
+    test('refreshCredentials', function(done) {
+      var responses = [
+        {
+          ok: false,
+          status: 403,
+          text: function() { return Promise.resolve(); }
+        },
+        {
+          ok: true,
+          status: 200,
+          text: function() { return Promise.resolve(')]}\'{}'); }
+        },
+      ];
+      var fetchStub = sandbox.stub(window, 'fetch', function(url) {
+        if (url === '/accounts/self/detail') {
+          return Promise.resolve(responses.shift());
+        }
+      });
+      element.getLoggedIn().then(function(isLoggedIn) {
+        assert.isFalse(isLoggedIn);
+        element.refreshCredentials().then(function(isRefreshed) {
+          assert.isTrue(isRefreshed);
+          done();
+        });
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/mock-diff-response_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
similarity index 99%
rename from polygerrit-ui/app/elements/diff/gr-diff-cursor/mock-diff-response_test.html
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
index e92247e..8141c8a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/mock-diff-response_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
@@ -67,7 +67,8 @@
               'Justo purus, semper eget et.',
             ],
           },
-          { 'a': [
+          {
+            'a': [
               'Est amet, vestibulum pellentesque.',
               'Erat ligula.',
               'Justo eros.',
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
index a2afb89..ff41a74 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -58,7 +58,7 @@
 
     _getDraftKey: function(location) {
       return ['draft', location.changeNum, location.patchNum, location.path,
-          location.line].join(':');
+          location.line || ''].join(':');
     },
 
     _cleanupDrafts: function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
index d826577..54e5577 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -33,12 +33,6 @@
     var element;
     var storage;
 
-    setup(function() {
-      element = fixture('basic');
-      storage = element._storage;
-      cleanupStorage();
-    });
-
     function cleanupStorage() {
       // Make sure there are no entries in storage.
       for (var key in window.localStorage) {
@@ -46,6 +40,12 @@
       }
     }
 
+    setup(function() {
+      element = fixture('basic');
+      storage = element._storage;
+      cleanupStorage();
+    });
+
     test('storing, retrieving and erasing drafts', function() {
       var changeNum = 1234;
       var patchNum = 5;
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
index cf54b5d..faf45d8 100644
--- a/polygerrit-ui/app/styles/app-theme.html
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -20,8 +20,8 @@
   --selection-background-color: #ebf5fb;
   --default-text-color: #000;
   --view-background-color: #fff;
-  --default-horizontal-margin: 1.25rem;
-  --font-family: 'Open Sans', sans-serif;
+  --default-horizontal-margin: 1rem;
+  --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
   --monospace-font-family: 'Source Code Pro', Menlo, 'Lucida Console', Monaco, monospace;
 
   --iron-overlay-backdrop: {
diff --git a/polygerrit-ui/app/styles/fonts.css b/polygerrit-ui/app/styles/fonts.css
index 30c6e2e..b5bf9ae 100644
--- a/polygerrit-ui/app/styles/fonts.css
+++ b/polygerrit-ui/app/styles/fonts.css
@@ -1,143 +1,3 @@
-/* cyrillic-ext */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 400;
-  src: local('Open Sans'), local('OpenSans'),
-       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
-       url(../fonts/OpenSans-Regular.woff) format('woff');
-  unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
-}
-/* cyrillic */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 400;
-  src: local('Open Sans'), local('OpenSans'),
-       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
-       url(../fonts/OpenSans-Regular.woff) format('woff');
-  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
-}
-/* greek-ext */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 400;
-  src: local('Open Sans'), local('OpenSans'),
-       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
-       url(../fonts/OpenSans-Regular.woff) format('woff');
-  unicode-range: U+1F00-1FFF;
-}
-/* greek */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 400;
-  src: local('Open Sans'), local('OpenSans'),
-       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
-       url(../fonts/OpenSans-Regular.woff) format('woff');
-  unicode-range: U+0370-03FF;
-}
-/* vietnamese */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 400;
-  src: local('Open Sans'), local('OpenSans'),
-       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
-       url(../fonts/OpenSans-Regular.woff) format('woff');
-  unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB;
-}
-/* latin-ext */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 400;
-  src: local('Open Sans'), local('OpenSans'),
-       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
-       url(../fonts/OpenSans-Regular.woff) format('woff');
-  unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 400;
-  src: local('Open Sans'), local('OpenSans'),
-       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
-       url(../fonts/OpenSans-Regular.woff) format('woff');
-  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
-}
-/* cyrillic-ext */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 700;
-  src: local('Open Sans Bold'), local('OpenSans-Bold'),
-       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
-       url(../fonts/OpenSans-Bold.woff) format('woff');
-  unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
-}
-/* cyrillic */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 700;
-  src: local('Open Sans Bold'), local('OpenSans-Bold'),
-       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
-       url(../fonts/OpenSans-Bold.woff) format('woff');
-  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
-}
-/* greek-ext */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 700;
-  src: local('Open Sans Bold'), local('OpenSans-Bold'),
-       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
-       url(../fonts/OpenSans-Bold.woff) format('woff');
-  unicode-range: U+1F00-1FFF;
-}
-/* greek */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 700;
-  src: local('Open Sans Bold'), local('OpenSans-Bold'),
-       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
-       url(../fonts/OpenSans-Bold.woff) format('woff');
-  unicode-range: U+0370-03FF;
-}
-/* vietnamese */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 700;
-  src: local('Open Sans Bold'), local('OpenSans-Bold'),
-       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
-       url(../fonts/OpenSans-Bold.woff) format('woff');
-  unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB;
-}
-/* latin-ext */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 700;
-  src: local('Open Sans Bold'), local('OpenSans-Bold'),
-       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
-       url(../fonts/OpenSans-Bold.woff) format('woff');
-  unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
-  font-family: 'Open Sans';
-  font-style: normal;
-  font-weight: 700;
-  src: local('Open Sans Bold'), local('OpenSans-Bold'),
-       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
-       url(../fonts/OpenSans-Bold.woff) format('woff');
-  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
-}
 /* latin-ext */
 @font-face {
   font-family: 'Source Code Pro';
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index 6e34c2b..d367a75 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -135,6 +135,9 @@
         :host {
           font-size: 14px;
         }
+        .project {
+          width: 20em;
+        }
       }
     </style>
   </template>
diff --git a/polygerrit-ui/app/styles/main.css b/polygerrit-ui/app/styles/main.css
index 9bf33a0..f8ffb14 100644
--- a/polygerrit-ui/app/styles/main.css
+++ b/polygerrit-ui/app/styles/main.css
@@ -30,5 +30,6 @@
   transition: none; /* Override the default Polymer fade-in. */
 }
 body {
-  font: 14px 'Open Sans', sans-serif;
+  font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  line-height: 1.4;
 }
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 153db20..2714f48 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -27,6 +27,8 @@
   [
     'change-list/gr-change-list-item/gr-change-list-item_test.html',
     'change-list/gr-change-list/gr-change-list_test.html',
+    'change/gr-account-entry/gr-account-entry_test.html',
+    'change/gr-account-list/gr-account-list_test.html',
     'change/gr-change-actions/gr-change-actions_test.html',
     'change/gr-change-metadata/gr-change-metadata_test.html',
     'change/gr-change-view/gr-change-view_test.html',
@@ -47,6 +49,7 @@
     'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
     'diff/gr-diff-comment/gr-diff-comment_test.html',
     'diff/gr-diff-cursor/gr-diff-cursor_test.html',
+    'diff/gr-diff-highlight/gr-annotation_test.html',
     'diff/gr-diff-highlight/gr-diff-highlight_test.html',
     'diff/gr-diff-preferences/gr-diff-preferences_test.html',
     'diff/gr-diff-processor/gr-diff-processor_test.html',
@@ -55,7 +58,10 @@
     'diff/gr-diff/gr-diff-group_test.html',
     'diff/gr-diff/gr-diff_test.html',
     'diff/gr-patch-range-select/gr-patch-range-select_test.html',
+    'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
     'diff/gr-selection-action-box/gr-selection-action-box_test.html',
+    'diff/gr-syntax-layer/gr-syntax-layer_test.html',
+    'gr-app_test.html',
     'settings/gr-account-info/gr-account-info_test.html',
     'settings/gr-email-editor/gr-email-editor_test.html',
     'settings/gr-group-list/gr-group-list_test.html',
@@ -64,10 +70,10 @@
     'settings/gr-settings-view/gr-settings-view_test.html',
     'settings/gr-ssh-editor/gr-ssh-editor_test.html',
     'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
-    'shared/gr-autocomplete/gr-autocomplete_test.html',
     'shared/gr-account-label/gr-account-label_test.html',
     'shared/gr-account-link/gr-account-link_test.html',
     'shared/gr-alert/gr-alert_test.html',
+    'shared/gr-autocomplete/gr-autocomplete_test.html',
     'shared/gr-avatar/gr-avatar_test.html',
     'shared/gr-change-star/gr-change-star_test.html',
     'shared/gr-confirm-dialog/gr-confirm-dialog_test.html',