Merge branch 'stable-2.12' into stable-2.13

* stable-2.12:
  Change bouncycastle urls
  ListAccess: Fix incorrect behavior when group appears twice for same rule

Change-Id: Ibd331bef8541e965b582a12c58d061193497d858
diff --git a/.bazelrc b/.bazelrc
new file mode 100644
index 0000000..00acd27
--- /dev/null
+++ b/.bazelrc
@@ -0,0 +1 @@
+build --strategy=Javac=worker
diff --git a/.buckconfig b/.buckconfig
index 51318f3..3ce69da 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -1,15 +1,14 @@
 [alias]
   api = //:api
-  api_deploy = //tools/maven:api_deploy
-  api_install = //tools/maven:api_install
-  war_deploy = //tools/maven:war_deploy
-  war_install = //tools/maven:war_install
   chrome = //:chrome
   docs = //Documentation:searchfree
   firefox = //:firefox
   gerrit = //:gerrit
+  gwtgerrit = //:gwtgerrit
   headless = //:headless
+  polygerrit = //:polygerrit
   release = //:release
+  releasenotes = //ReleaseNotes:html
   safari = //:safari
   soyc = //gerrit-gwtui:ui_soyc
   soyc_r = //gerrit-gwtui:ui_soyc_r
@@ -19,11 +18,18 @@
   includes = //tools/default.defs
 
 [java]
-  src_roots = java, resources
+  jar_spool_mode = direct_to_jar
+  src_roots = java, resources, src
+  source_level = 8
+  target_level = 8
 
 [project]
-  ignore = .git
+  ignore = .git, eclipse-out, bazel-gerrit, bin
+  parallel_parsing = true
 
 [cache]
   mode = dir
   dir = ~/.gerritcodereview/buck-cache/locally-built-artifacts
+
+[test]
+  excluded_labels = manual
diff --git a/.buckversion b/.buckversion
index 9daac2c..f5fe016 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-1b03b4313b91b634bd604fc3487a05f877e59dee
+e64a2e2ada022f81e42be750b774024469551398
diff --git a/.editorconfig b/.editorconfig
index 1f149cf..cb18523 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,3 +9,4 @@
 charset = utf-8
 indent_style = space
 indent_size = 2
+continuation_indent_size = 4
diff --git a/.gitignore b/.gitignore
index 32a1826..0fe7572 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,14 +7,22 @@
 /.settings/org.eclipse.ltk.core.refactoring.prefs
 /test_site
 /.idea
-/gerrit-parent.iml
+*.iml
+*.eml
 *.sublime-*
 /gerrit-package-plugins
+/.bazel_path
 /.buckconfig.local
 /.buckjavaargs
 /.buckd
+/bazel-bin
+/bazel-genfiles
+/bazel-gerrit
+/bazel-out
+/bazel-testlogs
 /buck-cache
 /buck-out
+/eclipse-out
 /extras
 /local.properties
 *.pyc
@@ -23,3 +31,6 @@
 *.swp
 *.asc
 /bin/
+*~
+.primary_build_tool
+.gwt_work_dir
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/.mailmap b/.mailmap
index c8e2f82..598d52d 100644
--- a/.mailmap
+++ b/.mailmap
@@ -9,7 +9,8 @@
 David Ostrovsky <david@ostrovsky.org>                                                       <d.ostrovsky@gmx.de>
 Deniz Türkoglu <deniz@spotify.com>                                                          Deniz Türkoglu <deniz@spotify.com>
 Deniz Türkoglu <deniz@spotify.com>                                                          Deniz Turkoglu <deniz@spotify.com>
-Edwin Kempin <edwin.kempin@sap.com>                                                         Edwin Kempin <edwin.kempin@gmail.com>
+Edwin Kempin <ekempin@google.com>                                                           Edwin Kempin <edwin.kempin@gmail.com>
+Edwin Kempin <ekempin@google.com>                                                           Edwin Kempin <edwin.kempin@sap.com>
 Eryk Szymanski <eryksz@gmail.com>                                                           <eryksz@google.com>
 Fredrik Luthander <fredrik.luthander@sonymobile.com>                                        <fredrik@gandaraj.com>
 Fredrik Luthander <fredrik.luthander@sonymobile.com>                                        <fredrik.luthander@sonyericsson.com>
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 8f5678f..828234b 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -46,7 +46,7 @@
 org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
 org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
 org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
-org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=enabled
 org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
 org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore
 org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled
@@ -92,7 +92,7 @@
 org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
 org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
 org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
-org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=warning
 org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
 org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
diff --git a/.watchmanconfig b/.watchmanconfig
index b1869ba..4467aec 100644
--- a/.watchmanconfig
+++ b/.watchmanconfig
@@ -1,6 +1,7 @@
 {
   "ignore_dirs": [
-    "buck-out"
+    "buck-out",
+    "eclipse-out"
   ],
   "ignore_vcs": [
     ".git"
diff --git a/BUCK b/BUCK
index c986874..9657ff3 100644
--- a/BUCK
+++ b/BUCK
@@ -1,10 +1,12 @@
 include_defs('//tools/build.defs')
 
 gerrit_war(name = 'gerrit')
-gerrit_war(name = 'headless', ui = None)
-gerrit_war(name = 'chrome',   ui = 'ui_chrome')
-gerrit_war(name = 'firefox',  ui = 'ui_firefox')
-gerrit_war(name = 'safari',   ui = 'ui_safari')
+gerrit_war(name = 'gwtgerrit',   ui = 'ui_dbg')
+gerrit_war(name = 'headless',    ui = None)
+gerrit_war(name = 'chrome',      ui = 'ui_chrome')
+gerrit_war(name = 'firefox',     ui = 'ui_firefox')
+gerrit_war(name = 'safari',      ui = 'ui_safari')
+gerrit_war(name = 'polygerrit',  ui = 'polygerrit')
 gerrit_war(name = 'withdocs', docs = True)
 gerrit_war(name = 'release',  ui = 'ui_optdbg_r', docs = True, context = ['//plugins:core'],  visibility = ['//tools/maven:'])
 
diff --git a/Documentation/BUCK b/Documentation/BUCK
index 126bf1f..48ca579 100644
--- a/Documentation/BUCK
+++ b/Documentation/BUCK
@@ -1,16 +1,20 @@
 include_defs('//Documentation/asciidoc.defs')
 include_defs('//Documentation/config.defs')
+include_defs('//Documentation/license.defs')
 include_defs('//tools/git.defs')
 
 DOC_DIR = 'Documentation'
-JSUI = '//gerrit-gwtui:ui_module'
-MAIN = '//gerrit-pgm:pgm'
+
+JSUI_JAVA_DEPS = ['//gerrit-gwtui:ui_module']
+JSUI_NON_JAVA_DEPS = ['//polygerrit-ui/app:polygerrit_ui']
+MAIN_JAVA_DEPS = ['//gerrit-pgm:pgm']
 SRCS = glob(['*.txt'], excludes = ['licenses.txt'])
 
+
 genasciidoc(
   name = 'html',
   out = 'html.zip',
-  docdir = DOC_DIR,
+  directory = DOC_DIR,
   srcs = SRCS + [':licenses.txt'],
   attributes = documentation_attributes(git_describe()),
   backend = 'html5',
@@ -20,7 +24,7 @@
 genasciidoc(
   name = 'searchfree',
   out = 'searchfree.zip',
-  docdir = DOC_DIR,
+  directory = DOC_DIR,
   srcs = SRCS + [':licenses.txt'],
   attributes = documentation_attributes(git_describe()),
   backend = 'html5',
@@ -28,31 +32,23 @@
   visibility = ['PUBLIC'],
 )
 
-genrule(
+genlicenses(
   name = 'licenses.txt',
-  cmd = '$(exe :gen_licenses) --asciidoc '
-    + '--classpath $(classpath %s) ' % MAIN
-    + '--classpath $(classpath %s) ' % JSUI
-    + MAIN + ' ' + JSUI + ' >$OUT',
+  opts = ['--asciidoc'],
+  java_deps = JSUI_JAVA_DEPS + MAIN_JAVA_DEPS,
+  non_java_deps = JSUI_NON_JAVA_DEPS,
   out = 'licenses.txt',
 )
 
 # Required by Google for gerrit-review.
-genrule(
+genlicenses(
   name = 'js_licenses.txt',
-  cmd = '$(exe :gen_licenses) --partial '
-    + '--classpath $(classpath %s) ' % JSUI
-    + JSUI + ' >$OUT',
+  opts = ['--partial'],
+  java_deps = JSUI_JAVA_DEPS,
+  non_java_deps = JSUI_NON_JAVA_DEPS,
   out = 'js_licenses.txt',
 )
 
-genrule(
-  name = 'doc.css',
-  srcs = ['doc.css.in'],
-  cmd = 'cp $SRCS $OUT',
-  out = 'doc.css',
-)
-
 python_binary(
   name = 'gen_licenses',
   main = 'gen_licenses.py',
@@ -61,6 +57,7 @@
 python_binary(
   name = 'replace_macros',
   main = 'replace_macros.py',
+  visibility = ['//ReleaseNotes:'],
 )
 
 genrule(
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 0758c5c..2cc8c05 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -99,21 +99,14 @@
 [[administrators]]
 === Administrators
 
-This is the Gerrit "root" identity. The capability
-link:access-control.html#capability_administrateServer['Administrate Server']
-is assigned to this predefined group on Gerrit site creation.
+This is a predefined group, created on Gerrit site initialization, that
+has the capability link:access-control.html#capability_administrateServer[
+'Administrate Server'] assigned.
 
-Users in the 'Administrators' group can perform any action under
-the Admin menu, to any group or project, without further validation
-or any other access controls.  In most installations only those
-users who have direct filesystem and database access would be
-placed into this group.
-
-Membership in the 'Administrators' group does not imply any other
-access rights.  Administrators do not automatically get code review
-approval or submit rights in projects.  This is a feature designed
-to permit administrative users to otherwise access Gerrit as any
-other normal user would, without needing two different accounts.
+It is a normal Gerrit group without magic. This means if you remove
+the 'Administrate Server' capability from it, its members are no longer
+Gerrit administrators, despite the group name. The group may also be
+renamed.
 
 
 [[non-interactive_users]]
@@ -211,8 +204,8 @@
 Permissions can be set on a single reference name to match one
 branch (e.g. `refs/heads/master`), or on a reference namespace
 (e.g. `+refs/heads/*+`) to match any branch starting with that
-prefix. So a permission with `+refs/heads/*+` will match
-`refs/heads/master` and `refs/heads/experimental`, etc.
+prefix. So a permission with `+refs/heads/*+` will match all of
+`refs/heads/master`, `refs/heads/experimental`, `refs/heads/release/1.0` etc.
 
 Reference names can also be described with a regular expression
 by prefixing the reference name with `^`.  For example
@@ -222,13 +215,21 @@
 The link:http://www.brics.dk/automaton/[dk.brics.automaton library]
 is used for evaluation of regular expression access control
 rules. See the library documentation for details on this
-particular regular expression flavor.
+particular regular expression flavor. One quirk is that the
+shortest possible pattern expansion must be a valid ref name:
+thus `^refs/heads/.*/name` will fail because `refs/heads//name`
+is not a valid reference, but `^refs/heads/.+/name` will work.
 
-References can have the current user name automatically included,
-creating dynamic access controls that change to match the currently
-logged in user.  For example to provide a personal sandbox space
-to all developers, `+refs/heads/sandbox/${username}/*+` allowing
-the user 'joe' to use 'refs/heads/sandbox/joe/foo'.
+References can have the user name or the sharded account ID of the
+current user automatically included, creating dynamic access controls
+that change to match the currently logged in user.  For example to
+provide a personal sandbox space to all developers,
+`+refs/heads/sandbox/${username}/*+` allows the user 'joe' to use
+'refs/heads/sandbox/joe/foo'. The sharded account ID can be used to
+give users access to their user branch in the `All-Users` repository,
+for example `+refs/users/${shardeduserid}+` is resolved to
+'refs/users/23/1011123' if the account ID of the current user is
+`1011123`.
 
 When evaluating a reference-level access right, Gerrit will use
 the full set of access rights to determine if the user
@@ -420,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]]
@@ -563,7 +564,6 @@
 new changes for code review, this depends on which namespace the
 permission is granted to.
 
-
 [[category_push_direct]]
 ==== Direct Push
 
@@ -611,6 +611,20 @@
 `+refs/for/refs/heads/*+` namespace.
 
 
+[[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/*`.
+
+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. Revoking
+this permission (by granting it to no groups and setting the "Exclusive" flag)
+will prevent users from uploading a patch set to a change they do not own.
+
+
 [[category_push_merge]]
 === Push Merge Commits
 
@@ -636,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.
@@ -676,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.
@@ -786,7 +800,9 @@
 === Submit (On Behalf Of)
 
 This category permits users who have also been granted the `Submit`
-permission to submit changes on behalf of another user.
+permission to submit changes on behalf of another user, by using the
+`on_behalf_of` field in link:rest-api-changes.html#submit-input[SubmitInput]
+when link:rest-api-changes.html#submit-change[submitting using the REST API].
 
 Note that this permission is named `submitAs` in the `project.config`
 file.
@@ -1066,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
@@ -1080,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.
@@ -1105,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
@@ -1133,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
@@ -1170,10 +1187,19 @@
 === Administrate Server
 
 This is in effect the owner and administrator role of the Gerrit
-instance.  Any members of a group granted this capability will be
+instance. Any members of a group granted this capability will be
 able to grant any access right to any group. They will also have all
 capabilities granted to them automatically.
 
+In most installations only those users who have direct filesystem and
+database access should be granted this capability.
+
+This capability does not imply any other access rights. Users that have
+this capability do not automatically get code review approval or submit
+rights in projects. This is a feature designed to permit administrative
+users to otherwise access Gerrit as any other normal user would,
+without needing two different accounts.
+
 
 [[capability_batchChangesLimit]]
 === Batch Changes Limit
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
index 2caf725..4b17071 100644
--- a/Documentation/asciidoc.defs
+++ b/Documentation/asciidoc.defs
@@ -35,7 +35,7 @@
   for attribute in attributes:
     asciidoc.extend(['-a', attribute])
   asciidoc.append('$SRCS')
-  newsrcs = [":doc.css"]
+  newsrcs = []
   for src in srcs:
     fn = src
     # We have two cases: regular source files and generated files.
@@ -52,15 +52,13 @@
 
     genrule(
       name = ex,
-      cmd = '$(exe :replace_macros) --suffix="%s"' % EXPN +
+      cmd = '$(exe //Documentation:replace_macros) --suffix="%s"' % EXPN +
         ' -s ' + passed_src + ' -o $OUT' +
         (' --searchbox' if searchbox else ' --no-searchbox'),
       srcs = srcs,
       out = ex,
     )
 
-    # The new AsciiDoctor requires both the css file and include files are under
-    # the same directory. Luckily Buck allows us to use :target as SRCS now.
     newsrcs.append(':%s' % ex)
 
   genrule(
@@ -74,41 +72,42 @@
 def genasciidoc(
     name,
     out,
-    docdir,
+    directory,
     srcs = [],
     attributes = [],
     backend = None,
     searchbox = True,
+    resources = True,
     visibility = []):
   SUFFIX = '_htmlonly'
 
   genasciidoc_htmlonly(
-    name = name + SUFFIX,
+    name = name + SUFFIX if resources else name,
     srcs = srcs,
     attributes = attributes,
     backend = backend,
     searchbox = searchbox,
-    out = name + SUFFIX + '.zip',
+    out = (name + SUFFIX + '.zip') if resources else (name + '.zip'),
   )
 
-  genrule(
-    name = name,
-    cmd = 'cd $TMP;' +
-      'mkdir -p %s/images;' % docdir +
-      'unzip -q $(location %s) -d %s/;'
-      % (':' + name + SUFFIX, docdir) +
-      'for s in $SRCS;do ln -s $s %s;done;' % docdir +
-      'mv %s/*.{jpg,png} %s/images;' % (docdir, docdir) +
-      'cp $(location %s) LICENSES.txt;' % ':licenses.txt' +
-      'zip -qr $OUT *',
-    srcs = glob([
-        'images/*.jpg',
-        'images/*.png',
-      ]) + [
-        ':doc.css',
-        '//gerrit-prettify:prettify.min.css',
-        '//gerrit-prettify:prettify.min.js',
-      ],
-    out = out,
-    visibility = visibility,
-  )
+  if resources:
+    genrule(
+      name = name,
+      cmd = 'cd $TMP;' +
+        'mkdir -p %s/images;' % directory +
+        'unzip -q $(location %s) -d %s/;'
+        % (':' + name + SUFFIX, directory) +
+        'for s in $SRCS;do ln -s $s %s/;done;' % directory +
+        'mv %s/*.{jpg,png} %s/images;' % (directory, directory) +
+        'cp $(location %s) LICENSES.txt;' % ':licenses.txt' +
+        'zip -qr $OUT *',
+      srcs = glob([
+          'images/*.jpg',
+          'images/*.png',
+        ]) + [
+          '//gerrit-prettify:prettify.min.css',
+          '//gerrit-prettify:prettify.min.js',
+        ],
+      out = out,
+      visibility = visibility,
+    )
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 d02e2ead..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 aafbd19..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'
+_ssh_ -p <port> <host> _gerrit index activate <INDEX>_
 --
 
 == DESCRIPTION
@@ -24,6 +25,20 @@
 == 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
new file mode 100644
index 0000000..d38c51a
--- /dev/null
+++ b/Documentation/cmd-index-changes.txt
@@ -0,0 +1,41 @@
+= gerrit index changes
+
+== NAME
+gerrit index changes - Index one or more changes.
+
+== SYNOPSIS
+[verse]
+--
+_ssh_ -p <port> <host> _gerrit index changes_ <CHANGE> [<CHANGE> ...]
+--
+
+== DESCRIPTION
+Indexes one or more changes.
+
+Changes can be specified in the link:rest-api-changes.html#change-id[same format]
+supported by the REST API.
+
+== ACCESS
+Caller must have the 'Maintain Server' capability, or be the owner of the change
+to be indexed.
+
+== SCRIPTING
+This command is intended to be used in scripts.
+
+== OPTIONS
+--CHANGE::
+    Required; changes to be indexed.
+
+== EXAMPLES
+Index changes with legacy ID numbers 1 and 2.
+
+----
+    $ ssh -p 29418 user@review.example.com gerrit index changes 1 2
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/cmd-index-start.txt b/Documentation/cmd-index-start.txt
index 1e4b24b..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'
+_ssh_ -p <port> <host> _gerrit index start_ <INDEX> [--force]
 --
 
 == DESCRIPTION
@@ -25,6 +26,23 @@
 == 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 90212fb..7af65ce 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].
@@ -78,6 +82,9 @@
 link:cmd-set-head.html[gerrit set-head]::
 	Change the HEAD reference of a project.
 
+link:cmd-set-project.html[gerrit set-project]::
+	Change a project's settings.
+
 link:cmd-set-reviewers.html[gerrit set-reviewers]::
 	Add or remove reviewers on a change.
 
@@ -126,6 +133,9 @@
 link:cmd-index-start.html[gerrit index start]::
 	Start the online indexer.
 
+link:cmd-index-changes.html[gerrit index changes]::
+	Index one or more changes.
+
 link:cmd-logging-ls-level.html[gerrit logging ls-level]::
 	List loggers and their logging level.
 
@@ -162,9 +172,6 @@
 link:cmd-set-members.html[gerrit set-members]::
 	Set group members.
 
-link:cmd-set-project.html[gerrit set-project]::
-	Change a project's settings.
-
 link:cmd-set-project-parent.html[gerrit set-project-parent]::
 	Change the project permissions are inherited from.
 
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 26b20a7..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]
@@ -58,7 +59,7 @@
 
 --type::
 	Display only projects of the specified type.  If not
-	specified, defaults to `code`. Supported types:
+	specified, defaults to `all`. Supported types:
 +
 --
 `code`:: Any project likely to contain user files.
@@ -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..5443613 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:
+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 0590337..53e2385 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>]
@@ -14,11 +14,13 @@
   [--submit | -s]
   [--abandon | --restore]
   [--rebase]
+  [--move <BRANCH>]
   [--publish]
   [--json | -j]
   [--delete]
   [--verified <N>] [--code-review <N>]
   [--label Label-Name=<N>]
+  [--tag TAG]
   {COMMIT | CHANGEID,PATCHSET}...
 --
 
@@ -65,7 +67,7 @@
 	link:rest-api-changes.html#review-input[ReviewInput] entity for the
 	format.
 	(option is mutually exclusive with --submit, --restore, --publish, --delete,
-	--abandon, --message and --rebase)
+	--abandon, --message, --rebase and --move)
 
 --notify::
 -n::
@@ -87,7 +89,7 @@
 --abandon::
 	Abandon the specified change(s).
 	(option is mutually exclusive with --submit, --restore, --publish, --delete,
-	--rebase and --json)
+	--rebase, --move and --json)
 
 --restore::
 	Restore the specified abandoned change(s).
@@ -97,6 +99,10 @@
 	Rebase the specified change(s).
 	(option is mutually exclusive with --abandon, --submit, --delete and --json)
 
+--move::
+	Move the specified change(s).
+	(option is mutually exclusive with --json and --abandon)
+
 --submit::
 -s::
 	Submit the specified patch set(s) for merging.
@@ -134,6 +140,15 @@
 	permitted for the user, or the vote is on an outdated or closed patch set,
 	return an error instead of silently discarding the vote.
 
+--tag::
+-t::
+  Apply a 'TAG' to the change message, votes, and inline comments. The 'TAG'
+  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 that to apply different tags on on different votes/comments, multiple
+  invocations of the SSH command are required.
+
 == ACCESS
 Any user who has configured an SSH key.
 
@@ -143,37 +158,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..62d6e92 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>]
@@ -25,7 +26,7 @@
 previous settings are kept intact.
 
 == ACCESS
-Caller must be a member of the privileged 'Administrators' group.
+Caller must be an owner of the given project.
 
 == SCRIPTING
 This command is intended to be used in scripts.
@@ -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 79f7651..3d53456 100644
--- a/Documentation/cmd-set-reviewers.txt
+++ b/Documentation/cmd-set-reviewers.txt
@@ -4,23 +4,23 @@
 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> ...]
   [--]
-  {COMMIT | CHANGE-ID}...
+  {CHANGE-ID}...
 --
 
 == DESCRIPTION
 Adds or removes reviewers to the specified change, sending email
 notifications when changes are made.
 
-Changes should be specified as complete or abbreviated Change-Ids
-such as 'Iac6b2ac2'.  They may also be specified by numeric change
-identifiers, such as '8242' or by complete or abbreviated commit
-SHA-1s.
+Changes can be specified in the
+link:rest-api-changes.html#change-id[same format] supported by the REST
+API.
 
 == OPTIONS
 
@@ -55,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 81eb174..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,7 +42,7 @@
 
 Start::
 	Time (local to the server) that this connection started.
-	Only valid for MINA backend.
+	Only shown for MINA backend.
 
 Idle::
 	Time since the last data transfer on this connection.
@@ -48,7 +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 valid for MINA backend.
+	Only shown for MINA backend.
 
 User::
 	The username of the account that is authenticated on this
@@ -62,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 dcdbb07..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
@@ -26,13 +27,28 @@
 == SCRIPTING
 This command is intended to be used in scripts.
 
+== OPTIONS
+--subscribe|-s::
+	Type of the event to subscribe to.  Multiple --subscribe options
+	may be specified to subscribe to multiple events. When this option
+	is provided, only subscribed events are emitted and all other
+	events are ignored. When this option is omitted, all events are
+	emitted.
+
 == 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*,
@@ -166,23 +182,6 @@
 eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
 created.
 
-=== Merge Failed
-
-Sent when a change has failed to be merged into the git repository.
-
-type:: "merge-failed"
-
-change:: link:json.html#change[change attribute]
-
-patchSet:: link:json.html#patchSet[patchSet attribute]
-
-submitter:: link:json.html#account[account attribute]
-
-reason:: Reason that the merge failed.
-
-eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
-created.
-
 === Patchset Created
 
 Sent when a new change has been uploaded, or a new patch set has been uploaded
@@ -232,6 +231,27 @@
 eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
 created.
 
+=== Reviewer Deleted
+
+Sent when a reviewer (with a vote) is removed from a change.
+
+type:: "reviewer-deleted"
+
+change:: link:json.html#change[change attribute]
+
+patchSet:: link:json.html#patchSet[patchSet attribute]
+
+reviewer:: link:json.html#account[account attribute]
+
+author:: link:json.html#account[account attribute]
+
+approvals:: All link:json.html#approval[approval attributes] removed.
+
+comment:: Review comment cover message.
+
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
 === Topic Changed
 
 Sent when the topic of a change has been changed.
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-auto-site-initialization.txt b/Documentation/config-auto-site-initialization.txt
index abd8d8f..acd03c9 100644
--- a/Documentation/config-auto-site-initialization.txt
+++ b/Documentation/config-auto-site-initialization.txt
@@ -8,53 +8,45 @@
 plugins will be installed when Gerrit is deployed in a servlet container
 and the location of the Gerrit distribution can be determined at
 runtime. It is also possible to install only a subset of packaged
-plugins or not install any plugin.
+plugins or not install any plugins.
 
 This feature may be useful for such setups where Gerrit administrators
 don't have direct access to the database and the file system of the
 server where Gerrit should be deployed and, therefore, cannot perform
 the init from their local machine prior to deploying Gerrit on such a
 server. It may also make deployment and testing in a local servlet
-container faster to setup as the init step could be skipped.
+container faster to set up as the init step could be skipped.
 
 == Gerrit Configuration
 
 The site initialization will be performed only if the `gerrit.init`
-system property exists (the value of the property is not used, only the
-existence of the property matters).
+system property exists. The value of the property is not used; only the
+existence of the property matters.
 
 If the `gerrit.site_path` system property is defined then the init is
 run for that site. The database connectivity, in that case, is defined
 in the `etc/gerrit.config`.
 
-If `gerrit.site_path` is not defined then Gerrit will try to find an
-existing site by looking into the `system_config` table in the database
-defined via the `jdbc/ReviewDb` JNDI property. If the `system_config`
-table exists then the `site_path` from that table is used for the
-initialization. The database connectivity is defined by the
-`jdbc/ReviewDb` JNDI property.
-
-Finally, if neither the `gerrit.site_path` property nor the
-`system_config` table exists, the `gerrit.init_path` system property,
-if defined, will be used to determine the site path. The database
-connectivity, also for this case, is defined by the `jdbc/ReviewDb`
-JNDI property.
+If `gerrit.site_path` is not defined then Gerrit will try to find the
+`gerrit.init_path` system property. If defined this property will be
+used to determine the site path. The database connectivity, also for
+this case, is defined by the `jdbc/ReviewDb` JNDI property.
 
 [WARNING]
 Defining the `jdbc/ReviewDb` JNDI property for an H2 database under the
 path defined by either `gerrit.site_path` or `gerrit.init_path` will
 cause an incomplete auto initialization and Gerrit will fail to start.
-Opening a connection to such database will create a subfolder under the
+Opening a connection to such a database will create a subfolder under the
 site path folder (in order to create the H2 database) and Gerrit will
-not any more consider that site path to be new and, because of that,
+no longer consider that site path to be new and, because of that,
 skip some required initialization steps (for example, Lucene index
 creation). In order to auto initialize Gerrit with an embedded H2
 database use the `gerrit.site_path` to define the location of the review
 site and don't define a JNDI resource with a URL under that path.
 
-If the 'gerrit.install_plugins' property is not defined then all packaged
+If the `gerrit.install_plugins` property is not defined then all packaged
 plugins will be installed. If it is defined then it is parsed as a
-comma separated list of plugin names to install. If the value is an
+comma-separated list of plugin names to install. If the value is an
 empty string then no plugin will be installed.
 
 === Example 1
@@ -70,18 +62,6 @@
 
 === Example 2
 
-Prepare Tomcat so that an existing site with the path defined in the
-`system_config` table is initialized (upgraded) on Gerrit startup. The
-assumption is that the `jdbc/ReviewDb` JNDI property is defined in
-Tomcat:
-
-----
-  $ export CATALINA_OPTS='-Dgerrit.init'
-  $ catalina.sh start
-----
-
-=== Example 3
-
 Assuming the database schema doesn't exist in the database defined
 via the `jdbc/ReviewDb` JNDI property, initialize a new site using that
 database and a given path:
diff --git a/Documentation/config-cla.txt b/Documentation/config-cla.txt
index 4c8d04a..c07a24f 100644
--- a/Documentation/config-cla.txt
+++ b/Documentation/config-cla.txt
@@ -12,27 +12,27 @@
 
 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.
@@ -41,10 +41,10 @@
 `autoVerify` and `accepted` variables in the groups file.
 
 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 bc64d7f..e8d504a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -19,6 +19,29 @@
   directory = /var/cache/gerrit2
 ----
 
+[[accountPatchReviewDb]]
+=== Section accountPatchReviewDb
+
+[[accountPatchReviewDb.url]]accountPatchReviewDb.url::
++
+The url of accountPatchReviewDb. Supported types are `H2`, `POSTGRESQL`, and
+`MYSQL`. Drop the driver jar in the lib folder of the site path if the Jdbc
+driver of the corresponding Database is not yet in the class path.
++
+Default is to create H2 database in the db folder of the site path.
++
+Changing this parameter requires to migrate database using the
+link:pgm-MigrateAccountPatchReviewDb.html[MigrateAccountPatchReviewDb] program.
+Migration cannot be done while the server is running.
++
+Also note that the db_name has to be a new db and not reusing gerrit's own review database,
+otherwise gerrit's init will remove the table.
+
+----
+[accountPatchReviewDb]
+  url = jdbc:postgresql://<host>:<port>/<db_name>?user=<user>&password=<password>
+----
+
 [[accounts]]
 === Section accounts
 
@@ -145,7 +168,7 @@
 The configured <<ldap.username,ldap.username>> identity is not used to obtain
 account information.
 +
-* OAUTH
+* `OAUTH`
 +
 OAuth is a protocol that lets external apps request authorization to private
 details in a user's account without getting their password. This is
@@ -383,6 +406,12 @@
 +
 If not set, HTTP request's path is used.
 
+[[auth.cookieDomain]]auth.cookieDomain::
++
+Sets "domain" attribute of the authentication cookie.
++
+If not set, HTTP request's domain is used.
+
 [[auth.cookieSecure]]auth.cookieSecure::
 +
 Sets "secure" flag of the authentication cookie.  If true, cookies
@@ -437,20 +466,73 @@
 [[auth.gitBasicAuth]]auth.gitBasicAuth::
 +
 If true then Git over HTTP and HTTP/S traffic is authenticated using
-standard BasicAuth and the credentials are validated against the randomly
-generated HTTP password or against LDAP when it is configured as Gerrit
-Web UI authentication method.
+standard BasicAuth. Depending on the configured `auth.type`, credentials
+are validated against the randomly generated HTTP password, against LDAP
+(`auth.type = LDAP`) or against an OAuth 2 provider (`auth.type = OAUTH`).
 +
 This parameter affects git over HTTP traffic and access to the REST
 API. If set to false then Gerrit will authenticate through DIGEST
 authentication and the randomly generated HTTP password in the Gerrit
 database.
 +
-When `auth.type` is `LDAP`, service users that only exist in the Gerrit
-database are still authenticated by their HTTP passwords.
+When `auth.type` is `LDAP`, users should authenticate using their LDAP passwords.
+However, if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP`,
+the randomly generated HTTP password is used exclusively. In the other hand,
+if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP_LDAP`,
+the password in the request is first checked against the HTTP password and, if
+it does not match, it is then validated against the LDAP password.
+Service users that only exist in the Gerrit database are authenticated by their
+HTTP passwords.
++
+When `auth.type` is `OAUTH`, Git clients may send OAuth 2 access tokens
+instead of passwords in the Basic authentication header. Note that provider
+specific plugins must be installed to facilitate this authentication scheme.
+If multiple OAuth 2 provider plugins are installed one of them must be
+selected as default with the `auth.gitOAuthProvider` option.
 +
 By default this is set to false.
 
+[[auth.gitBasicAuthPolicy]]auth.gitBasicAuthPolicy::
++
+When `auth.type` is `LDAP` and BasicAuth (i.e., link:#auth.gitBasicAuth[`auth.gitBasicAuth`]
+is set to true), it allows using either the generated HTTP password, the LDAP
+password or both to authenticate Git over HTTP and REST API requests. The
+supported values are:
++
+*`HTTP`
++
+Only the randomly generated HTTP password is accepted when doing Git over HTTP
+and REST API requests.
++
+*`LDAP`
++
+Only the `LDAP` password is allowed when doing Git over HTTP and REST API
+requests.
++
+*`HTTP_LDAP`
++
+The password in the request is first checked against the HTTP password and, if
+it does not match, it is then validated against the `LDAP` password.
++
+By default this is set to `LDAP` when link:#auth.type[`auth.type`] is `LDAP`.
+Otherwise, the default value is `HTTP`.
+
+[[auth.gitOAuthProvider]]auth.gitOAuthProvider::
++
+Selects the OAuth 2 provider to authenticate git over HTTP traffic with.
++
+In general there is no way to determine from an access token alone, which
+OAuth 2 provider to address to verify that token, and the BasicAuth
+scheme does not support amending such details. If multiple OAuth provider
+plugins in a system offer support for git over HTTP authentication site
+administrators must configure, which one to use as default provider.
+In case the provider cannot be determined from a request the access token
+will be sent to the default provider for verification.
++
+The value of this parameter must be the identifier of an OAuth 2 provider
+in the form `plugin-name:provider-name`. Consult the respective plugin
+documentation for details.
+
 [[auth.userNameToLowerCase]]auth.userNameToLowerCase::
 +
 If set the username that is received to authenticate a git operation
@@ -502,10 +584,40 @@
 expensive to compute information across restarts.  If the location
 does not exist, Gerrit will try to create it.
 +
+Technically, cached entities are persisted as a set of H2 databases
+inside this directory.
++
 If not absolute, the path is resolved relative to `$site_path`.
 +
 Default is unset, no disk cache.
 
+[[cache.h2CacheSize]]cache.h2CacheSize::
++
+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.
++
+See <<database.h2.cachesize,database.h2.cachesize>> for a detailed discussion.
++
+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
@@ -834,6 +946,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
 
@@ -867,6 +1006,12 @@
 +
 Default is 30 seconds.
 
+[[change.allowBlame]]change.allowBlame::
++
+Allow blame on side by side diff. If set to false, blame cannot be used.
++
+Default is true.
+
 [[change.allowDrafts]]change.allowDrafts::
 +
 Allow drafts workflow. If set to false, drafts cannot be created,
@@ -874,6 +1019,21 @@
 +
 Default is true.
 
+[[change.cacheAutomerge]]change.cacheAutomerge::
++
+When reviewing diff commits, the left-hand side shows the output of the
+result of JGit's automatic merge algorithm. This option controls whether
+this output is cached in the change repository, or if only the diff is
+cached in the persistent `diff` cache.
++
+If true, automerge results are stored in the repository under
+`refs/cache-automerge/*`; the results of diffing the change against its
+automerge base are stored in the diff cache. If false, no extra data is
+stored in the repository, only the diff cache. This can result in slight
+performance improvements by reducing the number of refs in the repo.
++
+Default is true.
+
 [[change.submitLabel]]change.submitLabel::
 +
 Label name for the submit button.
@@ -907,7 +1067,7 @@
 Default is "Submit all ${topicSize} changes of the same topic (${submitSize}
 changes including ancestors and other changes related by topic)".
 
-[[change.submitWholeTopic]]change.submitWholeTopic (*Experimental*)::
+[[change.submitWholeTopic]]change.submitWholeTopic::
 +
 Determines if the submit button submits the whole topic instead of
 just the current change.
@@ -924,7 +1084,7 @@
 
 [[change.submitTopicTooltip]]change.submitTopicTooltip::
 +
-If `change.submitWholeTopic` is configuerd to true and a change has a
+If `change.submitWholeTopic` is configured to true and a change has a
 topic, this configuration determines the tooltip for the submit button
 instead of `change.submitTooltip`. The variable `${topicSize}` is available
 for the number of changes in the same topic to be submitted. The number of
@@ -1038,25 +1198,6 @@
 +
 Default is 300 seconds (5 minutes).
 
-[[changeMerge.threadPoolSize]]changeMerge.threadPoolSize::
-+
-_Deprecated:_ Formerly used to control thread pool size for background
-mergeability checks. These checks were moved to the indexing threadpool,
-so this value is now used for
-link:#index.batchThreads[index.batchThreads], only if that value is not
-provided.
-+
-This option may be removed in a future version.
-
-[[changeMerge.interactiveThreadPoolSize]]changeMerge.interactiveThreadPoolSize::
-+
-_Deprecated:_ Formerly used to control thread pool size for interactive
-mergeability checks. These checks were moved to the indexing threadpool,
-so this value is now used for link:#index.threads[index.threads], only
-if that value is not provided.
-+
-This option may be removed in a future version.
-
 [[commentlink]]
 === Section commentlink
 
@@ -1199,6 +1340,12 @@
 called with the '--slave' switch, enabling slave mode. If no value is
 set (or any other value), Gerrit defaults to master mode.
 
+[[container.startupTimeout]]container.startupTimeout::
++
+The maximum time (in seconds) to wait for a gerrit.sh start command
+to run a new Gerrit daemon successfully.  If not set, defaults to
+90 seconds.
+
 [[container.user]]container.user::
 +
 Login name (or UID) of the operating system user the Gerrit JVM
@@ -1325,6 +1472,33 @@
 +
 Default is true.
 
+[[core.repositoryCacheCleanupDelay]]core.repositoryCacheCleanupDelay::
++
+Delay between each periodic cleanup of expired repositories.
++
+Values can be specified using standard time unit abbreviations (`ms`, `sec`,
+`min`, etc.).
++
+Set it to 0 in order to switch off cache expiration. If cache expiration is
+switched off, the JVM can still evict cache entries when it is running low
+on available heap memory.
++
+Set it to -1 to automatically derive cleanup delay from
+`core.repositoryCacheExpireAfter` (lowest value between 1/10 of
+`core.repositoryCacheExpireAfter` and 10 minutes).
++
+Default is -1.
+
+[[core.repositoryCacheExpireAfter]]core.repositoryCacheExpireAfter::
++
+Time an unused repository should expire and be evicted from the repository
+cache.
++
+Values can be specified using standard time unit abbreviations (`ms`, `sec`,
+`min`, etc.).
++
+Default is 1 hour.
+
 [[database]]
 === Section database
 
@@ -1435,7 +1609,8 @@
 httpd and sshd threads as some request processing code paths may
 need multiple connections.
 +
-Default is 8.
+Default is <<sshd.threads, sshd.threads>>
+ + <<httpd.maxThreads, httpd.maxThreads>> + 2.
 +
 This setting only applies if
 <<database.connectionPool,database.connectionPool>> is true.
@@ -1453,7 +1628,7 @@
 Maximum number of connections to keep idle in the pool.  If there
 are more idle connections, connections will be closed instead of
 being returned back to the pool.
-Default is 4.
+Default is min(<<database.poolLimit, database.poolLimit>>, 16).
 +
 This setting only applies if
 <<database.connectionPool,database.connectionPool>> is true.
@@ -1488,6 +1663,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
 
@@ -1720,8 +1931,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
@@ -1816,6 +2027,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
 
@@ -1830,29 +2048,31 @@
 be called by Gerrit Code Review when the URL `/gitweb` is accessed.
 Project level access controls are enforced prior to calling the CGI.
 +
-Defaults to `/usr/lib/cgi-bin/gitweb.cgi` if gitweb.url is not set.
+Defaults to `/usr/lib/cgi-bin/gitweb.cgi` if `gitweb.url` is not set.
 
 [[gitweb.url]]gitweb.url::
 +
 Optional URL of an affiliated gitweb service.  Defines the
 web location where a `gitweb.cgi` is installed to browse
-gerrit.basePath and the repositories it contains.
+`gerrit.basePath` and the repositories it contains.
 +
 Gerrit appends any necessary query arguments onto the end of this URL.
-For example, "?p=$project.git;h=$commit".
+For example, `?p=$project.git;h=$commit`.
 
 [[gitweb.type]]gitweb.type::
 +
 Optional type of affiliated gitweb service. This allows using
-alternatives to gitweb, such as cgit. If set to disabled there
-is no gitweb hyperlinking support.
+alternatives to gitweb, such as cgit.
 +
 Valid values are `gitweb`, `cgit`, `disabled` or `custom`.
++
+If not set, or set to `disabled`, there is no gitweb hyperlinking
+support.
 
 [[gitweb.revision]]gitweb.revision::
 +
 Optional pattern to use for constructing the gitweb URL when pointing
-at a specific commit when `custom` is used above.
+at a specific commit when `gitweb.type` is set to `custom`.
 +
 Valid replacements are `${project}` for the project name in Gerrit
 and `${commit}` for the SHA1 hash for the commit.
@@ -1860,14 +2080,14 @@
 [[gitweb.project]]gitweb.project::
 +
 Optional pattern to use for constructing the gitweb URL when pointing
-at a specific project when `custom` is used above.
+at a specific project when `gitweb.type` is set to `custom`.
 +
 Valid replacements are `${project}` for the project name in Gerrit.
 
 [[gitweb.branch]]gitweb.branch::
 +
 Optional pattern to use for constructing the gitweb URL when pointing
-at a specific branch when `custom` is used above.
+at a specific branch when `gitweb.type` is set to `custom`.
 +
 Valid replacements are `${project}` for the project name in Gerrit
 and `${branch}` for the name of the branch.
@@ -1875,8 +2095,8 @@
 [[gitweb.roottree]]gitweb.roottree::
 +
 Optional pattern to use for constructing the gitweb URL when pointing
-at the contents of the root tree in a specific commit when `custom` is
-used above.
+at the contents of the root tree in a specific commit when `gitweb.type`
+is set to `custom`.
 +
 Valid replacements are `${project}` for the project name in Gerrit
 and `${commit}` for the SHA1 hash for the commit.
@@ -1884,8 +2104,8 @@
 [[gitweb.file]]gitweb.file::
 +
 Optional pattern to use for constructing the gitweb URL when pointing
-at the contents of a file in a specific commit when `custom` is used
-above.
+at the contents of a file in a specific commit when `gitweb.type` is
+set to `custom`.
 +
 Valid replacements are `${project}` for the project name in Gerrit,
 `${file}` for the file name and `${commit}` for the SHA1 hash for
@@ -1894,8 +2114,8 @@
 [[gitweb.filehistory]]gitweb.filehistory::
 +
 Optional pattern to use for constructing the gitweb URL when pointing
-at the history of a file in a specific branch when `custom` is used
-above.
+at the history of a file in a specific branch when when `gitweb.type`
+is set to `custom`.
 +
 Valid replacements are `${project}` for the project name in Gerrit,
 `${file}` for the file name and `${branch}` for the name of the
@@ -1906,7 +2126,7 @@
 Optional setting for modifying the link name presented to the user
 in the Gerrit web-UI.
 +
-Default linkname for custom type is "gitweb".
+The default linkname for custom type is `gitweb`.
 
 [[gitweb.pathSeparator]]gitweb.pathSeparator::
 +
@@ -1921,9 +2141,9 @@
 allow using an alternative path separator character. In Gitblit, this can be
 configured through the property link:http://gitblit.com/properties.html[web.forwardSlashCharacter].
 In Gerrit, the alternative path separator can be configured correspondingly
-using the property 'gitweb.pathSeparator'.
+using the property `gitweb.pathSeparator`.
 +
-Valid values are the characters '*', '(' and ')'.
+Valid values are the characters `*`, `(` and `)`.
 
 [[gitweb.urlEncode]]gitweb.urlEncode::
 +
@@ -1931,22 +2151,12 @@
 +
 Gerrit composes the viewer URL using information about the project, branch, file
 or commit of the target object to be displayed. Typically viewers such as CGit
-and gitweb do need those parts to be encoded, including the '/' in project's name,
+and gitweb do need those parts to be encoded, including the `/` in project's name,
 for being correctly parsed.
 However other viewers could instead require an unencoded URL (e.g. GitHub web
-based viewer)
+based viewer).
 +
-Valid values are "true" and "false," default is "true."
-
-[[gitweb.linkDrafts]]gitweb.linkDrafts::
-+
-Whether or not Gerrit should provide links to gitweb on draft patch sets.
-+
-By default, Gerrit will show links to gitweb on all patch sets. If gitweb
-only allows publicly viewable references, set this to false to remove
-the links to draft patch sets from the change review screen.
-+
-Valid values are "true" and "false," default is "true".
+Valid values are `true` and `false`. The default is `true`.
 
 [[groups]]
 === Section groups
@@ -1958,90 +2168,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.mergeFailedHook]]hooks.mergeFailedHook::
-+
-Optional filename for the merge failed hook, if not specified then
-`merge-failed` 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
 
@@ -2151,6 +2277,14 @@
 +
 By default, true.
 
+[[httpd.inheritChannel]]httpd.inheritChannel::
++
+If true, permits the daemon to inherit its server socket channel
+from fd0/1(stdin/stdout). When set to true, the server can be socket
+activated via systemd or xinetd.
++
+By default, false.
+
 [[httpd.requestHeaderSize]]httpd.requestHeaderSize::
 +
 Size, in bytes, of the buffer used to parse the HTTP headers of an
@@ -2309,6 +2443,23 @@
 	filterClass = org.anyorg.MySecureFilter
 ----
 
+[[httpd.idleTimeout]]httpd.idleTimeout::
++
+Maximum idle time for a connection, which roughly translates to the
+TCP socket `SO_TIMEOUT`.
++
+This value is interpreted as the maximum time between some progress
+being made on the connection. So if a single byte is read or written,
+then the timeout is reset.
++
+The max idle time is applied:
++
+* When waiting for a new message to be received on a connection
+* When waiting for a new message to be sent on a connection
+
++
+By default, 30 seconds.
+
 [[httpd.robotsFile]]httpd.robotsFile::
 +
 Location of an external robots.txt file to be used instead of the one
@@ -2353,9 +2504,8 @@
 it to 0 disables the dedicated thread pool and indexing will be done in the same
 thread as the operation.
 +
-Defaults to 0 if not set, or set to a negative value (unless
-link:#changeMerge.interactiveThreadPoolSize[changeMerge.interactiveThreadPoolSize]
-is iset).
+If not set or set to a negative value, defaults to 1 plus half of the number of
+logical CPUs as returned by the JVM.
 
 [[index.batchThreads]]index.batchThreads::
 +
@@ -2363,8 +2513,7 @@
 online schema upgrades.
 +
 If not set or set to a negative value, defaults to the number of logical
-CPUs as returned by the JVM (unless
-link:#changeMerge.threadPoolSize[changeMerge.threadPoolSize] is set).
+CPUs as returned by the JVM.
 
 [[index.onlineUpgrade]]index.onlineUpgrade::
 +
@@ -2455,6 +2604,46 @@
 +
 Defaults to 300000 ms (5 minutes).
 
+
+[[index.name.maxMergeCount]]index.name.maxMergeCount::
++
+Determines the max number of simultaneous merges that are allowed. If a merge
+is necessary yet we already have this many threads running, the incoming thread
+(that is calling add/updateDocument) will block until a merge thread has
+completed.  Note that Lucene will only run the smallest maxThreadCount merges
+at a time. See the
+link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#setDefaultMaxMergesAndThreads(boolean)[
+Lucene documentation] for further details.
++
+Defaults to -1 for (auto detection).
+
+
+[[index.name.maxThreadCount]]index.name.maxThreadCount::
++
+Determines the max number of simultaneous Lucene merge threads that should be running at
+once. This must be less than or equal to maxMergeCount. See the
+link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#setDefaultMaxMergesAndThreads(boolean)[
+Lucene documentation] for further details.
++
+For further details on Lucene index configuration (auto detection) which
+affects maxThreadCount and maxMergeCount settings.
+See the
+link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#AUTO_DETECT_MERGES_AND_THREADS[
+Lucene documentation]
++
+Defaults to -1 for (auto detection).
+
+[[index.name.enableAutoIOThrottle]]index.name.enableAutoIOThrottle::
++
+Allows the control of whether automatic IO throttling is enabled and used by
+default in the lucene merge queue.  Automatic dynamic IO throttling, which when
+on is used to adaptively rate limit writes bytes/sec to the minimal rate necessary
+so merges do not fall behind. See the
+link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#enableAutoIOThrottle()[
+Lucene documentation] for further details.
++
+Defaults to true (throttling enabled).
+
 Sample Lucene index configuration:
 ----
 [index]
@@ -2463,10 +2652,17 @@
 [index "changes_open"]
   ramBufferSize = 60 m
   maxBufferedDocs = 3000
+  maxThreadCount = 5
+  maxMergeCount = 50
+
 
 [index "changes_closed"]
   ramBufferSize = 20 m
   maxBufferedDocs = 500
+  maxThreadCount = 10
+  maxMergeCount = 100
+  enableIOThrottle = false
+
 ----
 
 [[ldap]]
@@ -2823,6 +3019,19 @@
   javaOptions = -Dcom.sun.jndi.ldap.connect.pool.timeout=300000
 ----
 
+[[lfs]]
+=== Section lfs
+
+[[lfs.plugin]]lfs.plugin::
++
+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.
+
 [[log]]
 === Section log
 
@@ -2867,6 +3076,31 @@
   safe = true
 ----
 
+[[oauth]]
+=== Section oauth
+
+OAuth integration is only enabled if `auth.type` is set to `OAUTH`. See
+link:#auth.type[above] for a detailed description of the `auth.type` settings
+and their implications.
+
+By default, contact information, like the full name and email address,
+is retrieved from the selected OAuth provider when a user account is created,
+or when a user requests to reload that information in the settings UI. If
+that is not supported by the OAuth provider, users can be allowed to edit
+their contact information manually.
+
+[[oauth.allowEditFullName]]oauth.allowEditFullName::
++
+If true, the full name can be edited in the contact information.
++
+Default is false.
+
+[[oauth.allowRegisterNewEmail]]oauth.allowRegisterNewEmail::
++
+If true, additional email addresses can be registered in the contact
+information.
++
+Default is false.
 
 [[pack]]
 === Section pack
@@ -3117,10 +3351,22 @@
   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/*`.
 
+[[repository.name.basePath]]repository.<name>.basePath::
++
+Alternate to <<gerrit.basePath,gerrit.basePath>>. The repository will be created
+and used from this location instead: ${alternateBasePath}/${projectName}.git.
++
+If configuring the basePath for an existing project in gerrit, make sure to stop
+gerrit, move the repository in the alternate basePath, configure basePath for
+this repository and then start Gerrit.
++
+Path must be absolute.
+
 [[repository.name.defaultSubmitType]]repository.<name>.defaultSubmitType::
 +
 The default submit type for newly created projects. Supported values
@@ -3171,6 +3417,28 @@
 +
 Default is 10x reductionLimit (1,000,000).
 
+[[rules.maxSourceBytes]]rules.maxSourceBytes::
++
+Maximum input size (in bytes) of a Prolog rules.pl file.  Larger
+source files may need a larger rules.compileReductionLimit.  Consider
+using link:pgm-rulec.html[rulec] to precompile larger rule files.
++
+A size of 0 bytes disables rules, same as rules.enable = false.
++
+Common unit suffixes of 'k', 'm', or 'g' are supported.
++
+Default is 128 KiB.
+
+[[rules.maxPrologDatabaseSize]]rules.maxPrologDatabaseSize::
++
+Number of predicate clauses allowed to be defined in the Prolog
+database by project rules.  Very complex rules may need more than the
+default 256 limit, but cost more memory and may need more time to
+evaluate.  Consider using link:pgm-rulec.html[rulec] to precompile
+larger rule files.
++
+Default is 256.
+
 [[execution]]
 === Section execution
 
@@ -3375,9 +3643,9 @@
 +
 Starting from version 0.9.0 Apache SSHD project added support for NIO2
 IoSession. To use the new NIO2 session the `backend` option must be set
-to `NIO2`.
+to `NIO2`. Otherwise, this option must be set to `MINA`.
 +
-By default, `MINA`.
+By default, `NIO2`.
 
 [[sshd.listenAddress]]sshd.listenAddress::
 +
@@ -3436,7 +3704,7 @@
 If additional requests are received while all threads are busy they
 are queued and serviced in a first-come-first-served order.
 +
-By default, 1.5x the number of CPUs available to the JVM.
+By default, 2x the number of CPUs available to the JVM.
 
 [[sshd.batchThreads]]sshd.batchThreads::
 +
@@ -3500,8 +3768,9 @@
 [[sshd.idleTimeout]]sshd.idleTimeout::
 +
 Time in seconds after which the server automatically terminates idle
-connections (or 0 to disable closing of idle connections).  Values
-should use common unit suffixes to express their setting:
+connections (or 0 to disable closing of idle connections) not waiting for
+any server operation to complete.
+Values should use common unit suffixes to express their setting:
 +
 * s, sec, second, seconds
 * m, min, minute, minutes
@@ -3511,6 +3780,21 @@
 +
 By default, 0.
 
+[[sshd.waitTimeout]]sshd.waitTimeout::
++
+Time in seconds after which the server automatically terminates
+connections waiting for a server operation to complete, like for instance
+cloning a very large repo with lots of refs.
+Values should use common unit suffixes to express their setting:
++
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
+* d, day, days
+
++
+By default, 30s.
+
 [[sshd.maxConnectionsPerUser]]sshd.maxConnectionsPerUser::
 +
 Maximum number of concurrent SSH sessions that a user account
@@ -3547,6 +3831,40 @@
 +
 By default, all supported MACs are available.
 
+[[sshd.kex]]sshd.kex::
++
+--
+Available key exchange algorithms. To permit multiple algorithms,
+specify multiple `sshd.kex` keys in the configuration file, one key
+exchange algorithm per key.  Key exchange algorithm names starting
+with `+` are enabled in addition to the default key exchange
+algorithms, key exchange algorithm names starting with `-` are
+removed from the default key exchange algorithms.
+
+In the following example configuration, support for the 1024-bit
+`diffie-hellman-group1-sha1` key exchange is disabled while leaving
+all of the other default algorithms enabled:
+
+----
+[sshd]
+  kex = -diffie-hellman-group1-sha1
+----
+
+Supported key exchange algorithms:
+
+* `ecdh-sha2-nistp521`
+* `ecdh-sha2-nistp384`
+* `ecdh-sha2-nistp256`
+* `diffie-hellman-group-exchange-sha256`
+* `diffie-hellman-group-exchange-sha1`
+* `diffie-hellman-group14-sha1`
+* `diffie-hellman-group1-sha1`
+
+By default, all supported key exchange algorithms are available.
+Without Bouncy Castle, `diffie-hellman-group1-sha1` is the only
+available algorithm.
+--
+
 [[sshd.kerberosKeytab]]sshd.kerberosKeytab::
 +
 Enable kerberos authentication for SSH connections.  To permit
@@ -3606,35 +3924,12 @@
 [[suggest]]
 === Section suggest
 
-[[suggest.accounts]]suggest.accounts::
-+
-If `true`, visible user accounts (according to the value of
-`accounts.visibility`) will be offered as completion suggestions
-when adding a reviewer to a change, or a user to a group.
-+
-If `false`, account suggestion is disabled.
-+
-Older configurations may also have one of the `accounts.visibility`
-values for this field, including `OFF` as a synonym for `NONE`. If
-`accounts.visibility` is also set, that value overrides this one;
-otherwise, this value applies to both `suggest.accounts` and
-`accounts.visibility`.
-+
-New configurations should prefer the boolean value for this field
-and an enum value for `accounts.visibility`.
-
 [[suggest.maxSuggestedReviewers]]suggest.maxSuggestedReviewers::
 +
 The maximum numbers of reviewers suggested.
 +
 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
@@ -3642,18 +3937,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
@@ -3894,14 +4177,25 @@
 [[submodule]]
 === Section submodule
 
-[[submodule.verbosesuperprojectupdate]]submodule.verboseSuperprojectUpdate
+[[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::
++
+This allows to enable the superproject subscription mechanism.
++
+By default this is true.
 
 [[user]]
 === Section user
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 3068cc5..a71595f 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -1,190 +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.
-
-  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> ...]
-====
-
-=== 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>
-====
-
-=== merge-failed
-
-Called whenever a change has failed to merge.
-
-====
-  merge-failed --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1> --reason <reason>
-====
-
-=== 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>
-====
-
-=== 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 a40c5f3..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:
 
@@ -217,6 +217,19 @@
 The label is purely informational and values are not considered when
 determining whether a change is submittable.
 
+* `PatchSetLock`
++
+The `PatchSetLock` function provides a locking mechanism for patch
+sets.  This function's values are not considered when determining
+whether a change is submittable. When set, no new patchsets can be
+created and rebase and abandon are blocked.
++
+This function is designed to allow overlapping locks, so several lock
+accounts could lock the same change.
++
+Allowed range of values are 0 (Patch Set Unlocked) to 1 (Patch Set
+Locked).
+
 
 [[label_copyMinScore]]
 === `label.Label-Name.copyMinScore`
@@ -233,6 +246,23 @@
 sticky approvals, reducing turn-around for trivial cleanups prior to
 submitting a change. Defaults to false.
 
+[[label_copyAllScoresOnMergeCommitFirstParentUpdate]]
+=== `label.Label-Name.copyAllScoresOnMergeCommitFirstParentUpdate`
+
+This policy is useful if you don't want to trigger CI or human
+verification again if your target branch moved on but the feature
+branch being merged into the target branch did not change. It only
+applies if the patch set is a merge commit.
+
+If true, all scores for the label are copied forward when a new
+patch set is uploaded that is a new merge commit which only
+differs from the previous patch set in its first parent, or has
+identical parents. The first parent would be the parent of the merge
+commit that is part of the change's target branch, whereas the other
+parent(s) refer to the feature branch(es) to be merged.
+
+Defaults to false.
+
 [[label_copyAllScoresOnTrivialRebase]]
 === `label.Label-Name.copyAllScoresOnTrivialRebase`
 
@@ -240,10 +270,13 @@
 set is uploaded that is a trivial rebase. A new patch set is considered
 as trivial rebase if the commit message is the same as in the previous
 patch set and if it has the same code delta as the previous patch set.
-This is the case if the change was rebased onto a different parent.
+This is the case if the change was rebased onto a different parent, or
+if the parent did not change at all.
+
 This can be used to enable sticky approvals, reducing turn-around for
 trivial rebases prior to submitting a change.
 For the pre-installed Code-Review label this is enabled by default.
+
 Defaults to false.
 
 [[label_copyAllScoresIfNoCodeChange]]
@@ -257,6 +290,7 @@
 if only the commit message is changed prior to submitting a change.
 For the Verified label that is installed by the link:pgm-init.html[init]
 site program this is enabled by default.
+
 Defaults to false.
 
 [[label_copyAllScoresIfNoChange]]
@@ -268,7 +302,9 @@
 set SHA1 is different. This can be used to enable sticky
 approvals, reducing turn-around for this special case.
 It is recommended to leave this enabled for both Verified and
-Code-Review labels. Defaults to true.
+Code-Review labels.
+
+Defaults to true.
 
 [[label_canOverride]]
 === `label.Label-Name.canOverride`
@@ -287,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.
@@ -307,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
@@ -324,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
@@ -338,7 +375,7 @@
       value = +2 Hmm, this is pretty nice
       value = +3 Ohh, hell yes!
       defaultValue = -3
-====
+----
 
 Upon clicking the Reply button:
 
@@ -346,6 +383,20 @@
 * Project Owners have limited scores (-2..+2) available, -2 is set as the default.
 * Registered Users have limited scores (-1..+1) available, -1 is set as the default.
 
+=== Patch Set Lock Example
+
+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"]
+      function = PatchSetLock
+      value =  0 Patch Set Unlocked
+      value = +1 Patch Set Locked
+      defaultValue = 0
+----
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-login-register.txt b/Documentation/config-login-register.txt
index 2e775b4..ffeae62 100644
--- a/Documentation/config-login-register.txt
+++ b/Documentation/config-login-register.txt
@@ -106,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-mail.txt b/Documentation/config-mail.txt
index da213a8..51ea9c5 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -56,6 +56,18 @@
 text that will be appended to emails related to a user submitting comments on
 changes.  See `ChangeSubject.vm`, `Comment.vm` and `ChangeFooter.vm`.
 
+=== DeleteVote.vm
+
+The `DeleteVote.vm` template will determine the contents of the email related
+to removing votes on changes.  It is a `ChangeEmail`: see `ChangeSubject.vm`
+and `ChangeFooter.vm`.
+
+=== DeleteReviewer.vm
+
+The `DeleteReviewer.vm` template will determine the contents of the email related
+to a user removing a reviewer (with a vote) from a change.  It is a
+`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
+
 === Footer.vm
 
 The `Footer.vm` template will determine the contents of the footer text
@@ -68,12 +80,6 @@
 a change successfully merged to the head.  It is a `ChangeEmail`: see
 `ChangeSubject.vm` and `ChangeFooter.vm`.
 
-=== MergeFail.vm
-
-The `MergeFail.vm` template will determine the contents of the email related
-to a failure upon attempting to merge a change to the head.  It is a
-`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
-
 === NewChange.vm
 
 The `NewChange.vm` template will determine the contents of the email related
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index ca95099..b7c1415 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
 
@@ -128,14 +140,12 @@
 these plugins.
 
 The Gerrit Project doesn't provide binaries for these plugins, but
-there are some public services that offer the download of pre-built
+there is one public service that offers the download of pre-built
 plugin jars:
 
 * link:https://gerrit-ci.gerritforge.com[CI Server from GerritForge]
-* link:http://builds.quelltextlich.at/gerrit/nightly/index.html[
-  CI Server from Quelltextlich]
 
-The following list gives an overview about available plugins, but the
+The following list gives an overview of available plugins, but the
 list may not be complete. You may discover more plugins on
 link:https://gerrit-review.googlesource.com/#/admin/projects/?filter=plugins%252F[
 gerrit-review].
@@ -154,24 +164,24 @@
 Documentation]
 
 [[avatars-external]]
-=== avatars/external
+=== avatars-external
 
 This plugin allows to use an external url to load the avatar images
 from.
 
-link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/avatars/external[
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/avatars-external[
 Project] |
-link:https://gerrit.googlesource.com/plugins/avatars/external/+doc/master/src/main/resources/Documentation/about.md[
+link:https://gerrit.googlesource.com/plugins/avatars-external/+doc/master/src/main/resources/Documentation/about.md[
 Documentation] |
-link:https://gerrit.googlesource.com/plugins/avatars/external/+doc/master/src/main/resources/Documentation/config.md[
+link:https://gerrit.googlesource.com/plugins/avatars-external/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
 [[avatars-gravatar]]
-=== avatars/gravatar
+=== avatars-gravatar
 
 Plugin to display user icons from Gravatar.
 
-link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/avatars/gravatar[
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/avatars-gravatar[
 Project]
 
 [[branch-network]]
@@ -197,20 +207,10 @@
 link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/changemessage[
 Project] |
 link:https://gerrit.googlesource.com/plugins/changemessage/+doc/master/src/main/resources/Documentation/about.md[
-Plugin Documenatation] |
+Plugin Documentation] |
 link:https://gerrit.googlesource.com/plugins/changemessage/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
-[[codenvy]]
-=== codenvy
-
-Plugin to allow to edit code on-line on either an existing branch or an
-active change using the link:http://codenvy.com[Codenvy] cloud
-development platform.
-
-link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/codenvy[
-Project]
-
 [[delete-project]]
 === delete-project
 
@@ -235,6 +235,18 @@
 link:https://gerrit.googlesource.com/plugins/egit/+doc/master/src/main/resources/Documentation/about.md[
 Documentation]
 
+[[emoticons]]
+=== emoticons
+
+This plugin allows users to see emoticons in comments as images.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/emoticons[
+Project] |
+link:https://gerrit.googlesource.com/plugins/emoticons/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/emoticons/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
 [[force-draft]]
 === force-draft
 
@@ -406,6 +418,30 @@
 link:https://gerrit.googlesource.com/plugins/menuextender/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[metrics-reporter-elasticsearch]]
+=== metrics-reporter-elasticsearch
+
+This plugin reports Gerrit metrics to Elasticsearch.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-elasticsearch[
+Project].
+
+[[metrics-reporter-graphite]]
+=== metrics-reporter-graphite
+
+This plugin reports Gerrit metrics to Graphite.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-graphite[
+Project].
+
+[[metrics-reporter-jmx]]
+=== metrics-reporter-jmx
+
+This plugin reports Gerrit metrics to JMX.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-jmx[
+Project].
+
 [[motd]]
 === motd
 
@@ -546,6 +582,18 @@
 link:https://gerrit.googlesource.com/plugins/scripting/scala-provider/+doc/master/src/main/resources/Documentation/about.md[
 Documentation]
 
+[[scripts]]
+=== scripts
+
+Repository containing a collection of Gerrit scripting plugins that are intended
+to provide simple and useful extensions.
+
+Groovy and Scala scripts require the installation of the corresponding
+scripting/*-provider plugin in order to be loaded into Gerrit.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripts[Project]
+link:https://gerrit.googlesource.com/plugins/scripts/+doc/master/README.md[Documentation]
+
 [[server-config]]
 === server-config
 
@@ -555,7 +603,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]]
@@ -568,7 +616,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] |
@@ -584,22 +632,36 @@
 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] |
 link:https://gerrit.googlesource.com/plugins/uploadvalidator/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[verify-status]]
+=== verify-status
+
+This plugin adds a separate channel for Gerrit to store test metadata and
+view them on the Gerrit UI.  The metadata can be stored in the Gerrit database
+or in a completely separate datastore.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/verify-status[
+Project] |
+link:https://gerrit.googlesource.com/plugins/verify-status/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/verify-status/+doc/master/src/main/resources/Documentation/database.md[
+Configuration]
+
 [[websession-flatfile]]
 === websession-flatfile
 
 This plugin replaces the built-in Gerrit H2 based websession cache with
-a flatfile based implementation. This implemantation is shareable
+a flatfile based implementation. This implementation is shareable
 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] |
@@ -617,7 +679,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] |
@@ -629,7 +691,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-project-config.txt b/Documentation/config-project-config.txt
index 7d03681..7121265 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -65,8 +65,7 @@
 
 The link:#access-section[+access+ section] appears once per reference pattern,
 such as `+refs/*+` or `+refs/heads/*+`.  Only one access section per pattern is
-allowed.  You will find examples of keys and values in each category section
-<<access_category,below>>.
+allowed.
 
 The link:#receive-section[+receive+ section] appears once per project.
 
@@ -74,8 +73,7 @@
 
 The link:#capability-section[+capability+] section only appears once, and only
 in the +All-Projects+ repository.  It controls core features that are configured
-on a global level.  You can find examples of these
-<<capability_category,below>>.
+on a global level.
 
 The link:#label-section[+label+] section can appear multiple times. You can
 also redefine the text and behavior of the built in label types `Code-Review`
@@ -175,6 +173,22 @@
 Default is `INHERIT`, which means that this property is inherited from
 the parent project.
 
+[[receive.rejectImplicitMerges]]receive.rejectImplicitMerges::
++
+Controls whether a check for implicit merges will be performed when changes are
+pushed for review. An implicit merge is a case where merging an open change
+would implicitly merge another branch into the target branch. Typically, this
+happens when a change is done on master and, by mistake, pushed to a stable branch
+for review. When submitting such change, master would be implicitly merged into
+stable without anyone noticing that. When this option is set to 'true' Gerrit
+will reject the push if an implicit merge is detected.
++
+This check is only done for non-merge commits, merge commits are not subject of
+the implicit merge check.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
 [[submit-section]]
 === Submit section
 
diff --git a/Documentation/config-sso.txt b/Documentation/config-sso.txt
index 897ca29..684b87c 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/config.defs b/Documentation/config.defs
index 380080f..7f814d3 100644
--- a/Documentation/config.defs
+++ b/Documentation/config.defs
@@ -15,7 +15,7 @@
     'tilde="&#126;"',
     'last-update-label!',
     'source-highlighter=prettify',
-    'stylesheet=doc.css',
+    'stylesheet=DEFAULT',
     'linkcss=true',
     'prettifydir=.',
     'revnumber="%s"' % revision,
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt
index d720287..0f73bd3 100644
--- a/Documentation/database-setup.txt
+++ b/Documentation/database-setup.txt
@@ -40,7 +40,7 @@
 for larger installations. It's the database backend with the largest userbase
 in the Gerrit community.
 
-Create a user for the web application within Postgres, assign it a
+Create a user for the web application within PostgreSQL, assign it a
 password, create a database to store the metadata, and grant the user
 full rights on the newly created database:
 
@@ -207,6 +207,51 @@
         password = secret_pasword
 ----
 
+[[createdb_hana]]
+=== SAP HANA
+
+SAP HANA is a supported database for running Gerrit Code Review. However it is
+recommended only for environments where you intend to run Gerrit on an existing
+HANA installation to reduce administrative overhead.
+
+In the HANA studio or the SAP HANA Web-based Development Workbench create a user
+'GERRIT2' with the role 'RESTRICTED_USER_JDBC_ACCESS' and a password
+<secret password>. This will also create an associated schema on the database.
+As this user would be required to change the password upon first login you might
+want to to disable the password lifetime check by executing
+'ALTER USER GERRIT2 DISABLE PASSWORD LIFETIME'.
+
+To run Gerrit on HANA, you need to obtain the HANA JDBC driver. It can be found
+as described
+link:http://help.sap.com/saphelp_hanaplatform/helpdata/en/ff/15928cf5594d78b841fbbe649f04b4/frameset.htm[here].
+It needs to be stored in the 'lib' folder of the review site.
+
+In the following sample database section it is assumed that HANA is running on
+the host 'hana.host' with the instance number 00 where a schema/user GERRIT2
+was created:
+
+In $site_path/etc/gerrit.config:
+
+----
+[database]
+        type = hana
+        instance = 00
+        hostname = hana.host
+        username = GERRIT2
+
+----
+
+In $site_path/etc/secure.config:
+
+----
+[database]
+        password = <secret password>
+----
+
+Visit SAP HANA's link:http://help.sap.com/hana_appliance/[documentation] for
+further information regarding using SAP HANA.
+
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index ec8515f..dd707f5 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -3,11 +3,11 @@
 
 == Installation
 
-Note that you need to use Java 7 for building gerrit.
+You need to use Java 8 and Node.js for building gerrit.
 
 There is currently no binary distribution of Buck, so it has to be manually
-built and installed.  Apache Ant is required.  Currently only Linux and Mac
-OS are supported.
+built and installed.  Apache Ant and gcc are required.  Currently only Linux
+and Mac OS are supported.
 
 Clone the git and build it:
 
@@ -47,7 +47,7 @@
 ----
 
 To enable autocompletion of buck commands, install the autocompletion
-script from `./scripts/buck_completion.bash` in the buck project.  Refer
+script from `./scripts/buck-completion.bash` in the buck project.  Refer
 to the script's header comments for installation instructions.
 
 == Prerequisites
@@ -81,13 +81,15 @@
 
 === Attaching Sources
 
-To save time and bandwidth source JARs are only downloaded by the buck
-build where necessary to compile Java source into JavaScript using the
-GWT compiler.  Additional sources may be obtained, allowing Eclipse to
-show documentation or dive into the implementation of a library JAR:
+Source JARs are downloaded by default. This allows Eclipse to show
+documentation or dive into the implementation of a library JAR.
+
+To save time and bandwidth, download of source JARs can be restricted
+to only those that are necessary to compile Java source into JavaScript
+using the GWT compiler:
 
 ----
-  tools/eclipse/project.py --src
+  tools/eclipse/project.py --no-src
 ----
 
 
@@ -97,18 +99,34 @@
 
 === Gerrit Development WAR File
 
-To build the Gerrit web application:
+To build the Gerrit web application that includes GWT UI and PolyGerrit UI:
 
 ----
   buck build gerrit
 ----
 
+[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:
 
 ----
   buck-out/gen/gerrit/gerrit.war
 ----
 
+To build the Gerrit web application that includes only GWT UI:
+
+----
+  buck build gwtgerrit
+----
+
+The output executable WAR will be placed in:
+
+----
+  buck-out/gen/gwtgerrit/gwtgerrit.war
+----
+
 
 === Headless Mode
 
@@ -144,13 +162,13 @@
 Install {extension,plugin,gwt}-api to the local maven repository:
 
 ----
-  buck build api_install
+  tools/maven/api.sh install
 ----
 
 Install gerrit.war to the local maven repository:
 
 ----
-  buck build war_install
+  tools/maven/api.sh war_install
 ----
 
 === Plugins
@@ -596,7 +614,7 @@
 The following tests should be executed, when Buck version is upgraded:
 
 * buck build release
-* buck build api_install
+* tools/maven/api.sh install
 * buck test
 * buck build gerrit, change some sources in gerrit-server project,
   repeat buck build gerrit and verify that gerrit.war was updated
@@ -617,7 +635,7 @@
 [known bug](https://github.com/facebook/buck/issues/341) related to
 symbolic links. The symbolic links are used very often with external
 plugins, that are linked per symbolic link to the plugins directory.
-With this use case Buck is failing to rebuild the plugin artefact
+With this use case Buck is failing to rebuild the plugin artifact
 after it was built. All attempts to convince Buck to rebuild will fail.
 The only known way to recover is to weep out `buck-out` directory. The
 better workaround is to avoid using Watchman in this specific use case.
@@ -625,30 +643,6 @@
 link:#buck-daemon[Using Buck daemon] section above how to temporarily
 disable `buckd`.
 
-=== Re-triggering rule execution
-
-There is no way to re-trigger custom rules with side effects, like
-`api_{deploy|install}`. This is a `genrule()` that depends on Java sources
-and is deploying the Plugin API through custom Python script to the local or
-remote Maven repositories. When for some reasons the deployment was undone,
-there is no supported way to re-trigger the execution of `api_{deploy|install}`
-targets. That's because `--no-cache` option will ignore the `Buck` cache, but
-there is no way to ignore `buck-out` directory. To overcome this Buck's design
-limitation new `tools/maven/api.py` script was added, that always re-triggers
-installation or deployment of Plugin API to local or Central Maven repository.
-
-```
-  tools/maven/api.py {deploy|install}
-```
-
-Dry run mode is also supported:
-
-```
-  tools/maven/api.py -n {deploy|install}
-```
-
-With this script the deployment would re-trigger on every invocation.
-
 == Troubleshooting Buck
 
 In some cases problems with Buck itself need to be investigated. See for example
diff --git a/Documentation/dev-build-plugins.txt b/Documentation/dev-build-plugins.txt
index 2c04d17..13071df 100644
--- a/Documentation/dev-build-plugins.txt
+++ b/Documentation/dev-build-plugins.txt
@@ -36,7 +36,7 @@
 * build and install `SNAPSHOT` version of plugin API in local Maven repository:
 
 ----
-buck build api_install
+./tools/maven/api.sh install
 ----
 
 === Exception 2:
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 2d96b84..775fe21 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -164,7 +164,8 @@
     contributors may also like to open several editors side by
     side while editing new changes.
   * Use 2 spaces for indent (no tabs)
-  * Use brackets in all ifs, spaces before/after if parens.
+  * Use braces in all if/else/for/do/while/catch blocks, spaces before/after
+    if/for/while/catch parens.
   * Use /** */ style Javadocs for variables.
 
 Additionally, you will notice that most of the newline spacing
@@ -182,7 +183,7 @@
 Always:
 
   * final fields: marking fields as final forces them to be
-  initialised in the constructor or at declaration
+  initialized in the constructor or at declaration
   * final static fields: clearly communicates the intent
   * to use final variables in inner anonymous classes
 
@@ -360,7 +361,7 @@
 * Determine the sha1 hash of the zip file:
 +
 ----
- openssl sha1 4.10.0-6-gd0a2dda.zip
+ openssl sha1 codemirror-4.10.0-6-gd0a2dda.zip
 ----
 
 * Upload the zip file to the
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 188ce14..4d636cbb 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.12.9 \
+    -DarchetypeVersion=2.13.14 \
     -DgroupId=com.googlesource.gerrit.plugins.testplugin \
     -DartifactId=testplugin
 ----
@@ -90,12 +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.
-  Implementation-URL: http://example.com/opensource/plugin-foo/
-====
+----
 
 === ApiType
 
@@ -105,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
 
@@ -120,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
@@ -217,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
@@ -260,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
@@ -381,10 +380,16 @@
 
 * `com.google.gerrit.common.EventListener`:
 +
-Allows to listen to events. These are the same
-link:cmd-stream-events.html#events[events] that are also streamed by
+Allows to listen to events without user visibility restrictions. These
+are the same link:cmd-stream-events.html#events[events] that are also streamed by
 the link:cmd-stream-events.html[gerrit stream-events] command.
 
+* `com.google.gerrit.common.UserScopedEventListener`:
++
+Allows to listen to events visible to the specified user. These are the
+same link:cmd-stream-events.html#events[events] that are also streamed
+by the link:cmd-stream-events.html[gerrit stream-events] command.
+
 * `com.google.gerrit.extensions.events.LifecycleListener`:
 +
 Plugin start and stop
@@ -409,6 +414,14 @@
 +
 Garbage collection ran on a project
 
+* `com.google.gerrit.server.extensions.events.ChangeIndexedListener`:
++
+Update of the change secondary index
+
+* `com.google.gerrit.server.extensions.events.AccountIndexedListener`:
++
+Update of the account secondary index
+
 [[stream-events]]
 == Sending Events to the Events Stream
 
@@ -416,17 +429,49 @@
 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.
-Deserialzing events with the
+Deserializing events with the
 `com.google.gerrit.server.events.EventDeserializer` class requires
 that the event be registered in EventTypes.
 
+== Modifying the Stream Event Flow
+
+It is possible to modify the stream event flow from plugins by registering
+an `com.google.gerrit.server.events.EventDispatcher`. A plugin may register
+a Dispatcher class to replace the internal Dispatcher. EventDispatcher is
+a DynamicItem, so Gerrit may only have one copy.
+
 [[validation]]
 == Validation Listeners
 
@@ -457,6 +502,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
 
@@ -587,6 +638,48 @@
 $ ssh -p 29418 review.example.com sh ps
 ----
 
+[[search_operators]]
+=== Search Operators ===
+
+Plugins can define new search operators to extend change searching by
+implementing the `ChangeQueryBuilder.ChangeOperatorFactory` interface
+and registering it to an operator name in the plugin module's
+`configure()` method.  The search operator name is defined during
+registration via the DynamicMap annotation mechanism.  The plugin
+name will get appended to the annotated name, with an underscore
+in between, leading to the final operator name.  An example
+registration looks like this:
+
+    bind(ChangeOperatorFactory.class)
+       .annotatedWith(Exports.named("sample"))
+       .to(SampleOperator.class);
+
+If this is registered in the `myplugin` plugin, then the resulting
+operator will be named `sample_myplugin`.
+
+The search operator itself is implemented by ensuring that the
+`create()` method of the class implementing the
+`ChangeQueryBuilder.ChangeOperatorFactory` interface returns a
+`Predicate<ChangeData>`.  Here is a sample operator factory
+definition which creates a `MyPredicate`:
+
+[source,java]
+----
+@Singleton
+public class SampleOperator
+    implements ChangeQueryBuilder.ChangeOperatorFactory {
+  public static class MyPredicate extends OperatorChangePredicate<ChangeData> {
+    ...
+  }
+
+  @Override
+  public Predicate<ChangeData> create(ChangeQueryBuilder builder, String value)
+      throws QueryParseException {
+    return new MyPredicate(value);
+  }
+}
+----
+
 [[simple-configuration]]
 == Simple Configuration in `gerrit.config`
 
@@ -654,6 +747,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:
 
@@ -666,6 +771,8 @@
 
 String[] reviewers = cfg.getGlobalPluginConfig("default-reviewer")
                         .getStringList("branch", "refs/heads/master", "reviewer");
+String password = cfg.getGlobalPluginConfig("default-reviewer")
+                     .getString("auth", null, "password");
 ----
 
 
@@ -997,15 +1104,26 @@
 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`:
 +
 The link:rest-api-changes.html#change-info[ChangeInfo] entity for the
 current change.
++
+The link:rest-api-changes.html#revision-info[RevisionInfo] entity for
+the current patch set.
 
 * Project Info Screen:
 ** `GerritUiExtensionPoint.PROJECT_INFO_SCREEN_TOP`:
@@ -1158,20 +1276,15 @@
   [...]
   // update change
   ReviewDb db = dbProvider.get();
-  db.changes().beginTransaction(change.getId());
-  try {
-    change = db.changes().atomicUpdate(
-      change.getId(),
-      new AtomicUpdate<Change>() {
-        @Override
-        public Change update(Change change) {
-          ChangeUtil.updated(change);
-          return change;
-        }
-      });
-    db.commit();
-  } finally {
-    db.rollback();
+  try (BatchUpdate bu = batchUpdateFactory.create(
+      db, project.getNameKey(), user, TimeUtil.nowTs())) {
+    bu.addOp(change.getId(), new BatchUpdate.Op() {
+      @Override
+      public boolean updateChange(BatchUpdate.ChangeContext ctx) {
+        return true;
+      }
+    });
+    bu.execute();
   }
   [...]
 }
@@ -1284,7 +1397,7 @@
 doesn't have to set `UiAction.Description.setVisible()` explicitly in this
 case.
 
-The following prerequisities must be met, to satisfy the capability check:
+The following prerequisites must be met, to satisfy the capability check:
 
 * user is authenticated
 * user is a member of a group which has the `Administrate Server` capability, or
@@ -1298,13 +1411,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:
@@ -1746,7 +1859,7 @@
 
   @Inject
   public MyMenu(@PluginName String name) {
-    menuEntries = Lists.newArrayList();
+    menuEntries = new ArrayList<>();
     menuEntries.add(new MenuEntry("My Menu", Collections.singletonList(
       new MenuItem("My Screen", "#/x/" + name + "/my-screen", ""))));
   }
@@ -1912,6 +2025,32 @@
 No Guice bindings or modules are required. Gerrit will automatically
 discover and bind the implementation.
 
+[[accountcreation]]
+== Account Creation
+
+Plugins can hook into the
+link:rest-api-accounts.html#create-account[account creation] REST API and
+inject additional external identifiers for an account that represents a user
+in some external user store. For that, an implementation of the extension
+point `com.google.gerrit.server.api.accounts.AccountExternalIdCreator`
+must be registered.
+
+[source,java]
+----
+class MyExternalIdCreator implements AccountExternalIdCreator {
+  @Override
+  public List<AccountExternalId> create(Account.Id id, String username,
+      String email) {
+    // your code
+  }
+}
+
+bind(AccountExternalIdCreator.class)
+  .annotatedWith(UniqueAnnotations.create())
+  .to(MyExternalIdCreator.class);
+}
+----
+
 [[download-commands]]
 == Download Commands
 
@@ -1971,6 +2110,12 @@
 }
 ----
 
+ParentWebLinks will appear to the right of the SHA1 of the parent
+revisions in the UI. The implementation should in most use cases direct
+to the same external service as PatchSetWebLink; it is provided as a
+separate interface because not all users want to have links for the
+parent revisions.
+
 FileWebLinks will appear in the side-by-side diff screen on the right
 side of the patch selection on each side.
 
@@ -1982,6 +2127,144 @@
 
 BranchWebLinks will appear in the branch list in the last column.
 
+FileHistoryWebLinks will appear on the access rights screen.
+
+[[lfs-extension]]
+== LFS Storage Plugins
+
+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]
+----
+/** Provide an LFS protocol implementation */
+import org.eclipse.jgit.lfs.server.LargeFileRepository;
+import org.eclipse.jgit.lfs.server.LfsProtocolServlet;
+
+@Singleton
+public class LfsApiServlet extends LfsProtocolServlet {
+  private static final long serialVersionUID = 1L;
+
+  private final S3LargeFileRepository repository;
+
+  @Inject
+  LfsApiServlet(S3LargeFileRepository repository) {
+    this.repository = repository;
+  }
+
+  @Override
+  protected LargeFileRepository getLargeFileRepository() {
+    return repository;
+  }
+}
+
+/** Register the LfsApiServlet to listen on the default LFS protocol endpoint */
+import static com.google.gerrit.httpd.plugins.LfsPluginServlet.URL_REGEX;
+
+import com.google.gerrit.httpd.plugins.HttpPluginModule;
+
+public class HttpModule extends HttpPluginModule {
+
+  @Override
+  protected void configureServlets() {
+    serveRegex(URL_REGEX).with(LfsApiServlet.class);
+  }
+}
+
+/** Provide an implementation of the LargeFileRepository */
+import org.eclipse.jgit.lfs.server.s3.S3Repository;
+
+public class S3LargeFileRepository extends S3Repository {
+...
+}
+----
+
+[[metrics]]
+== Metrics
+
+=== Metrics Reporting
+
+To send Gerrit's metrics data to an external reporting backend, a plugin can
+get a `MetricRegistry` injected and register an instance of a class that
+implements the `Reporter` interface from link:http://metrics.dropwizard.io/[
+DropWizard Metrics].
+
+Metric reporting plugin implementations are provided for
+link:https://gerrit.googlesource.com/plugins/metrics-reporter-jmx/[JMX],
+link:https://gerrit.googlesource.com/plugins/metrics-reporter-elasticsearch/[Elastic Search],
+and link:https://gerrit.googlesource.com/plugins/metrics-reporter-graphite/[Graphite].
+
+There is also a working example of reporting metrics to the console in the
+link:https://gerrit.googlesource.com/plugins/cookbook-plugin/+/master/src/main/java/com/googlesource/gerrit/plugins/cookbook/ConsoleMetricReporter.java[
+cookbook plugin].
+
+=== Providing own metrics
+
+Plugins may provide metrics to be dispatched to external reporting services by
+getting a `MetricMaker` injected and creating instances of specific types of
+metric:
+
+* Counter
++
+Metric whose value increments during the life of the process.
+
+* Timer
++
+Metric recording time spent on an operation.
+
+* Histogram
++
+Metric recording statistical distribution (rate) of values.
+
+Note that metrics cannot be recorded from plugin init steps that
+are run during site initialization.
+
+By default, plugin metrics are recorded under
+`plugins/${plugin-name}/${metric-name}`. This can be changed by
+setting `plugins.${plugin-name}.metricsPrefix` in the `gerrit.config`
+file. For example:
+
+----
+  [plugin "my-plugin"]
+    metricsPrefix = my-metrics
+----
+
+will cause the metrics to be recorded under `my-metrics/${metric-name}`.
+
+See the replication metrics in the
+link:https://gerrit.googlesource.com/plugins/replication/+/master/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationMetrics.java[
+replication plugin] for an example of usage.
+
+[[account-patch-review-store]]
+== AccountPatchReviewStore
+
+The AccountPatchReviewStore is used to store reviewed flags on changes.
+A reviewed flag is a tuple of (patch set ID, file, account ID) and
+records whether the user has reviewed a file in a patch set. Each user
+can easily have thousands of reviewed flags and the number of reviewed
+flags is growing without bound. The store must be able handle this data
+volume efficiently.
+
+Gerrit implements this extension point, but plugins may bind another
+implementation, e.g. one that supports multi-master.
+
+----
+DynamicItem.bind(binder(), AccountPatchReviewStore.class)
+    .to(MultiMasterAccountPatchReviewStore.class);
+
+...
+
+public class MultiMasterAccountPatchReviewStore
+    implements AccountPatchReviewStore {
+  ...
+}
+----
+
 [[documentation]]
 == Documentation
 
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index b2c5358..921244f 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -142,8 +142,22 @@
   </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.
 
-* Add this to the `pom.xml` to enable the wagon provider:
+
+* Add these two snippets to the `pom.xml` to enable the wagon provider:
+
+----
+  <pluginRepositories>
+    <pluginRepository>
+      <id>gerrit-maven</id>
+      <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
+    </pluginRepository>
+  </pluginRepositories>
+----
 
 ----
   <build>
diff --git a/Documentation/dev-release-jgit.txt b/Documentation/dev-release-jgit.txt
new file mode 100644
index 0000000..f6d4d68
--- /dev/null
+++ b/Documentation/dev-release-jgit.txt
@@ -0,0 +1,41 @@
+= Making a Release of JGit
+
+This step is only necessary if we need to create an unofficial JGit
+snapshot release and publish it to the
+link:https://developers.google.com/storage/[Google Cloud Storage].
+
+
+[[prepare-release]]
+== Prepare the Release
+
+Since JGit has its own release process we do not push any release tags
+for JGit. Instead we will use the output of the `git describe` as the
+version of the current JGit snapshot.
+
+----
+  ./tools/version.sh --release $(git describe)
+----
+
+
+[[publish-release]]
+== Publish the Release
+
+* Make sure you have done the configuration needed for deployment:
+** link:dev-release-deploy-config.html#deploy-configuration-settings-xml[
+Configuration in Maven `settings.xml`]
+** link:dev-release-deploy-config.html#deploy-configuration-subprojects[
+Configuration for Subprojects in `pom.xml`]
+
+* Deploy the new snapshot. From JGit workspace execute:
++
+----
+  mvn deploy
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
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 3157214..96695db 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`)
 
@@ -154,17 +148,26 @@
 [[build-gerrit]]
 === Build Gerrit
 
-* Build the Gerrit WAR and API JARs
+* Build the Gerrit WAR, API JARs and documentation
 +
 ----
   buck clean
-  buck build --no-cache release
-  buck build api_install
+  buck build --no-cache release docs
+  ./tools/maven/api.sh install
 ----
 
 * Sanity check WAR
 * Test the new Gerrit version
 
+* Verify plugin versions
++
+Sometimes `buck` doesn't rebuild plugins after they are tagged, and the
+versions don't reflect the tag. Verify the versions:
++
+----
+  java -jar ./buck-out/gen/release/release.war init --list-plugins
+----
+
 [[publish-gerrit]]
 === Publish the Gerrit Release
 
@@ -182,26 +185,19 @@
 * Push the WAR to Maven Central:
 +
 ----
-  buck build war_deploy
+  ./tools/maven/api.sh war_deploy
 ----
 
 * Push the plugin artifacts to Maven Central:
 +
 ----
-  buck build api_deploy
-----
-+
-For troubleshooting, the environment variable `VERBOSE` can be set. This
-prints out the commands that are executed by the Buck build process:
-+
-----
-  VERBOSE=1 buck build api_deploy
+  ./tools/maven/api.sh deploy
 ----
 +
 If no artifacts are uploaded, clean the `buck-out` folder and retry:
 +
 ----
-  rm -rf buck-out
+  buck clean ; rm -rf buck-out
 ----
 
 * Push the plugin Maven archetypes to Maven Central:
@@ -296,7 +292,8 @@
 [[publish-to-google-storage]]
 ==== Publish the Gerrit WAR to the Google Cloud Storage
 
-* go to https://console.developers.google.com/project/164060093628/storage/gerrit-releases/
+* go to the link:https://console.cloud.google.com/storage/browser/gerrit-releases/?project=api-project-164060093628[
+gerrit-releases bucket in the Google cloud storage console]
 * make sure you are signed in with your Gmail account
 * manually upload the Gerrit WAR file by using the `Upload` button
 
@@ -333,20 +330,18 @@
 * Build the release notes:
 +
 ----
-  make -C ReleaseNotes
+  buck build releasenotes
 ----
 
-* Build the documentation:
-+
-----
-  buck build docs
-----
+* Extract the release notes files from the zip file generated from the previous
+step: `buck-out/gen/ReleaseNotes/html/html.zip`.
 
-* Extract the documentation html files from the generated zip file
-`buck-out/gen/Documentation/searchfree.zip`.
+* Extract the documentation files from the zip file generated from
+`buck build docs`: `buck-out/gen/Documentation/searchfree/searchfree.zip`.
 
-* Upload the html files manually via web browser to the
-link:https://console.developers.google.com/project/164060093628/storage/gerrit-documentation/[
+* Upload the files manually via web browser to the appropriate folder
+in the
+link:https://console.cloud.google.com/storage/browser/gerrit-documentation/?project=api-project-164060093628[
 gerrit-documentation] storage bucket.
 
 [[update-links]]
@@ -358,20 +353,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.
 
 
@@ -383,13 +373,12 @@
 ** A link to the release and the release notes (if a final release)
 ** A link to the docs
 ** Describe the type of release (stable, bug fix, RC)
-
-* Add an entry to the `NEWS` section of the main Gerrit project web page
-** Go to: http://code.google.com/p/gerrit/admin
-** Add entry like:
-----
- * Jun 14, 2012 - Gerrit 2.4.1 [https://groups.google.com/d/topic/repo-discuss/jHg43gixqzs/discussion Released]
-----
+** Hash values (SHA1, SHA256, MD5) for the release WAR file.
++
+The SHA1 and MD5 can be taken from the artifact page on Sonatype. The
+SHA256 can be generated with
+`openssl sha -sha256 buck-out/gen/release/release.war` or an equivalent
+command.
 
 * Update the new discussion group announcement to be sticky
 ** Go to: http://groups.google.com/group/repo-discuss/topics
diff --git a/Documentation/dev-stars.txt b/Documentation/dev-stars.txt
new file mode 100644
index 0000000..dfcbb6f
--- /dev/null
+++ b/Documentation/dev-stars.txt
@@ -0,0 +1,91 @@
+= Gerrit Code Review - Stars
+
+== Description
+
+Changes can be starred with labels that behave like private hashtags.
+Any label can be applied to a change, but these labels are only visible
+to the user for which the labels have been set.
+
+Stars allow users to categorize changes by self-defined criteria and
+then build link:user-dashboards.html[dashboards] for them by making use
+of the link:#query-stars[star query operators].
+
+[[star-api]]
+== Star API
+
+The link:rest-api-accounts.html#star-endpoints[star REST API] supports:
+
+* link:rest-api-accounts.html#get-stars[
+  get star labels from a change]
+* link:rest-api-accounts.html#set-stars[
+  update star labels on a change]
+* link:rest-api-accounts.html#get-starred-changes[
+  list changes that are starred by any label]
+
+Star labels are also included in
+link:rest-api-changes.html#change-info[ChangeInfo] entities that are
+returned by the link:rest-api-changes.html[changes REST API].
+
+There are link:rest-api-accounts.html#default-star-endpoints[
+additional REST endpoints] for the link:#default-star[default star].
+
+Only the link:#default-star[default star] is shown in the WebUI and
+can be updated from there. Other stars do not show up in the WebUI.
+
+[[default-star]]
+== Default Star
+
+If the default star is set by a user, this user is automatically
+notified by email whenever updates are made to that change.
+
+The default star is the star that is shown in the WebUI and which can
+be updated from there.
+
+The default star is represented by the special star label 'star'.
+
+[[ignore-star]]
+== Ignore Star
+
+If the ignore star is set by a user, this user gets no email
+notifications for updates of that change, even if this user is a
+reviewer of the change or the change is matched by a project watch of
+the user.
+
+Since changes can only be ignored once they are created, users that
+watch a project will always get the email notifications for the change
+creation. Only then the change can be ignored.
+
+Users that are added as reviewer to a change that they have ignored
+will be notified about this, so that they know about the review
+request. They can the decide to remove the ignore star.
+
+The ignore star is represented by the special star label 'ignore'.
+
+[[query-stars]]
+== Query Stars
+
+There are several query operators to find changes with stars:
+
+* link:user-search.html#star[star:<LABEL>]:
+  Matches any change that was starred by the current user with the
+  label `<LABEL>`.
+* link:user-search.html#has-stars[has:stars]:
+  Matches any change that was starred by the current user with any
+  label.
+* link:user-search.html#is-starred[is:starred] /
+  link:user-search.html#has-star[has:star]:
+  Matches any change that was starred by the current user with the
+  link:#default-star[default star].
+
+[[syntax]]
+== Syntax
+
+Star labels cannot contain whitespace characters. All other characters
+are allowed.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/doc.css.in b/Documentation/doc.css.in
deleted file mode 100644
index 429e81c..0000000
--- a/Documentation/doc.css.in
+++ /dev/null
@@ -1,60 +0,0 @@
-body {
-  margin: 1em auto;
-  width: 900px;
-}
-
-#toctitle {
-  margin-top: 0.5em;
-  font-weight: bold;
-}
-
-h1, h2, h3, h4, h5, h6, #toctitle {
-  color: #527bbd;
-  font-family: sans-serif;
-}
-
-h1, h2, h3 {
-  border-bottom: 2px solid silver;
-}
-
-h1 {
-  margin-top: 1.5em;
-}
-
-p {
-  margin: 0.5em 0 0.5em 0;
-}
-li p {
-  margin: 0.2em 0 0.2em 0;
-}
-
-#license > .content,
-.listingblock > .content {
-  border: 2px solid silver;
-  background: #ebebeb;
-  margin-left: 2em;
-  color: darkgreen;
-  padding: 2px;
-  overflow: auto;
-}
-
-#license > .content pre,
-.listingblock > .content pre {
-  background: none;
-  border: 0 solid silver;
-  padding: 0 0 0 0;
-}
-
-dl dt {
-  margin-top: 1em;
-}
-
-table.tableblock {
-  border-collapse: collapse;
-}
-
-table.tableblock,
-th.tableblock,
-td.tableblock {
-  border: 1px solid #EEE;
-}
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-messages.txt b/Documentation/error-messages.txt
index 16bc37b..2632254 100644
--- a/Documentation/error-messages.txt
+++ b/Documentation/error-messages.txt
@@ -20,7 +20,6 @@
 * link:error-missing-changeid.html[missing Change-Id in commit message footer]
 * link:error-missing-subject.html[missing subject; Change-Id must be in commit message footer]
 * link:error-multiple-changeid-lines.html[multiple Change-Id lines in commit message footer]
-* link:error-no-changes-made.html[no changes made]
 * link:error-no-common-ancestry.html[no common ancestry]
 * link:error-no-new-changes.html[no new changes]
 * link:error-non-fast-forward.html[non-fast forward]
@@ -32,7 +31,7 @@
 * link:error-permission-denied.html[Permission denied (publickey)]
 * link:error-prohibited-by-gerrit.html[prohibited by Gerrit]
 * link:error-project-not-found.html[Project not found: ...]
-* link:error-squash-commits-first.html[squash commits first]
+* link:error-same-change-id-in-multiple-changes.html[same Change-Id in multiple changes]
 * link:error-upload-denied.html[Upload denied for project \'...']
 * link:error-not-allowed-to-upload-merges.html[you are not allowed to upload merges]
 
diff --git a/Documentation/error-no-changes-made.txt b/Documentation/error-no-changes-made.txt
deleted file mode 100644
index 6182fcf..0000000
--- a/Documentation/error-no-changes-made.txt
+++ /dev/null
@@ -1,23 +0,0 @@
-= no changes made
-
-With this error message Gerrit rejects to push a commit as a new
-patch set for a change, if the pushed commit is identical to the
-current patch set of this change.
-
-A pushed commit is considered to be identical to the current patch
-set if
-
-- the files in the commit,
-- the commit message,
-- the author of the commit and
-- the parents of the commit
-
-are all identical.
-
-
-GERRIT
-------
-Part of link:error-messages.html[Gerrit Error Messages]
-
-SEARCHBOX
----------
diff --git a/Documentation/error-same-change-id-in-multiple-changes.txt b/Documentation/error-same-change-id-in-multiple-changes.txt
new file mode 100644
index 0000000..b6aad69
--- /dev/null
+++ b/Documentation/error-same-change-id-in-multiple-changes.txt
@@ -0,0 +1,112 @@
+= same Change-Id in multiple changes
+
+With this error message Gerrit rejects to push a commit if it
+contains the same Change-Id as a predecessor commit.
+
+The reason for rejecting such a commit is that it would introduce, for
+the corresponding change in Gerrit, a dependency upon itself. Gerrit
+prevents such dependencies between patch sets within the same change
+to keep the review process simple. Otherwise reviewers would not only
+have to review the latest patch set but also all the patch sets the
+latest one depends on.
+
+This error is quite common, it appears when a user tries to address
+review comments and creates a new commit instead of amending the
+existing commit. Another possibility for this error, although less
+likely, is that the user tried to create a patch series with multiple
+changes to be reviewed and accidentally included the same Change-Id
+into the different commit messages.
+
+
+== Example
+
+Here an example about how the push is failing. Please note that the
+two commits 'one commit' and 'another commit' both have the same
+Change-Id (of course in real life it can happen that there are more
+than two commits that have the same Change-Id).
+
+----
+  $ git log
+  commit 13d381265ffff88088e1af88d0e2c2c1143743cd
+  Author: John Doe <john.doe@example.com>
+  Date:   Thu Dec 16 10:15:48 2010 +0100
+
+      another commit
+
+      Change-Id: I93478acac09965af91f03c82e55346214811ac79
+
+  commit ca45e125145b12fe9681864b123bc9daea501bf7
+  Author: John Doe <john.doe@example.com>
+  Date:   Thu Dec 16 10:12:54 2010 +0100
+
+      one commit
+
+      Change-Id: I93478acac09965af91f03c82e55346214811ac79
+
+  $ git push ssh://JohnDoe@host:29418/myProject HEAD:refs/for/master
+  Counting objects: 8, done.
+  Delta compression using up to 2 threads.
+  Compressing objects: 100% (2/2), done.
+  Writing objects: 100% (6/6), 558 bytes, done.
+  Total 6 (delta 0), reused 0 (delta 0)
+  To ssh://JohnDoe@host:29418/myProject
+  ! [remote rejected] HEAD -> refs/for/master (same Change-Id in multiple changes.
+  Squash the commits with the same Change-Id or ensure Change-Ids are unique for each commit)
+  error: failed to push some refs to 'ssh://JohnDoe@host:29418/myProject'
+
+----
+
+If it was the intention to rework a change and push a new patch
+set, the problem can be fixed by squashing the commits that contain the
+same Change-Id. The squashed commit can then be pushed to Gerrit.
+
+To squash the commits, use `git rebase -i` to do an interactive rebase. For
+the example above where the last two commits have the same Change-Id,
+this means an interactive rebase for the last two commits should be
+done. For further details about the git rebase command please check
+the link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documentation for rebase].
+
+----
+  $ git rebase -i HEAD~2
+
+  pick ca45e12 one commit
+  squash 13d3812 another commit
+
+  [detached HEAD ab37207] squashed commit
+   1 files changed, 3 insertions(+), 0 deletions(-)
+  Successfully rebased and updated refs/heads/master.
+
+  $ git log
+  commit ab37207d33647685801dba36cb4fd51f3eb73507
+  Author: John Doe <john.doe@example.com>
+  Date:   Thu Dec 16 10:12:54 2010 +0100
+
+      squashed commit
+
+      Change-Id: I93478acac09965af91f03c82e55346214811ac79
+
+  $ git push ssh://JohnDoe@host:29418/myProject HEAD:refs/for/master
+  Counting objects: 5, done.
+  Writing objects: 100% (3/3), 307 bytes, done.
+  Total 3 (delta 0), reused 0 (delta 0)
+  To ssh://JohnDoe@host:29418/myProject
+   * [new branch]      HEAD -> refs/for/master
+----
+
+If it was the intention to create a patch series with multiple
+changes to be reviewed, each commit message should contain the
+Change-Id of the corresponding change in Gerrit.  If a change in
+Gerrit does not exist yet, the Change-Id should be generated (either
+by using a link:cmd-hook-commit-msg.html[commit hook] or by using EGit) or the Change-Id could be
+removed (not recommended since then amending this commit to create
+subsequent patch sets is more error prone). To change the Change-Id
+of an existing commit do an interactive link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase] and fix the
+affected commit messages.
+
+
+GERRIT
+------
+Part of link:error-messages.html[Gerrit Error Messages]
+
+SEARCHBOX
+---------
diff --git a/Documentation/error-squash-commits-first.txt b/Documentation/error-squash-commits-first.txt
deleted file mode 100644
index 4069d5b..0000000
--- a/Documentation/error-squash-commits-first.txt
+++ /dev/null
@@ -1,110 +0,0 @@
-= squash commits first
-
-With this error message Gerrit rejects to push a commit if it
-contains the same Change-Id as a predecessor commit.
-
-The reason for rejecting such a commit is that it would introduce, for
-the corresponding change in Gerrit, a dependency upon itself. Gerrit
-prevents such dependencies between patch sets within the same change
-to keep the review process simple. Otherwise reviewers would not only
-have to review the latest patch set but also all the patch sets the
-latest one depends on.
-
-This error is quite common, it appears when a user tries to address
-review comments and creates a new commit instead of amending the
-existing commit. Another possibility for this error, although less
-likely, is that the user tried to create a patch series with multiple
-changes to be reviewed and accidentally included the same Change-Id
-into the different commit messages.
-
-
-== Example
-
-Here an example about how the push is failing. Please note that the
-two commits 'one commit' and 'another commit' both have the same
-Change-Id (of course in real life it can happen that there are more
-than two commits that have the same Change-Id).
-
-----
-  $ git log
-  commit 13d381265ffff88088e1af88d0e2c2c1143743cd
-  Author: John Doe <john.doe@example.com>
-  Date:   Thu Dec 16 10:15:48 2010 +0100
-
-      another commit
-
-      Change-Id: I93478acac09965af91f03c82e55346214811ac79
-
-  commit ca45e125145b12fe9681864b123bc9daea501bf7
-  Author: John Doe <john.doe@example.com>
-  Date:   Thu Dec 16 10:12:54 2010 +0100
-
-      one commit
-
-      Change-Id: I93478acac09965af91f03c82e55346214811ac79
-
-  $ git push ssh://JohnDoe@host:29418/myProject HEAD:refs/for/master
-  Counting objects: 8, done.
-  Delta compression using up to 2 threads.
-  Compressing objects: 100% (2/2), done.
-  Writing objects: 100% (6/6), 558 bytes, done.
-  Total 6 (delta 0), reused 0 (delta 0)
-  To ssh://JohnDoe@host:29418/myProject
-   ! [remote rejected] HEAD -> refs/for/master (squash commits first)
-  error: failed to push some refs to 'ssh://JohnDoe@host:29418/myProject'
-----
-
-If it was the intention to rework a change and push a new patch
-set, the problem can be fixed by squashing the commits that contain the
-same Change-Id. The squashed commit can then be pushed to Gerrit.
-
-To squash the commits, use `git rebase -i` to do an interactive rebase. For
-the example above where the last two commits have the same Change-Id,
-this means an interactive rebase for the last two commits should be
-done. For further details about the git rebase command please check
-the link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documentation for rebase].
-
-----
-  $ git rebase -i HEAD~2
-
-  pick ca45e12 one commit
-  squash 13d3812 another commit
-
-  [detached HEAD ab37207] squashed commit
-   1 files changed, 3 insertions(+), 0 deletions(-)
-  Successfully rebased and updated refs/heads/master.
-
-  $ git log
-  commit ab37207d33647685801dba36cb4fd51f3eb73507
-  Author: John Doe <john.doe@example.com>
-  Date:   Thu Dec 16 10:12:54 2010 +0100
-
-      squashed commit
-
-      Change-Id: I93478acac09965af91f03c82e55346214811ac79
-
-  $ git push ssh://JohnDoe@host:29418/myProject HEAD:refs/for/master
-  Counting objects: 5, done.
-  Writing objects: 100% (3/3), 307 bytes, done.
-  Total 3 (delta 0), reused 0 (delta 0)
-  To ssh://JohnDoe@host:29418/myProject
-   * [new branch]      HEAD -> refs/for/master
-----
-
-If it was the intention to create a patch series with multiple
-changes to be reviewed, each commit message should contain the
-Change-Id of the corresponding change in Gerrit.  If a change in
-Gerrit does not exist yet, the Change-Id should be generated (either
-by using a link:cmd-hook-commit-msg.html[commit hook] or by using EGit) or the Change-Id could be
-removed (not recommended since then amending this commit to create
-subsequent patch sets is more error prone). To change the Change-Id
-of an existing commit do an interactive link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase] and fix the
-affected commit messages.
-
-
-GERRIT
-------
-Part of link:error-messages.html[Gerrit Error Messages]
-
-SEARCHBOX
----------
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 db3480b..15f470c 100755
--- a/Documentation/gen_licenses.py
+++ b/Documentation/gen_licenses.py
@@ -19,8 +19,8 @@
 
 import argparse
 from collections import defaultdict, deque
+import json
 from os import chdir, path
-import re
 from shutil import copyfileobj
 from subprocess import Popen, PIPE
 from sys import stdout, stderr
@@ -28,7 +28,6 @@
 parser = argparse.ArgumentParser()
 parser.add_argument('--asciidoc', action='store_true')
 parser.add_argument('--partial', action='store_true')
-parser.add_argument('--classpath', action='append')
 parser.add_argument('targets', nargs='+')
 args = parser.parse_args()
 
@@ -38,37 +37,30 @@
   '//lib/bouncycastle:bcprov',
 ]
 
+for target in args.targets:
+  if not target.startswith('//'):
+    print('Target must be absolute: %s' % target, file=stderr)
+
 def parse_graph():
   graph = defaultdict(list)
   while not path.isfile('.buckconfig'):
     chdir('..')
-  # TODO(davido): use passed in classpath from Buck instead
-  p = Popen(
-    ['buck', 'audit', 'classpath', '--dot'] + args.targets,
-    stdout = PIPE)
-  for line in p.stdout:
-    m = re.search(r'"(//.*?)" -> "(//.*?)";', line)
-    if not m:
-      continue
-    target, dep = m.group(1), m.group(2)
-    if args.partial:
-      if dep == '//lib/codemirror:js_minifier':
-        if target == '//lib/codemirror:js':
-          continue
-        if target.startswith('//lib/codemirror:mode_'):
-          continue
-      if target == '//gerrit-gwtui:ui_module' and \
-         dep == '//gerrit-gwtexpui:CSS':
+  query = ' + '.join('deps(%s)' % t for t in args.targets)
+  p = Popen([
+      'buck', 'query', query,
+      '--output-attributes=buck.direct_dependencies'], stdout=PIPE)
+  obj = json.load(p.stdout)
+  for target, attrs in obj.iteritems():
+    for dep in attrs['buck.direct_dependencies']:
+
+      if target in KNOWN_PROVIDED_DEPS:
         continue
 
-    # Dependencies included in provided_deps set are contained in audit
-    # classpath and must be sorted out. That's safe thing to do because
-    # they are not included in the final artifact.
-    if "DO_NOT_DISTRIBUTE" in dep:
-      if not target in KNOWN_PROVIDED_DEPS:
-        print('DO_NOT_DISTRIBUTE license for target: %s' % target, file=stderr)
-        exit(1)
-    else:
+      if (args.partial
+          and dep == '//gerrit-gwtexpui:CSS'
+          and target == '//gerrit-gwtui:ui_module'):
+        continue
+
       graph[target].append(dep)
   r = p.wait()
   if r != 0:
@@ -78,28 +70,39 @@
 graph = parse_graph()
 licenses = defaultdict(set)
 
+do_not_distribute = False
 queue = deque(args.targets)
 while queue:
   target = queue.popleft()
   for dep in graph[target]:
     if not dep.startswith('//lib:LICENSE-'):
       continue
+    if 'DO_NOT_DISTRIBUTE' in dep:
+      do_not_distribute = True
     licenses[dep].add(target)
   queue.extend(graph[target])
+
+if do_not_distribute:
+  print('DO_NOT_DISTRIBUTE license found', file=stderr)
+  for target in args.targets:
+    print('...via %s:' % target)
+    Popen(['buck', 'query',
+           'allpaths(%s, //lib:LICENSE-DO_NOT_DISTRIBUTE)' % target],
+          stdout=stderr).communicate()
+  exit(1)
+
 used = sorted(licenses.keys())
 
 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,
@@ -134,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:
@@ -144,27 +146,29 @@
   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:'):]
     else:
       p = d[d.index(':')+1:].lower()
+    if '__' in p:
+      p = p[:p.index('__')]
     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/images/link.png b/Documentation/images/link.png
deleted file mode 100644
index 621443e..0000000
--- a/Documentation/images/link.png
+++ /dev/null
Binary files differ
diff --git a/Documentation/images/user-review-ui-change-screen-not-current.png b/Documentation/images/user-review-ui-change-screen-not-current.png
new file mode 100644
index 0000000..9a87c67
--- /dev/null
+++ b/Documentation/images/user-review-ui-change-screen-not-current.png
Binary files differ
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 63e3be64..f53463c 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -25,16 +25,15 @@
 
 == Project Management
 . link:project-configuration.html[Project Configuration]
-. link:access-control.html[Access Controls]
 .. link:config-labels.html[Review Labels]
-.. link:config-project-config.html[Access Controls Configuration Format]
+.. link:config-project-config.html[Project Configuration File Format]
+. link:access-control.html[Access Controls]
 . Multi-project management
 .. link:user-submodules.html[Submodules]
 .. link:https://source.android.com/source/using-repo.html[Repo] (external)
 . 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
@@ -53,6 +52,7 @@
 . link:cmd-index.html[Command Line Tools]
 . link:config-plugins.html#replication[Replication]
 . link:config-plugins.html[Plugins]
+. link:metrics.html[Metrics]
 . link:config-reverseproxy.html[Reverse Proxy]
 . link:config-auto-site-initialization.html[Automatic Site Initialization on Startup]
 . link:pgm-index.html[Server Side Administrative Tools]
@@ -68,12 +68,14 @@
 .. link:dev-build-plugins.html[Building Gerrit plugins]
 .. link:js-api.html[JavaScript Plugin API]
 .. link:config-validation.html[Validation Interfaces]
+.. link:dev-stars.html[Starring Changes]
 . link:dev-design.html[System Design]
 . link:i18n-readme.html[i18n Support]
 
 == Maintainer
-. link:dev-release.html[Developer Release]
-. link:dev-release-subproject.html[Developer Subproject Release]
+. link:dev-release.html[Making a Gerrit Release]
+. link:dev-release-subproject.html[Making a Release of a Gerrit Subproject]
+. link:dev-release-jgit.html[Making a Release of JGit]
 
 == Resources
 * link:licenses.html[Licenses and Notices]
@@ -83,5 +85,9 @@
 * link:https://gerrit.googlesource.com/gerrit[Source Code]
 * link:https://www.gerritcodereview.com/about.md[A History of Gerrit Code Review]
 
+GERRIT
+------
+Part of link:https://www.gerritcodereview.com/[Gerrit Code Review]
+
 SEARCHBOX
 ---------
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..7a724f7 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
@@ -652,7 +652,7 @@
 How to develop a Gerrit plugin is described in the link:dev-plugins.html[
 Plugin Development] section.
 
-[[prject-lifecycle]]
+[[project-lifecycle]]
 == Project Lifecycle
 
 [[project-creation]]
@@ -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 b9bdad0..9bf6842 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -583,22 +583,6 @@
 
 The following preferences can be configured:
 
-- [[show-site-header]]`Show Site Header`:
-+
-Whether the site header should be shown.
-
-- [[use-flash]]`Use Flash Clipboard Widget`:
-+
-Whether the Flash clipboard widget should be used. If enabled Gerrit
-offers a copy-to-clipboard icon next to IDs and commands that need to
-be copied frequently, such as the Change-Ids, commit IDs and download
-commands.
-
-- [[cc-me]]`CC Me On Comments I Write`:
-+
-Whether you get notified by email as CC on comments that you write
-yourself.
-
 - [[review-category]]`Display In Review Category`:
 +
 This setting controls how the values of the review labels in change
@@ -607,7 +591,7 @@
 ** `None`:
 +
 For each review label only the voting value is shown. Approvals are
-rendered as a green check mark icon, vetos as a red X icon.
+rendered as a green check mark icon, vetoes as a red X icon.
 +
 ** `Show Name`:
 +
@@ -638,6 +622,32 @@
 +
 The format that should be used to render dates and timestamps.
 
+- [[email-notifications]]`Email Notifications`:
++
+This setting controls the email notifications.
++
+** `Enabled`:
++
+Email notifications are enabled.
++
+** [[cc-me]]`CC Me On Comments I Write`:
++
+Email notifications are enabled and you get notified by email as CC
+on comments that you write yourself.
++
+** `Disabled`:
++
+Email notifications are disabled.
+
+- [[diff-view]]`Diff View`:
++
+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 / Footer`:
++
+Whether the site header and footer should be shown.
+
 - [[relative-dates]]`Show Relative Dates In Changes Table`:
 +
 Whether timestamps in change lists and dashboards should be shown as
@@ -660,10 +670,20 @@
 Whether common path prefixes in the file list on the change screen
 should be link:user-review-ui.html#repeating-path-segments[grayed out].
 
-- [[diff-view]]`Diff View`:
+- [[inline-signed-off]]`Insert Signed-off-by Footer For Inline Edit Changes`:
 +
-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.
+Whether a `Signed-off-by` footer should be automatically inserted into
+changes that are created from the web UI (e.g. by the `Create Change`
+and `Edit Config` buttons on the project screen, and the `Follow-Up`
+button on the change screen).
+
+- [[use-flash]]`Use Flash Clipboard Widget`:
++
+Whether the Flash clipboard widget should be used. If enabled and the Flash
+plugin is available, Gerrit offers a copy-to-clipboard icon next to IDs and
+commands that need to be copied frequently, such as the Change-Ids, commit IDs
+and download commands. Note that this option is only shown if the Flash plugin
+is available and the JavaScript Clipboard API is unavailable.
 
 [[my-menu]]
 In addition it is possible to customize the menu entries of the `My`
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt
index 03ff5a5..8c9950e 100644
--- a/Documentation/js-api.txt
+++ b/Documentation/js-api.txt
@@ -176,6 +176,13 @@
   function must return true to allow the operation to continue, or
   false to prevent it.
 
+* `comment`: Invoked when a DOM element that represents a comment is
+  created. This DOM element is passed as argument. This DOM element
+  contains nested elements that Gerrit uses to format the comment. The
+  DOM structure may differ between comment types such as inline
+  comments, file-level comments and summary comments, and it may change
+  with new Gerrit versions.
+
 [[self_onAction]]
 === self.onAction()
 Register a JavaScript callback to be invoked when the user clicks
@@ -219,6 +226,50 @@
 * callback: JavaScript function to invoke when the user navigates to
   the screen. The function will be passed a link:#ScreenContext[screen context].
 
+[[self_settingsScreen]]
+=== self.settingsScreen()
+Register a JavaScript callback to be invoked when the user navigates
+to an extension settings screen provided by the plugin. Extension settings
+screens are automatically linked from the settings menu under the given
+menu entry.
+The callback can populate the DOM with the screen's contents.
+
+.Signature
+[source,javascript]
+----
+self.settingsScreen(path, menu, callback);
+----
+
+* path: URL path to identify the settings screen.
+
+* menu: The name of the menu entry in the settings menu that should
+  link to the settings screen.
+
+* callback: JavaScript function to invoke when the user navigates to
+  the settings screen. The function will be passed a
+  link:#SettingsScreenContext[settings screen context].
+
+[[self_panel]]
+=== self.panel()
+Register a JavaScript callback to be invoked when a screen with the
+given extension point is loaded.
+The callback can populate the DOM with the panel's contents.
+
+.Signature
+[source,javascript]
+----
+self.panel(extensionpoint, callback);
+----
+
+* extensionpoint: The name of the extension point that marks the
+  position where the panel is added to an existing screen. The
+  available extension points are described in the
+  link:dev-plugins.html#panels[plugin development documentation].
+
+* callback: JavaScript function to invoke when a screen with the
+  extension point is loaded. The function will be passed a
+  link:#PanelContext[panel context].
+
 [[self_url]]
 === self.url()
 Returns a URL within the plugin's URL space. If invoked with no
@@ -562,6 +613,86 @@
 Destroy the currently visible screen and display the plugin's screen.
 This method must be called after adding content to `screen.body`.
 
+[[SettingsScreenContext]]
+== Settings Screen Context
+A new settings screen context is passed to the `settingsScreen` callback
+function each time the user navigates to a matching URL.
+
+[[settingsScreen_body]]
+=== settingsScreen.body
+Empty HTML `<div>` node the plugin should add its content to.  The
+node is already attached to the document, but is invisible.  Plugins
+must call `settingsScreen.show()` to display the DOM node.  Deferred
+display allows an implementor to partially populate the DOM, make
+remote HTTP requests, finish populating when the callbacks arrive, and
+only then make the view visible to the user.
+
+[[settingsScreen_onUnload]]
+=== settingsScreen.onUnload()
+Configures an optional callback to be invoked just before the screen
+is deleted from the browser DOM.  Plugins can use this callback to
+remove event listeners from DOM nodes, preventing memory leaks.
+
+.Signature
+[source,javascript]
+----
+settingsScreen.onUnload(callback)
+----
+
+* callback: JavaScript function to be invoked just before the
+  `settingsScreen.body` DOM element is removed from the browser DOM.
+  This event happens when the user navigates to another screen.
+
+[[settingsScreen.setTitle]]
+=== settingsScreen.setTitle()
+Sets the heading text to be displayed when the screen is visible.
+This is presented in a large bold font below the menus, but above the
+content in `settingsScreen.body`. Setting the title also sets the
+window title to the same string, if it has not already been set.
+
+.Signature
+[source,javascript]
+----
+settingsScreen.setPageTitle(titleText)
+----
+
+[[settingsScreen.setWindowTitle]]
+=== settingsScreen.setWindowTitle()
+Sets the text to be displayed in the browser's title bar when the
+screen is visible.  Plugins should always prefer this method over
+trying to set `window.title` directly.  The window title defaults to
+the title given to `setTitle`.
+
+.Signature
+[source,javascript]
+----
+settingsScreen.setWindowTitle(titleText)
+----
+
+[[settingsScreen_show]]
+=== settingsScreen.show()
+Destroy the currently visible screen and display the plugin's screen.
+This method must be called after adding content to
+`settingsScreen.body`.
+
+[[PanelContext]]
+== Panel Context
+A new panel context is passed to the `panel` callback function each
+time a screen with the given extension point is loaded.
+
+[[panel_body]]
+=== panel.body
+Empty HTML `<div>` node the plugin should add the panel content to.
+The node is already attached to the document.
+
+[[PanelProperties]]
+=== Properties
+
+The extension panel parameters that are described in the
+link:dev-plugins.html#panels[plugin development documentation] are
+contained in the context as properties. Which properties are available
+depends on the extension point.
+
 [[Gerrit]]
 == Gerrit
 
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 32fa472..ef40aee 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -116,6 +116,8 @@
 
   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.
@@ -140,6 +142,8 @@
 
 value:: Value assigned by the approval, usually a numerical score.
 
+oldValue:: The previous approval score, only present if the value changed as a result of this event.
+
 grantedOn:: Time in seconds since the UNIX epoch when this approval
 was added or last updated.
 
diff --git a/Documentation/license.defs b/Documentation/license.defs
new file mode 100644
index 0000000..42dd3eb
--- /dev/null
+++ b/Documentation/license.defs
@@ -0,0 +1,29 @@
+def genlicenses(
+    name,
+    out,
+    opts = [],
+    java_deps = [],
+    non_java_deps = [],
+    visibility = []):
+  cmd = ['$(exe :gen_licenses)']
+  cmd.extend(opts)
+  cmd.append('>$OUT')
+  cmd.extend(java_deps)
+  cmd.extend(non_java_deps)
+
+  # Must use $(classpath) for Java deps, since transitive dependencies are not
+  # first-order dependencies of the output jar, so changes would not cause
+  # invalidation of the build cache key for the genrule.
+  cmd.extend('; true $(classpath %s)' % d for d in java_deps)
+
+  # Must use $(location) for non-Java deps, since $(classpath) will fail with an
+  # error. This is ok, because transitive dependencies are included in the
+  # output artifacts for everything _except_ Java libraries.
+  cmd.extend('; true $(location %s)' % d for d in non_java_deps)
+
+  genrule(
+    name = name,
+    out = out,
+    cmd = ' '.join(cmd),
+    visibility = visibility,
+  )
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
new file mode 100644
index 0000000..1270971
--- /dev/null
+++ b/Documentation/metrics.txt
@@ -0,0 +1,110 @@
+= Gerrit Code Review - Metrics
+
+Metrics about Gerrit's internal state can be sent to external monitoring systems
+via plugins. See the link:dev-plugins.html#metrics[plugin documentation] for
+details of plugin implementations.
+
+== Metrics
+
+The following metrics are reported.
+
+=== General
+
+* `build/label`: Version of Gerrit server software.
+* `events`: Triggered events.
+
+=== Process
+
+* `proc/birth_timestamp`: Time at which the Gerrit process started.
+* `proc/uptime`: Uptime of the Gerrit process.
+* `proc/cpu/usage`: CPU time used by the Gerrit process.
+* `proc/num_open_fds`: Number of open file descriptors.
+* `proc/jvm/memory/heap_committed`: Amount of memory guaranteed for user objects.
+* `proc/jvm/memory/heap_used`: Amount of memory holding user objects.
+* `proc/jvm/memory/non_heap_committed`: Amount of memory guaranteed for classes,
+etc.
+* `proc/jvm/memory/non_heap_used`: Amount of memory holding classes, etc.
+* `proc/jvm/memory/object_pending_finalization_count`: Approximate number of
+objects needing finalization.
+* `proc/jvm/gc/count`: Number of GCs.
+* `proc/jvm/gc/time`: Approximate accumulated GC elapsed time.
+* `proc/jvm/thread/num_live`: Current live thread count.
+
+=== Caches
+
+* `caches/memory_cached`: Memory entries.
+* `caches/memory_hit_ratio`: Memory hit ratio.
+* `caches/memory_eviction_count`: Memory eviction count.
+* `caches/disk_cached`: Disk entries used by persistent cache.
+* `caches/disk_hit_ratio`: Disk hit ratio for persistent cache.
+
+=== HTTP
+
+* `http/server/error_count`: Rate of REST API error responses.
+* `http/server/success_count`: Rate of REST API success responses.
+* `http/server/rest_api/count`: Rate of REST API calls by view.
+* `http/server/rest_api/error_count`: Rate of REST API calls by view.
+* `http/server/rest_api/server_latency`: REST API call latency by view.
+* `http/server/rest_api/response_bytes`: Size of REST API response on network
+(may be gzip compressed) by view.
+
+=== Query
+
+* `query/query_latency`: Successful query latency, accumulated over the life
+of the process.
+
+=== SSH sessions
+
+* `sshd/sessions/connected`: Number of currently connected SSH sessions.
+* `sshd/sessions/created`: Rate of new SSH sessions.
+* `sshd/sessions/authentication_failures`: Rate of SSH authentication failures.
+
+=== SQL connections
+
+* `sql/connection_pool/connections`: SQL database connections.
+
+=== JGit
+
+* `jgit/block_cache/cache_used`: Bytes of memory retained in JGit block cache.
+* `jgit/block_cache/open_files`: File handles held open by JGit block cache.
+
+=== Git
+
+* `git/upload-pack/request_count`: Total number of git-upload-pack requests.
+* `git/upload-pack/phase_counting`: Time spent in the 'Counting...' phase.
+* `git/upload-pack/phase_compressing`: Time spent in the 'Compressing...' phase.
+* `git/upload-pack/phase_writing`: Time spent transferring bytes to client.
+* `git/upload-pack/pack_bytes`: Distribution of sizes of packs sent to clients.
+
+=== NoteDb
+
+* `notedb/update_latency`: NoteDb update latency by table.
+* `notedb/stage_update_latency`: Latency for staging updates to NoteDb by table.
+* `notedb/read_latency`: NoteDb read latency by table.
+* `notedb/parse_latency`: NoteDb parse latency by table.
+* `notedb/auto_rebuild_latency`: NoteDb auto-rebuilding latency by table.
+* `notedb/auto_rebuild_failure_count`: NoteDb auto-rebuilding attempts that
+failed by table.
+
+=== Reviewer Suggestion
+
+* `reviewer_suggestion/query_accounts`: Latency for querying accounts for
+reviewer suggestion.
+* `reviewer_suggestion/query_groups`: Latency for querying groups for reviewer
+suggestion.
+
+=== Replication Plugin
+
+* `plugins/replication/replication_latency`: Time spent pushing to remote
+destination.
+* `plugins/replication/replication_delay`: Time spent waiting before pushing to
+remote destination.
+* `plugins/replication/replication_retries`: Number of retries when pushing to
+remote destination.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
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-MigrateAccountPatchReviewDb.txt b/Documentation/pgm-MigrateAccountPatchReviewDb.txt
new file mode 100644
index 0000000..5718a8a
--- /dev/null
+++ b/Documentation/pgm-MigrateAccountPatchReviewDb.txt
@@ -0,0 +1,66 @@
+= MigrateAccountPatchReviewDb
+
+== NAME
+MigrateAccountPatchReviewDb - Migrates AccountPatchReviewDb from one database
+backend to another.
+
+== SYNOPSIS
+[verse]
+--
+_java_ -jar gerrit.war MigrateAccountPatchReviewDb
+  -d <SITE_PATH>
+  [--sourceUrl] [--chunkSize]
+--
+
+== DESCRIPTION
+Migrates AccountPatchReviewDb from one database backend to another. The
+AccountPatchReviewDb is a database used to store the user file reviewed flags.
+
+This command is only intended to be run if the configuration parameter
+link:config-gerrit.html#accountPatchReviewDb.url[accountPatchReviewDb.url]
+is set or changed.
+
+To migrate AccountPatchReviewDb:
+
+* Stop Gerrit
+* Configure new value for link:config-gerrit.html#accountPatchReviewDb.url[accountPatchReviewDb.url]
+* Migrate data using this command
+* Start Gerrit
+
+== OPTIONS
+
+-d::
+--site-path::
+	Location of the `gerrit.config` file, and all other per-site
+	configuration data, supporting libraries and log files.
+
+--sourceUrl::
+	Url of source database. Only need to be specified if the source is not H2.
+
+--chunkSize::
+	Chunk size of fetching from source and pushing to target on each time.
+	Defaults to 100000.
+
+== CONTEXT
+This command can only be run on a server which has direct
+connectivity to the database.
+
+== EXAMPLES
+To migrate from H2 to the database specified by
+link:config-gerrit.html#accountPatchReviewDb.url[accountPatchReviewDb.url]
+in gerrit.config:
+
+----
+	$ java -jar gerrit.war MigrateAccountPatchReviewDb -d site_path
+----
+
+== SEE ALSO
+
+* Configuration parameter link:config-gerrit.html#accountPatchReviewDb.url[accountPatchReviewDb.url]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
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-index.txt b/Documentation/pgm-index.txt
index bf6dc57..d61cc0b 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -33,11 +33,17 @@
 version::
 	Display the release version of Gerrit Code Review.
 
+link:pgm-passwd.html[passwd]::
+	Set or reset password in secure.config.
+
 === Transition Utilities
 
 link:pgm-LocalUsernamesToLowerCase.html[LocalUsernamesToLowerCase]::
 	Convert the local username of every account to lower case.
 
+link:pgm-MigrateAccountPatchReviewDb.html[MigrateAccountPatchReviewDb]::
+	Migrates AccountPatchReviewDb from one database backend to another.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index 6aa3a74..9a16cdf 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -1,17 +1,25 @@
 = init
 
 == NAME
-init - Initialize a new Gerrit server installation
+init - Initialize a new Gerrit server installation or upgrade an existing
+installation.
 
 == SYNOPSIS
+[verse]
 --
-'java' -jar gerrit.war 'init'
-	-d <SITE_PATH>
-	[--batch]
-	[--no-auto-start]
-	[--list-plugins]
-	[--install-plugin=<PLUGIN_NAME>]
-        [--dev]
+_java_ -jar gerrit.war _init_
+  -d <SITE_PATH>
+  [--batch]
+  [--delete-caches]
+  [--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
@@ -19,19 +27,25 @@
 for some basic setup prior to writing default configuration files
 into a newly created `$site_path`.
 
-If run in an existing `$site_path`, init will upgrade some resources
-as necessary.
+If run in an existing `$site_path`, init upgrades existing resources
+(e.g. DB schema, plugins) as necessary.
 
 == OPTIONS
+-b::
 --batch::
-	Run in batch mode, skipping interactive prompts.  Reasonable
-	configuration defaults are chosen based on the whims of
-	the Gerrit developers.
+	Run in batch mode, skipping interactive prompts. For a fresh
+	install, reasonable configuration defaults are chosen based
+	on the whims of the Gerrit developers. On upgrades, the existing
+	settings in `gerrit.config` are respected.
 +
 If during a schema migration unused objects (e.g. tables, columns)
-are detected they are *not* automatically dropped, but only a list of
-SQL statements to drop these objects is provided. To drop the unused
-objects these SQL statements have to be executed manually.
+are detected, they are *not* automatically dropped; a list of SQL
+statements to drop these objects is provided. To drop the unused
+objects these SQL statements must be executed manually.
+
+--delete-caches::
+	Force deletion of all persistent cache files. Note that
+	re-creation of these caches may be expensive.
 
 --no-auto-start::
 	Don't automatically start the daemon after initializing a
@@ -41,21 +55,50 @@
 
 -d::
 --site-path::
-	Location of the gerrit.config file, and all other per-site
+	Location of the `gerrit.config` file, and all other per-site
 	configuration data, supporting libraries and log files.
 
+--skip-plugins::
+	Entirely skip installation and initialization of plugins. This option
+	is needed when initializing a gerrit site without an archive. That
+	happens when running gerrit acceptance or integration tests in a
+	debugger, using classes. Supplying this option leads to ignoring the
+	`--install-plugin` and `--install-all-plugins` options, if supplied as well.
+
 --list-plugins::
 	Print names of plugins that can be installed during init process.
 
+--install-all-plugins::
+	Automatically install all plugins from gerrit.war without asking.
+	This option also works in batch mode. This option cannot be supplied
+	alongside `--install-plugin`.
+
+--secure-store-lib::
+	Path to the jar providing the chosen
+	link:dev-plugins.html#secure-store[SecureStore] implementation class.
+	This option is used in the same way as the `--new-secure-store-lib` option
+	documented in link:pgm-SwitchSecureStore.html[SwitchSecureStore].
+
 --install-plugin::
 	Automatically install plugin with given name without asking.
-	This option may be supplied more than once to install multiple
-	plugins.
+	This option also works in batch mode. This option may be supplied
+	more than once to install multiple plugins. This option cannot be
+	supplied alongside `--install-all-plugins`.
 
 --dev::
 	Install in developer mode. Default configuration settings are
 	chosen to run the Gerrit server as a developer.
 
+--skip-all-downloads::
+	Do not automatically download and install required libraries. The
+	administrator must manually install the required libraries in the `lib/`
+	folder.
+
+--skip-download::
+	Do not automatically download and install the library with the given name.
+	The administrator must manually install the required library in the `lib/`
+	folder.
+
 == CONTEXT
 This command can only be run on a server which has direct
 connectivity to the metadata database, and local access to the
diff --git a/Documentation/pgm-passwd.txt b/Documentation/pgm-passwd.txt
new file mode 100644
index 0000000..133fb03
--- /dev/null
+++ b/Documentation/pgm-passwd.txt
@@ -0,0 +1,49 @@
+= passwd
+
+== NAME
+passwd - Set or reset password in secure.config.
+
+== SYNOPSIS
+[verse]
+--
+_java_ -jar gerrit.war _passwd_
+  -d <SITE_PATH>
+  <SECTION.KEY>
+  [PASSWORD]
+
+--
+
+== DESCRIPTION
+Set or reset password in an existing Gerrit server installation,
+interactively prompting for a new password or using the one
+provided in the command line argument.
+
+== OPTIONS
+
+-d::
+--site-path::
+	Location of the `gerrit.config` file, and all other per-site
+	configuration data, supporting libraries and log files.
+
+== ARGUMENTS
+
+SECTION.KEY::
+	Section and key in the `secure.config` file for setting or editing the
+	password value.
+
+PASSWORD::
+	New password to set in `secure.config` associated to the section and key.
+	When specified as argument, automatically implies batch mode and the command
+	would not ask anything interactively.
+
+== CONTEXT
+
+This utility is typically useful when a secure store is configured
+to encrypt password values and thus editing the file manually is not an option.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
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 bf09e0c..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,19 @@
 --threads::
 	Number of threads to use for indexing.
 
---schema-version::
+--changes-schema-version::
 	Schema version to reindex; default is most recent version.
 
---output::
-	Prefix for output; path for local disk index, or prefix for remote index.
-
 --verbose::
 	Output debug information for each change.
 
+--list::
+	List available index names.
+
+--index::
+	Reindex only index with given name. This option can be supplied
+	more than once to reindex multiple indices.
+
 == CONTEXT
 The secondary index must be enabled. See
 link:config-gerrit.html#index.type[index.type].
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 6d7c6d0..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
@@ -53,7 +53,9 @@
 
 The method Gerrit uses to submit a change to a project can be
 modified by any project owner through the project console, `Projects` >
-`List` > my/project.  The following methods are supported:
+`List` > my/project. In general, a submitted change is only merged if all
+its dependencies are also submitted, with exceptions documented below.
+The following submit types are supported:
 
 [[fast_forward_only]]
 * Fast Forward Only
@@ -96,10 +98,12 @@
 is also set to the submitter, while the author header retains the
 original patch set author.
 +
-Note that Gerrit ignores patch set dependencies when operating in
-cherry-pick mode. Submitters must remember to submit changes in
-the right order since inter-change dependencies will not be
-enforced for them.
+Note that Gerrit ignores dependencies between changes when using this
+submit type unless
+link:config-gerrit.html#change.submitWholeTopic[`change.submitWholeTopic`]
+is enabled and depending changes share the same topic. So generally
+submitters must remember to submit changes in the right order when using this
+submit type.
 
 [[rebase_if_necessary]]
 * Rebase If Necessary
@@ -258,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 cd97223..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
@@ -56,6 +57,9 @@
                       |`current_user(user(anonymous)).`
                       |`current_user(user(peer_daemon)).`
                       |`current_user(user(replication)).`
+
+|`uploader/1`     |`uploader(user(1000000)).`
+    |Uploader as `user(ID)` term. ID is the numeric account ID
 |=============================================================================
 
 In addition Gerrit provides a set of built-in helper predicates that can be used
@@ -94,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 b53da4b..cced53e2 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -17,8 +17,9 @@
 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
-`rules.enabled=false` in the Gerrit config file (see
+[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])
 
 link:https://groups.google.com/d/topic/repo-discuss/wJxTGhlHZMM/discussion[This
@@ -48,6 +49,13 @@
 Prolog based submit type computes a submit type for each change. The computed
 submit type is shown on the change screen for each change.
 
+When submitting changes in a batch using "Submit including ancestors" or "Submit
+whole topic", submit type rules may not be used to mix submit types on a single
+branch, and trying to submit such a batch will fail. This avoids potentially
+confusing behavior and spurious submit failures. It is recommended to only use
+submit type rules to change submit types for an entire branch, which avoids this
+situation.
+
 == Prolog Language
 This document is not a complete Prolog tutorial.
 link:http://en.wikipedia.org/wiki/Prolog[This Wikipedia page on Prolog] is a
@@ -66,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].
 
@@ -85,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
@@ -106,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.
@@ -132,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
@@ -166,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.
@@ -178,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.
@@ -210,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.
@@ -223,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
@@ -233,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>
   |
@@ -248,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`
@@ -260,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.
 
@@ -282,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
@@ -299,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
@@ -339,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
@@ -348,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
@@ -357,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
@@ -373,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
@@ -427,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
@@ -439,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(_)).
@@ -475,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
@@ -496,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`):
@@ -509,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:
@@ -522,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 "
@@ -549,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
@@ -573,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
@@ -585,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
@@ -682,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
@@ -734,7 +760,7 @@
 Which of these two behaviors is desired will always depend on how a particular
 Gerrit server is managed.
 
-==== Example 9: Remove the `Verified` category
+=== Example 9: Remove the `Verified` category
 A project has no build and test. It consists of only text files and needs only
 code review.  We want to remove the `Verified` category from this project so
 that `Code-Review+2` is the only criteria for a change to become submittable.
@@ -802,7 +828,7 @@
     N = label('Non-Author-Code-Review', need(_)).
 
 base(CR) :-
-    gerrit:max_with_block(-2, 2, 'Code-Review', CR),
+    gerrit:max_with_block(-2, 2, 'Code-Review', CR).
 ----
 
 === Example 11: Remove the `Verified` category from all projects
@@ -999,37 +1025,13 @@
 submit_type(fast_forward_only) :-
     gerrit:change_branch(B), regex_matches('refs/heads/stable.*', B),
     !.
-submit_type(T) :- gerrit:project_default_submit_type(T)
+submit_type(T) :- gerrit:project_default_submit_type(T).
 ----
 
 The first `submit_type` predicate defines the `Fast Forward Only` submit type
 for `+refs/heads/stable.*+` branches. The second `submit_type` predicate returns
 the project's default submit type.
 
-=== Example 3: Don't require `Fast Forward Only` if only documentation was changed
-Like in the previous example we want the `Fast Forward Only` submit type for the
-`+refs/heads/stable*+` branches.  However, if only documentation was changed
-(only `+*.txt+` files), then we allow project's default submit type for such
-changes.
-
-`rules.pl`
-[source,prolog]
-----
-submit_type(fast_forward_only) :-
-    gerrit:commit_delta('(?<!\.txt)$'),
-    gerrit:change_branch(B), regex_matches('refs/heads/stable.*', B),
-    !.
-submit_type(T) :- gerrit:project_default_submit_type(T)
-----
-
-The `gerrit:commit_delta('(?<!\.txt)$')` succeeds if the change contains a file
-whose name doesn't end with `.txt` The rest of this rule is same like in the
-previous example.
-
-If all file names in the change end with `.txt`, then the
-`gerrit:commit_delta('(?<!\.txt)$')` will fail as no file name will match this
-regular expression.
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
index fec4a58..baf08e7 100755
--- a/Documentation/replace_macros.py
+++ b/Documentation/replace_macros.py
@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+# coding=utf-8
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -60,9 +61,32 @@
 SEARCH_BOX = """
 
 ++++
-<div style="position:absolute; right:20px; top:20px;">
-<input type="text" id="docSearch" size="70" />
-<button type="button" id="searchBox">Search</button>
+<div style="
+  position:fixed;
+  top:0px;
+  right:0px;
+  text-align:
+  right;
+  padding-top:2px;
+  padding-right:0.5em;
+  padding-bottom:2px;">
+<input size="40"
+  style="line-height: 0.75em;font-size: 0.75em;"
+  id="docSearch"
+  type="text">
+<button style="
+  background:none!important;
+  border:none;
+  padding:0!important;
+  vertical-align:bottom;
+  font-family:'Open Sans','DejaVu Sans',sans-serif;
+  font-size:0.8em;
+  color:#1d4b8f;
+  text-decoration:none;"
+  type="button"
+  id="searchBox">
+  Search
+</button>
 <script type="text/javascript">
 var f = function() {
   window.location = '../#/Documentation/' +
@@ -148,11 +172,14 @@
           a.setAttribute('href', '#' + id);
           a.setAttribute('style', 'position: absolute;'
               + ' left: ' + (element.offsetLeft - 16 - 2 * 4) + 'px;'
-              + ' padding-left: 4px; padding-right: 4px; padding-top:4px;');
-          var img = document.createElement('img');
-          img.setAttribute('src', 'images/link.png');
-          img.setAttribute('style', 'background-color: #FFFFFF;');
-          a.appendChild(img);
+              + ' padding-left: 4px; padding-right: 4px;');
+          var span = document.createElement('span');
+          span.setAttribute('style', 'height: ' + element.offsetHeight + 'px;'
+              + ' display: inline-block; vertical-align: baseline;'
+              + ' font-size: 16px; text-decoration: none; color: grey;');
+          a.appendChild(span);
+          var link = document.createTextNode('🔗');
+          span.appendChild(link);
           element.insertBefore(a, element.firstChild);
 
           // remove the link icon when the mouse is moved away,
@@ -160,14 +187,16 @@
           hide = function(evt) {
             if (document.elementFromPoint(evt.clientX, evt.clientY) != element
                 && document.elementFromPoint(evt.clientX, evt.clientY) != a
-                && document.elementFromPoint(evt.clientX, evt.clientY) != img
+                && document.elementFromPoint(evt.clientX, evt.clientY) != span
+                && document.elementFromPoint(evt.clientX, evt.clientY) != link
                 && element.contains(a)) {
               element.removeChild(a);
             }
           }
           element.onmouseout = hide;
           a.onmouseout = hide;
-          img.onmouseout = hide;
+          span.onmouseout = hide;
+          link.onmouseout = hide;
         }
       }
     }
diff --git a/Documentation/rest-api-access.txt b/Documentation/rest-api-access.txt
index ee3e8ce..61ea582 100644
--- a/Documentation/rest-api-access.txt
+++ b/Documentation/rest-api-access.txt
@@ -334,10 +334,10 @@
 |`force`        |not set if `false`|
 Whether the force flag is set.
 |`min`          |
-not set if range if empty (from `0` to `0`) or not set|
+not set if range is empty (from `0` to `0`) or not set|
 The min value of the permission range.
 |`max`          |
-not set if range if empty (from `0` to `0`) or not set|
+not set if range is empty (from `0` to `0`) or not set|
 The max value of the permission range.
 |==================================
 
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 5459306..e784c1c 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -7,19 +7,76 @@
 [[account-endpoints]]
 == Account Endpoints
 
-[[suggest-account]]
-=== Suggest Account
+[[query-account]]
+=== Query Account
 --
 'GET /accounts/'
 --
 
-Suggest users for a given query `q` and result limit `n`. If result
-limit is not passed, then the default 10 is used. Returns a list of
-matching link:#account-info[AccountInfo] entities.
+Queries accounts visible to the caller. The
+link:user-search-accounts.html#_search_operators[query string] must be
+provided by the `q` parameter. The `n` parameter can be used to limit
+the returned results.
+
+As result a list of link:#account-info[AccountInfo] entities is
+returned.
 
 .Request
 ----
-  GET /accounts/?q=John HTTP/1.0
+  GET /accounts/?q=name:John+email:example.com&n=2 HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "_account_id": 1000096,
+    },
+    {
+      "_account_id": 1001439,
+      "_more_accounts": true
+    }
+  ]
+----
+
+If the number of accounts matching the query exceeds either the
+internal limit or a supplied `n` query parameter, the last account
+object has a `_more_accounts: true` JSON field set.
+
+The `S` or `start` query parameter can be supplied to skip a number
+of accounts from the list.
+
+Additional fields can be obtained by adding `o` parameters, each
+option slows down the query response time to the client so they are
+generally disabled by default. Optional fields are:
+
+[[details]]
+--
+* `DETAILS`: Includes full name, preferred email, username and avatars
+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] and
+link:#all-emails[all emails] are always returned.
+
+.Request
+----
+  GET /accounts/?suggest&q=John HTTP/1.0
 ----
 
 .Response
@@ -420,6 +477,43 @@
   HTTP/1.1 204 No Content
 ----
 
+[[get-oauth-token]]
+=== Get OAuth Access Token
+--
+'GET /accounts/link:#account-id[\{account-id\}]/oauthtoken'
+--
+
+Returns a previously obtained OAuth access token.
+
+.Request
+----
+  GET /accounts/self/oauthtoken HTTP/1.1
+----
+
+As a response, an link:#oauth-token-info[OAuthTokenInfo] entity is returned
+that describes the OAuth access token.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+   )]}'
+    {
+      "username": "johndow",
+      "resource_host": "gerrit.example.org",
+      "access_token": "eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOi",
+      "provider_id": "oauth-plugin:oauth-provider",
+      "expires_at": "922337203775807",
+      "type": "bearer"
+    }
+----
+
+If there is no token available, or the token has already expired,
+"`404 Not Found`" is returned as response. Requests to obtain an access
+token of another user are rejected with "`403 Forbidden`".
+
 [[list-account-emails]]
 === List Account Emails
 --
@@ -500,12 +594,16 @@
 configured, the added email address must belong to a domain that is
 allowed, unless `no_confirmation` is set.
 
-In the request body additional data for the email address can be
-provided as link:#email-input[EmailInput].
+The link:#email-input[EmailInput] object in the request body may
+contain additional options for the email address.
 
 .Request
 ----
   PUT /accounts/self/emails/john.doe@example.com HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+  Content-Length: 3
+
+  {}
 ----
 
 As response the new email address is returned as
@@ -641,6 +739,9 @@
 
 The SSH public key must be provided as raw content in the request body.
 
+Trying to add an SSH key that already exists succeeds, but no new SSH
+key is persisted.
+
 .Request
 ----
   POST /accounts/self/sshkeys HTTP/1.0
@@ -1086,6 +1187,14 @@
 As result the account preferences of the user are returned as a
 link:#preferences-info[PreferencesInfo] entity.
 
+Users may only retrieve the preferences for their own account,
+unless they are an
+link:access-control.html#administrators[Administrator] or a member
+of a group that is granted the
+link:access-control.html#capability_modifyAccount[ModifyAccount]
+capability, in which case they can retrieve the preferences for
+any account.
+
 .Response
 ----
   HTTP/1.1 200 OK
@@ -1097,11 +1206,13 @@
     "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": "ABBREV",
-    "diff_view": "SIDE_BY_SIDE",
+    "mute_common_path_prefixes": true,
     "my": [
       {
         "url": "#/dashboard/self",
@@ -1151,11 +1262,13 @@
     "changes_per_page": 50,
     "show_site_header": true,
     "use_flash_clipboard": true,
+    "download_command": "CHECKOUT",
     "date_format": "STD",
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
     "review_category_strategy": "NAME",
     "diff_view": "SIDE_BY_SIDE",
+    "mute_common_path_prefixes": true,
     "my": [
       {
         "url": "#/dashboard/self",
@@ -1199,11 +1312,13 @@
     "changes_per_page": 50,
     "show_site_header": true,
     "use_flash_clipboard": true,
+    "download_command": "CHECKOUT",
     "date_format": "STD",
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
     "review_category_strategy": "NAME",
     "diff_view": "SIDE_BY_SIDE",
+    "mute_common_path_prefixes": true,
     "my": [
       {
         "url": "#/dashboard/self",
@@ -1353,6 +1468,7 @@
     "key_map_type": "VIM",
     "tab_size": 4,
     "line_length": 80,
+    "indent_unit": 2,
     "cursor_blink_rate": 530,
     "hide_top_menu": true,
     "show_whitespace_errors": true,
@@ -1384,6 +1500,7 @@
     "key_map_type": "VIM",
     "tab_size": 4,
     "line_length": 80,
+    "indent_unit": 2,
     "cursor_blink_rate": 530,
     "hide_top_menu": true,
     "show_tabs": true,
@@ -1396,23 +1513,166 @@
   }
 ----
 
-The response is "`204 No Content`"
+As result the new edit preferences of the user are returned as a
+link:#edit-preferences-info[EditPreferencesInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "theme": "ECLIPSE",
+    "key_map_type": "VIM",
+    "tab_size": 4,
+    "line_length": 80,
+    "cursor_blink_rate": 530,
+    "hide_top_menu": true,
+    "show_whitespace_errors": true,
+    "hide_line_numbers": true,
+    "match_brackets": true,
+    "auto_close_brackets": true
+  }
+----
+
+[[get-watched-projects]]
+=== Get Watched Projects
+--
+'GET /accounts/link:#account-id[\{account-id\}]/watched.projects'
+--
+
+Retrieves all projects a user is watching.
+
+.Request
+----
+  GET /a/accounts/self/watched.projects HTTP/1.0
+----
+
+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 1",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    },
+    {
+      "project": "Test Project 2",
+      "filter": "branch:experimental",
+      "notify_all_comments": true,
+      "notify_submitted_changes": true,
+      "notify_abandoned_changes": true
+    }
+  ]
+----
+
+[[set-watched-projects]]
+=== Add/Update a List of Watched Project Entities
+--
+'POST /accounts/link:#account-id[\{account-id\}]/watched.projects'
+--
+
+Add new projects to watch or update existing watched projects.
+Projects that are already watched by a user will be updated with
+the provided configuration. All other projects in the request
+will be watched using the provided configuration. The posted body
+can contain link:#project-watch-info[ProjectWatchInfo] entities.
+Omitted boolean values will be set to false.
+
+.Request
+----
+  POST /a/accounts/self/watched.projects HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  [
+    {
+      "project": "Test Project 1",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    }
+  ]
+----
+
+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 1",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    },
+    {
+      "project": "Test Project 2",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    }
+  ]
+----
+
+[[delete-watched-projects]]
+=== Delete Watched Projects
+--
+'POST /accounts/link:#account-id[\{account-id\}]/watched.projects:delete'
+--
+
+Projects posted to this endpoint will no longer be watched. The posted body
+can contain a list of link:#project-watch-info[ProjectWatchInfo] entities.
+
+.Request
+----
+  POST /a/accounts/self/watched.projects:delete HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  [
+    {
+      "project": "Test Project 1",
+      "filter": "branch:master"
+    }
+  ]
+----
 
 .Response
 ----
   HTTP/1.1 204 No Content
 ----
 
-[[get-starred-changes]]
-=== Get Starred Changes
+[[default-star-endpoints]]
+== Default Star Endpoints
+
+[[get-changes-with-default-star]]
+=== Get Changes With Default Star
 --
 'GET /accounts/link:#account-id[\{account-id\}]/starred.changes'
 --
 
-Gets the changes starred by the identified user account. This
-URL endpoint is functionally identical to the changes query
-`GET /changes/?q=is:starred`. The result is a list of
-link:rest-api-changes.html#change-info[ChangeInfo] entities.
+Gets the changes that were starred with the default star by the
+identified user account. This URL endpoint is functionally identical
+to the changes query `GET /changes/?q=is:starred`. The result is a list
+of link:rest-api-changes.html#change-info[ChangeInfo] entities.
 
 .Request
 ----
@@ -1436,7 +1696,14 @@
       "status": "NEW",
       "created": "2013-02-01 09:59:32.126000000",
       "updated": "2013-02-21 11:16:36.775000000",
+      "starred": true,
+      "stars": [
+        "star"
+      ],
       "mergeable": true,
+      "submittable": false,
+      "insertions": 145,
+      "deletions": 12,
       "_number": 3965,
       "owner": {
         "name": "John Doe"
@@ -1446,14 +1713,15 @@
 ----
 
 [[star-change]]
-=== Star Change
+=== Put Default Star On Change
 --
 'PUT /accounts/link:#account-id[\{account-id\}]/starred.changes/link:rest-api-changes.html#change-id[\{change-id\}]'
 --
 
-Star a change. Starred changes are returned for the search query
-`is:starred` or `starredby:USER` and automatically notify the user
-whenever updates are made to the change.
+Star a change with the default label. Changes starred with the default
+label are returned for the search query `is:starred` or `starredby:USER`
+and automatically notify the user whenever updates are made to the
+change.
 
 .Request
 ----
@@ -1466,12 +1734,12 @@
 ----
 
 [[unstar-change]]
-=== Unstar Change
+=== Remove Default Star From Change
 --
 'DELETE /accounts/link:#account-id[\{account-id\}]/starred.changes/link:rest-api-changes.html#change-id[\{change-id\}]'
 --
 
-Unstar a change. Removes the starred flag, stopping notifications.
+Remove the default star label from a change. This stops notifications.
 
 .Request
 ----
@@ -1483,6 +1751,214 @@
   HTTP/1.1 204 No Content
 ----
 
+[[star-endpoints]]
+== Star Endpoints
+
+[[get-starred-changes]]
+=== Get Starred Changes
+--
+'GET /accounts/link:#account-id[\{account-id\}]/stars.changes'
+--
+
+Gets the changes that were starred with any label by the identified
+user account. This URL endpoint is functionally identical to the
+changes query `GET /changes/?q=has:stars`. The result is a list of
+link:rest-api-changes.html#change-info[ChangeInfo] entities.
+
+.Request
+----
+  GET /a/accounts/self/stars.changes
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "project": "myProject",
+      "branch": "master",
+      "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "subject": "Implementing Feature X",
+      "status": "NEW",
+      "created": "2013-02-01 09:59:32.126000000",
+      "updated": "2013-02-21 11:16:36.775000000",
+      "stars": [
+        "ignore",
+        "risky"
+      ],
+      "mergeable": true,
+      "submittable": false,
+      "insertions": 145,
+      "deletions": 12,
+      "_number": 3965,
+      "owner": {
+        "name": "John Doe"
+      }
+    }
+  ]
+----
+
+[[get-stars]]
+=== Get Star Labels From Change
+--
+'GET /accounts/link:#account-id[\{account-id\}]/stars.changes/link:rest-api-changes.html#change-id[\{change-id\}]'
+--
+
+Get star labels from a change.
+
+.Request
+----
+  GET /a/accounts/self/stars.changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
+----
+
+As response the star labels that the user applied on the change are
+returned. The labels are lexicographically sorted.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    "blue",
+    "green",
+    "red"
+  ]
+----
+
+[[set-stars]]
+=== Update Star Labels On Change
+--
+'POST /accounts/link:#account-id[\{account-id\}]/stars.changes/link:rest-api-changes.html#change-id[\{change-id\}]'
+--
+
+Update star labels on a change. The star labels to be added/removed
+must be specified in the request body as link:#stars-input[StarsInput]
+entity. Starred changes are returned for the search query `has:stars`.
+
+.Request
+----
+  POST /a/accounts/self/stars.changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "add": [
+      "blue",
+      "red"
+    ],
+    "remove": [
+      "yellow"
+    ]
+  }
+----
+
+As response the star labels that the user applied on the change are
+returned. The labels are lexicographically sorted.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    "blue",
+    "green",
+    "red"
+  ]
+----
+
+[[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"
+----
+
+[[index-account]]
+=== Index Account
+--
+'POST /accounts/link:#account-id[\{account-id\}]/index'
+--
+
+Adds or updates the account in the secondary index.
+
+.Request
+----
+  POST /accounts/1000096/index HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
 [[ids]]
 == IDs
 
@@ -1529,7 +2005,7 @@
 
 [[account-detail-info]]
 === AccountDetailInfo
-The `AccountDetailInfo` entity contains detailled information about an
+The `AccountDetailInfo` entity contains detailed information about an
 account.
 
 `AccountDetailInfo` has the same fields as link:#account-info[
@@ -1548,20 +2024,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. +
-Only set if link:rest-api-changes.html#detailed-accounts[detailed
-account information] is requested.
-|`email`       |optional|
+|===============================
+|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|
 The email address the user prefers to be contacted through. +
-Only set if link:rest-api-changes.html#detailed-accounts[detailed
-account information] is requested.
-|`username`    |optional|The username of the user. +
-Only set if link:rest-api-changes.html#detailed-accounts[detailed
-account information] is requested.
-|===========================
+Only set if detailed account information is requested. +
+See option link:rest-api-changes.html#detailed-accounts[
+DETAILED_ACCOUNTS] for change queries +
+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`|
+Whether the query would deliver more results if not limited. +
+Only set on the last account that is returned.
+|===============================
 
 [[account-input]]
 === AccountInput
@@ -1655,6 +2145,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
@@ -1666,8 +2181,8 @@
 |`context`                     ||
 The number of lines of context when viewing a patch.
 |`theme`                       ||
-The CodeMirror theme. Currently only a subset of light and dark
-CodeMirror themes are supported.
+The CodeMirror theme name in upper case, for example `DEFAULT`. All the themes
+from the CodeMirror release that Gerrit is using are available.
 |`expand_all_comments`         |not set if `false`|
 Whether all inline comments should be automatically expanded.
 |`ignore_whitespace`           ||
@@ -1784,16 +2299,17 @@
 |===========================================
 |Field Name                    ||Description
 |`theme`                       ||
-The CodeMirror theme. Currently only a subset of light and dark
-CodeMirror themes are supported. Light themes `DEFAULT`, `ECLIPSE`,
-`ELEGANT`, `NEAT`. Dark themes `MIDNIGHT`, `NIGHT`, `TWILIGHT`.
+The CodeMirror theme name in upper case, for example `DEFAULT`. All the themes
+from the CodeMirror release that Gerrit is using are available.
 |`key_map_type`                ||
 The CodeMirror key map. Currently only a subset of key maps are
-supported: `DEFAULT`, `EMACS`, `VIM`.
+supported: `DEFAULT`, `EMACS`, `SUBLIME`, `VIM`.
 |`tab_size`                    ||
 Number of spaces that should be used to display one tab.
 |`line_length`                 ||
 Number of characters that should be displayed per line.
+|`indent_unit`                 ||
+Number of spaces that should be used for auto-indent.
 |`cursor_blink_rate`           ||
 Half-period in milliseconds used for cursor blinking.
 Setting it to 0 disables cursor blinking.
@@ -1906,6 +2422,22 @@
 password is deleted.
 |============================
 
+[[oauth-token-info]]
+=== OAuthTokenInfo
+The `OAuthTokenInfo` entity contains information about an OAuth access token.
+
+[options="header",cols="1,^1,5"]
+|========================
+|Field Name      ||Description
+|`username`      ||The owner of the OAuth access token.
+|`resource_host` ||The host of the Gerrit instance.
+|`access_token`  ||The actual token value.
+|`provider_id`   |optional|
+The identifier of the OAuth provider in the form `plugin-name:provider-name`.
+|`expires_at`    |optional|Time of expiration of this token in milliseconds.
+|`type`          ||The type of the OAuth access token, always `bearer`.
+|========================
+
 [[preferences-info]]
 === PreferencesInfo
 The `PreferencesInfo` entity contains information about a user's preferences.
@@ -1920,7 +2452,7 @@
 Whether the site header should be shown.
 |`use_flash_clipboard`          |not set if `false`|
 Whether to use the flash clipboard widget.
-|`download_scheme`              ||
+|`download_scheme`              |optional|
 The type of download URL the user prefers to use. May be any key from
 the `schemes` map in
 link:rest-api-config.html#download-info[DownloadInfo].
@@ -1942,6 +2474,9 @@
 Whether to show change number in the change table.
 |`mute_common_path_prefixes`    |not set if `false`|
 Whether to mute common path prefixes in file names in the file table.
+|`signed_off_by`                |not set if `false`|
+Whether to insert Signed-off-by footer in changes created with the
+inline edit feature.
 |`review_category_strategy`     ||
 The strategy used to displayed info in the review category column.
 Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
@@ -1954,6 +2489,12 @@
 |`url_aliases`                  |optional|
 A map of URL path pairs, where the first URL path is an alias for the
 second URL path.
+|`email_strategy`               ||
+The type of email strategy to use. On `ENABLED`, the user will receive emails
+from Gerrit. On `CC_ON_OWN_COMMENTS` the user will also receive emails for
+their own comments. On `DISABLED` the user will not receive any email
+notifications from Gerrit.
+Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `DISABLED`.
 |============================================
 
 [[preferences-input]]
@@ -1991,6 +2532,9 @@
 Whether to show change number in the change table.
 |`mute_common_path_prefixes`    |optional|
 Whether to mute common path prefixes in file names in the file table.
+|`signed_off_by`                |optional|
+Whether to insert Signed-off-by footer in changes created with the
+inline edit feature.
 |`review_category_strategy`     |optional|
 The strategy used to displayed info in the review category column.
 Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
@@ -2003,6 +2547,12 @@
 |`url_aliases`                  |optional|
 A map of URL path pairs, where the first URL path is an alias for the
 second URL path.
+|`email_strategy`               |optional|
+The type of email strategy to use. On `ENABLED`, the user will receive emails
+from Gerrit. On `CC_ON_OWN_COMMENTS` the user will also receive emails for
+their own comments. On `DISABLED` the user will not receive any email
+notifications from Gerrit.
+Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `DISABLED`.
 |============================================
 
 [[query-limit-info]]
@@ -2033,6 +2583,18 @@
 |`valid`         ||Whether the SSH key is valid.
 |=============================
 
+[[stars-input]]
+=== StarsInput
+The `StarsInput` entity contains star labels that should be added to
+or removed from a change.
+
+[options="header",cols="1,^1,5"]
+|========================
+|Field Name ||Description
+|`add`      |optional|List of labels to add to the change.
+|`remove`   |optional|List of labels to remove from the change.
+|========================
+
 [[username-input]]
 === UsernameInput
 The `UsernameInput` entity contains information for setting the
@@ -2044,6 +2606,22 @@
 |`username` |The new username of the account.
 |=======================
 
+[[project-watch-info]]
+=== ProjectWatchInfo
+The `WatchedProjectsInfo` entity contains information about a project watch
+for a user.
+
+[options="header",cols="1,^1,5"]
+|=======================
+|Field Name                 |        |Description
+|`project`                  |        |The name of the project.
+|`filter`                   |optional|A filter string to be applied to the project.
+|`notify_new_changes`       |optional|Notify on new changes.
+|`notify_new_patch_sets`    |optional|Notify on new patch sets.
+|`notify_all_comments`      |optional|Notify on comments.
+|`notify_submitted_changes` |optional|Notify on submitted changes.
+|`notify_abandoned_changes` |optional|Notify on abandoned changes.
+|=======================
 
 GERRIT
 ------
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index aa9417b..77feb18 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -13,10 +13,8 @@
 'POST /changes'
 --
 
-The change info link:#change-info[ChangeInfo] entity must be provided in the
-request body. Only the following attributes are honored: `project`,
-`branch`, `subject`, `status` and `topic`. The first three attributes are
-mandatory. Valid values for status are: `DRAFT` and `NEW`.
+The change input link:#change-input[ChangeInput] entity must be provided in the
+request body.
 
 .Request
 ----
@@ -130,8 +128,8 @@
   ]
 ----
 
-If the `n` query parameter is supplied and additional changes exist
-that match the query beyond the end, the last change object has a
+If the number of changes matching the query exceeds either the internal
+limit or a supplied `n` query parameter, the last change object has a
 `_more_changes: true` JSON field set.
 
 The `S` or `start` query parameter can be supplied to skip a number
@@ -194,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
@@ -209,8 +208,8 @@
 --
 * `DETAILED_LABELS`: detailed label information, including numeric
   values of all existing approvals, recognized label values, values
-  permitted to be set by the current user, and reviewers that may be
-  removed by the current user.
+  permitted to be set by the current user, all reviewers by state, and
+  reviewers that may be removed by the current user.
 --
 
 [[current-revision]]
@@ -266,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.
@@ -355,6 +360,7 @@
       "current_revision": "184ebe53805e102605d11f6b143486d15c23a09c",
       "revisions": {
         "184ebe53805e102605d11f6b143486d15c23a09c": {
+          "kind": "REWORK",
           "_number": 1,
           "ref": "refs/changes/97/97/1",
           "fetch": {
@@ -414,35 +420,42 @@
           "files": {
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java": {
               "lines_deleted": 8,
-              "size_delta": -412
+              "size_delta": -412,
+              "size": 7782
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java": {
               "lines_inserted": 1,
-              "size_delta": 23
+              "size_delta": 23,
+              "size": 6762
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java": {
               "lines_inserted": 11,
               "lines_deleted": 19,
-              "size_delta": -298
+              "size_delta": -298,
+              "size": 47023
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java": {
               "lines_inserted": 23,
               "lines_deleted": 20,
-              "size_delta": 132
+              "size_delta": 132,
+              "size": 17727
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java": {
               "status": "D",
               "lines_deleted": 139,
-              "size_delta": -5512
+              "size_delta": -5512,
+              "size": 13098
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java": {
               "status": "A",
               "lines_inserted": 204,
-              "size_delta": 8345
+              "size_delta": 8345,
+              "size": 8345
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java": {
               "lines_deleted": 9,
-              "size_delta": -343
+              "size_delta": -343,
+              "size": 5385
             }
           }
         }
@@ -505,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
@@ -634,6 +647,72 @@
         "username": "jroe"
       }
     ],
+    "reviewers": {
+      "REVIEWER": [
+        {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com",
+          "username": "jdoe"
+        },
+        {
+          "_account_id": 1000097,
+          "name": "Jane Roe",
+          "email": "jane.roe@example.com",
+          "username": "jroe"
+        }
+      ]
+    },
+    "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",
@@ -912,6 +991,7 @@
     "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
     "revisions": {
       "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
+        "kind": "REWORK",
         "_number": 2,
         "ref": "refs/changes/99/4799/2",
         "fetch": {
@@ -959,6 +1039,84 @@
   The change could not be rebased due to a path conflict during merge.
 ----
 
+[[move-change]]
+=== Move Change
+--
+'POST /changes/link:#change-id[\{change-id\}]/move'
+--
+
+Move a change.
+
+The destination branch must be provided in the request body inside a
+link:#move-input[MoveInput] entity.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/move HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "destination_branch" : "release-branch"
+  }
+
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the moved change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "myProject~release-branch~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "release-branch",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "mergeable": true,
+    "insertions": 2,
+    "deletions": 13,
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
+
+If the change cannot be moved because the change state doesn't
+allow moving the change, the response is "`409 Conflict`" and
+the error message is contained in the response body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Disposition: attachment
+  Content-Type: text/plain; charset=UTF-8
+
+  change is merged
+----
+
+If the change cannot be moved because the user doesn't have
+abandon permission on the change or upload permission on the destination,
+the response is "`409 Conflict`" and the error message is contained in the
+response body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Disposition: attachment
+  Content-Type: text/plain; charset=UTF-8
+
+  move not permitted
+----
+
 [[revert-change]]
 === Revert Change
 --
@@ -972,7 +1130,7 @@
 
 .Request
 ----
-  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revert HTTP/1.0
+  POST /changes/myProject~master~I1ffe09a505e25f15ce1521bcfb222e51e62c2a14/revert HTTP/1.0
 ----
 
 As response a link:#change-info[ChangeInfo] entity is returned that
@@ -1026,8 +1184,7 @@
 Submits a change.
 
 The request body only needs to include a link:#submit-input[
-SubmitInput] entity if the request should wait for the merge to
-complete.
+SubmitInput] entity if submitting on behalf of another user.
 
 .Request
 ----
@@ -1035,7 +1192,7 @@
   Content-Type: application/json; charset=UTF-8
 
   {
-    "wait_for_merge": true
+    "on_behalf_of": 1001439
   }
 ----
 
@@ -1058,6 +1215,7 @@
     "status": "MERGED",
     "created": "2013-02-01 09:59:32.126000000",
     "updated": "2013-02-21 11:16:36.775000000",
+    "submitted": "2013-02-21 11:16:36.615000000",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -1079,29 +1237,15 @@
 ----
 
 [[submitted-together]]
-=== Changes submitted together
+=== Changes Submitted Together
 --
-'GET /changes/link:#change-id[\{change-id\}]/submitted_together'
+'GET /changes/link:#change-id[\{change-id\}]/submitted_together?o=NON_VISIBLE_CHANGES'
 --
 
-Returns a list of all changes which are submitted when
-link:#submit-change[\{submit\}] is called for this change,
+Computes list of all changes which are submitted when
+link:#submit-change[Submit] is called for this change,
 including the current change itself.
 
-An empty list is returned if this change will be submitted
-by itself (no other changes).
-
-.Request
-----
-  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/submitted_together HTTP/1.0
-  Content-Type: application/json; charset=UTF-8
-----
-
-The return value is a list of changes in the same format as in
-link:#list-changes[\{listing changes\}] with the options
-link:#labels[\{LABELS\}], link:#detailed-labels[\{DETAILED_LABELS\}],
-link:#current-revision[\{CURRENT_REVISION\}],
-link:#current-commit[\{CURRENT_COMMIT\}] set.
 The list consists of:
 
 * The given change.
@@ -1110,6 +1254,27 @@
 * For each change whose submit type is not CHERRY_PICK, include unmerged
   ancestors targeting the same branch.
 
+As a special case, the list is empty if this change would be
+submitted by itself (without other changes).
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/submitted_together?o=NON_VISIBLE_CHANGES HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+----
+
+As a response a link:#submitted-together-info[SubmittedTogetherInfo]
+entity is returned that describes what would happen if the change were
+submitted. This response contains a list of changes and a count of
+changes that are not visible to the caller that are part of the set of
+changes to be merged.
+
+The listed changes use the same format as in
+link:#list-changes[Query Changes] with the
+link:#labels[`LABELS`], link:#detailed-labels[`DETAILED_LABELS`],
+link:#current-revision[`CURRENT_REVISION`], and
+link:#current-commit[`CURRENT_COMMIT`] options set.
+
 .Response
 ----
   HTTP/1.1 200 OK
@@ -1117,232 +1282,257 @@
   Content-Type: application/json; charset=UTF-8
 
 )]}'
-[
-  {
-    "id": "gerrit~master~I1ffe09a505e25f15ce1521bcfb222e51e62c2a14",
-    "project": "gerrit",
-    "branch": "master",
-    "hashtags": [],
-    "change_id": "I1ffe09a505e25f15ce1521bcfb222e51e62c2a14",
-    "subject": "ChangeMergeQueue: Rewrite such that it works on set of changes",
-    "status": "NEW",
-    "created": "2015-05-01 15:39:57.979000000",
-    "updated": "2015-05-20 19:25:21.592000000",
-    "mergeable": true,
-    "insertions": 303,
-    "deletions": 210,
-    "_number": 1779,
-    "owner": {
-      "_account_id": 1000000
-    },
-    "labels": {
-      "Code-Review": {
-        "approved": {
-          "_account_id": 1000000
-        },
-        "all": [
-          {
-            "value": 2,
-            "date": "2015-05-20 19:25:21.592000000",
-            "_account_id": 1000000
-          }
-        ],
-        "values": {
-          "-2": "This shall not be merged",
-          "-1": "I would prefer this is not merged as is",
-          " 0": "No score",
-          "+1": "Looks good to me, but someone else must approve",
-          "+2": "Looks good to me, approved"
-        },
-        "default_value": 0
-      },
-      "Verified": {
-        "approved": {
-          "_account_id": 1000000
-        },
-        "all": [
-          {
-            "value": 1,
-            "date": "2015-05-20 19:25:21.592000000",
-            "_account_id": 1000000
-          }
-        ],
-        "values": {
-          "-1": "Fails",
-          " 0": "No score",
-          "+1": "Verified"
-        },
-        "default_value": 0
-      }
-    },
-    "permitted_labels": {
-      "Code-Review": [
-        "-2",
-        "-1",
-        " 0",
-        "+1",
-        "+2"
-      ],
-      "Verified": [
-        "-1",
-        " 0",
-        "+1"
-      ]
-    },
-    "removable_reviewers": [
-      {
+{
+  "changes": [
+    {
+      "id": "gerrit~master~I1ffe09a505e25f15ce1521bcfb222e51e62c2a14",
+      "project": "gerrit",
+      "branch": "master",
+      "hashtags": [],
+      "change_id": "I1ffe09a505e25f15ce1521bcfb222e51e62c2a14",
+      "subject": "ChangeMergeQueue: Rewrite such that it works on set of changes",
+      "status": "NEW",
+      "created": "2015-05-01 15:39:57.979000000",
+      "updated": "2015-05-20 19:25:21.592000000",
+      "mergeable": true,
+      "insertions": 303,
+      "deletions": 210,
+      "_number": 1779,
+      "owner": {
         "_account_id": 1000000
-      }
-    ],
-    "current_revision": "9adb9f4c7b40eeee0646e235de818d09164d7379",
-    "revisions": {
-      "9adb9f4c7b40eeee0646e235de818d09164d7379": {
-        "_number": 1,
-        "created": "2015-05-01 15:39:57.979000000",
-        "uploader": {
-          "_account_id": 1000000
-        },
-        "ref": "refs/changes/79/1779/1",
-        "fetch": {},
-        "commit": {
-          "parents": [
+      },
+      "labels": {
+        "Code-Review": {
+          "approved": {
+            "_account_id": 1000000
+          },
+          "all": [
             {
-              "commit": "2d3176497a2747faed075f163707e57d9f961a1c",
-              "subject": "Merge changes from topic \u0027submodule-subscription-tests-and-fixes-3\u0027"
+              "value": 2,
+              "date": "2015-05-20 19:25:21.592000000",
+              "_account_id": 1000000
             }
           ],
-          "author": {
-            "name": "Stefan Beller",
-            "email": "sbeller@google.com",
-            "date": "2015-04-29 21:36:52.000000000",
-            "tz": -420
+          "values": {
+            "-2": "This shall not be merged",
+            "-1": "I would prefer this is not merged as is",
+            " 0": "No score",
+            "+1": "Looks good to me, but someone else must approve",
+            "+2": "Looks good to me, approved"
           },
-          "committer": {
-            "name": "Stefan Beller",
-            "email": "sbeller@google.com",
-            "date": "2015-05-01 00:11:16.000000000",
-            "tz": -420
+          "default_value": 0
+        },
+        "Verified": {
+          "approved": {
+            "_account_id": 1000000
           },
-          "subject": "ChangeMergeQueue: Rewrite such that it works on set of changes",
-          "message": "ChangeMergeQueue: Rewrite such that it works on set of changes\n\nChangeMergeQueue used to work on branches rather than sets of changes.\nThis change is a first step to merge sets of changes (e.g. grouped by a\ntopic and `changes.submitWholeTopic` enabled) in an atomic fashion.\nThis change doesn\u0027t aim to implement these changes, but only as a step\ntowards it.\n\nMergeOp keeps its functionality and behavior as is. A new class\nMergeOpMapper is introduced which will map the set of changes to\nthe set of branches. Additionally the MergeOpMapper is also\nresponsible for the threading done right now, which was part of\nthe ChangeMergeQueue before.\n\nChange-Id: I1ffe09a505e25f15ce1521bcfb222e51e62c2a14\n"
+          "all": [
+            {
+              "value": 1,
+              "date": "2015-05-20 19:25:21.592000000",
+              "_account_id": 1000000
+            }
+          ],
+          "values": {
+            "-1": "Fails",
+            " 0": "No score",
+            "+1": "Verified"
+          },
+          "default_value": 0
+        }
+      },
+      "permitted_labels": {
+        "Code-Review": [
+          "-2",
+          "-1",
+          " 0",
+          "+1",
+          "+2"
+        ],
+        "Verified": [
+          "-1",
+          " 0",
+          "+1"
+        ]
+      },
+      "removable_reviewers": [
+        {
+          "_account_id": 1000000
+        }
+      ],
+      "reviewers": {
+        "REVIEWER": [
+          {
+            "_account_id": 1000000
+          }
+        ]
+      },
+      "current_revision": "9adb9f4c7b40eeee0646e235de818d09164d7379",
+      "revisions": {
+        "9adb9f4c7b40eeee0646e235de818d09164d7379": {
+          "kind": "REWORK",
+          "_number": 1,
+          "created": "2015-05-01 15:39:57.979000000",
+          "uploader": {
+            "_account_id": 1000000
+          },
+          "ref": "refs/changes/79/1779/1",
+          "fetch": {},
+          "commit": {
+            "parents": [
+              {
+                "commit": "2d3176497a2747faed075f163707e57d9f961a1c",
+                "subject": "Merge changes from topic \u0027submodule-subscription-tests-and-fixes-3\u0027"
+              }
+            ],
+            "author": {
+              "name": "Stefan Beller",
+              "email": "sbeller@google.com",
+              "date": "2015-04-29 21:36:52.000000000",
+              "tz": -420
+            },
+            "committer": {
+              "name": "Stefan Beller",
+              "email": "sbeller@google.com",
+              "date": "2015-05-01 00:11:16.000000000",
+              "tz": -420
+            },
+            "subject": "ChangeMergeQueue: Rewrite such that it works on set of changes",
+            "message": "ChangeMergeQueue: Rewrite such that it works on set of changes\n\nChangeMergeQueue used to work on branches rather than sets of changes.\nThis change is a first step to merge sets of changes (e.g. grouped by a\ntopic and `changes.submitWholeTopic` enabled) in an atomic fashion.\nThis change doesn\u0027t aim to implement these changes, but only as a step\ntowards it.\n\nMergeOp keeps its functionality and behavior as is. A new class\nMergeOpMapper is introduced which will map the set of changes to\nthe set of branches. Additionally the MergeOpMapper is also\nresponsible for the threading done right now, which was part of\nthe ChangeMergeQueue before.\n\nChange-Id: I1ffe09a505e25f15ce1521bcfb222e51e62c2a14\n"
+          }
+        }
+      }
+    },
+    {
+      "id": "gerrit~master~I7fe807e63792b3d26776fd1422e5e790a5697e22",
+      "project": "gerrit",
+      "branch": "master",
+      "hashtags": [],
+      "change_id": "I7fe807e63792b3d26776fd1422e5e790a5697e22",
+      "subject": "AbstractSubmoduleSubscription: Split up createSubscription",
+      "status": "NEW",
+      "created": "2015-05-01 15:39:57.979000000",
+      "updated": "2015-05-20 19:25:21.546000000",
+      "mergeable": true,
+      "insertions": 15,
+      "deletions": 6,
+      "_number": 1780,
+      "owner": {
+        "_account_id": 1000000
+      },
+      "labels": {
+        "Code-Review": {
+          "approved": {
+            "_account_id": 1000000
+          },
+          "all": [
+            {
+              "value": 2,
+              "date": "2015-05-20 19:25:21.546000000",
+              "_account_id": 1000000
+            }
+          ],
+          "values": {
+            "-2": "This shall not be merged",
+            "-1": "I would prefer this is not merged as is",
+            " 0": "No score",
+            "+1": "Looks good to me, but someone else must approve",
+            "+2": "Looks good to me, approved"
+          },
+          "default_value": 0
+        },
+        "Verified": {
+          "approved": {
+            "_account_id": 1000000
+          },
+          "all": [
+            {
+              "value": 1,
+              "date": "2015-05-20 19:25:21.546000000",
+              "_account_id": 1000000
+            }
+          ],
+          "values": {
+            "-1": "Fails",
+            " 0": "No score",
+            "+1": "Verified"
+          },
+          "default_value": 0
+        }
+      },
+      "permitted_labels": {
+        "Code-Review": [
+          "-2",
+          "-1",
+          " 0",
+          "+1",
+          "+2"
+        ],
+        "Verified": [
+          "-1",
+          " 0",
+          "+1"
+        ]
+      },
+      "removable_reviewers": [
+        {
+          "_account_id": 1000000
+        }
+      ],
+      "reviewers": {
+        "REVIEWER": [
+          {
+            "_account_id": 1000000
+          }
+        ]
+      },
+      "current_revision": "1bd7c12a38854a2c6de426feec28800623f492c4",
+      "revisions": {
+        "1bd7c12a38854a2c6de426feec28800623f492c4": {
+          "kind": "REWORK",
+          "_number": 1,
+          "created": "2015-05-01 15:39:57.979000000",
+          "uploader": {
+            "_account_id": 1000000
+          },
+          "ref": "refs/changes/80/1780/1",
+          "fetch": {},
+          "commit": {
+            "parents": [
+              {
+                "commit": "9adb9f4c7b40eeee0646e235de818d09164d7379",
+                "subject": "ChangeMergeQueue: Rewrite such that it works on set of changes"
+              }
+            ],
+            "author": {
+              "name": "Stefan Beller",
+              "email": "sbeller@google.com",
+              "date": "2015-04-25 00:11:59.000000000",
+              "tz": -420
+            },
+            "committer": {
+              "name": "Stefan Beller",
+              "email": "sbeller@google.com",
+              "date": "2015-05-01 00:11:16.000000000",
+              "tz": -420
+            },
+            "subject": "AbstractSubmoduleSubscription: Split up createSubscription",
+            "message": "AbstractSubmoduleSubscription: Split up createSubscription\n\nLater we want to have subscriptions to more submodules, so we need to\nfind a way to add more submodule entries into the file. By splitting up\nthe createSubscription() method, that is very easy by using the\naddSubmoduleSubscription method multiple times.\n\nChange-Id: I7fe807e63792b3d26776fd1422e5e790a5697e22\n"
+          }
         }
       }
     }
-  },
-  {
-    "id": "gerrit~master~I7fe807e63792b3d26776fd1422e5e790a5697e22",
-    "project": "gerrit",
-    "branch": "master",
-    "hashtags": [],
-    "change_id": "I7fe807e63792b3d26776fd1422e5e790a5697e22",
-    "subject": "AbstractSubmoduleSubscription: Split up createSubscription",
-    "status": "NEW",
-    "created": "2015-05-01 15:39:57.979000000",
-    "updated": "2015-05-20 19:25:21.546000000",
-    "mergeable": true,
-    "insertions": 15,
-    "deletions": 6,
-    "_number": 1780,
-    "owner": {
-      "_account_id": 1000000
-    },
-    "labels": {
-      "Code-Review": {
-        "approved": {
-          "_account_id": 1000000
-        },
-        "all": [
-          {
-            "value": 2,
-            "date": "2015-05-20 19:25:21.546000000",
-            "_account_id": 1000000
-          }
-        ],
-        "values": {
-          "-2": "This shall not be merged",
-          "-1": "I would prefer this is not merged as is",
-          " 0": "No score",
-          "+1": "Looks good to me, but someone else must approve",
-          "+2": "Looks good to me, approved"
-        },
-        "default_value": 0
-      },
-      "Verified": {
-        "approved": {
-          "_account_id": 1000000
-        },
-        "all": [
-          {
-            "value": 1,
-            "date": "2015-05-20 19:25:21.546000000",
-            "_account_id": 1000000
-          }
-        ],
-        "values": {
-          "-1": "Fails",
-          " 0": "No score",
-          "+1": "Verified"
-        },
-        "default_value": 0
-      }
-    },
-    "permitted_labels": {
-      "Code-Review": [
-        "-2",
-        "-1",
-        " 0",
-        "+1",
-        "+2"
-      ],
-      "Verified": [
-        "-1",
-        " 0",
-        "+1"
-      ]
-    },
-    "removable_reviewers": [
-      {
-        "_account_id": 1000000
-      }
-    ],
-    "current_revision": "1bd7c12a38854a2c6de426feec28800623f492c4",
-    "revisions": {
-      "1bd7c12a38854a2c6de426feec28800623f492c4": {
-        "_number": 1,
-        "created": "2015-05-01 15:39:57.979000000",
-        "uploader": {
-          "_account_id": 1000000
-        },
-        "ref": "refs/changes/80/1780/1",
-        "fetch": {},
-        "commit": {
-          "parents": [
-            {
-              "commit": "9adb9f4c7b40eeee0646e235de818d09164d7379",
-              "subject": "ChangeMergeQueue: Rewrite such that it works on set of changes"
-            }
-          ],
-          "author": {
-            "name": "Stefan Beller",
-            "email": "sbeller@google.com",
-            "date": "2015-04-25 00:11:59.000000000",
-            "tz": -420
-          },
-          "committer": {
-            "name": "Stefan Beller",
-            "email": "sbeller@google.com",
-            "date": "2015-05-01 00:11:16.000000000",
-            "tz": -420
-          },
-          "subject": "AbstractSubmoduleSubscription: Split up createSubscription",
-          "message": "AbstractSubmoduleSubscription: Split up createSubscription\n\nLater we want to have subscriptions to more submodules, so we need to\nfind a way to add more submodule entries into the file. By splitting up\nthe createSubscription() method, that is very easy by using the\naddSubmoduleSubscription method multiple times.\n\nChange-Id: I7fe807e63792b3d26776fd1422e5e790a5697e22\n"
-        }
-      }
-    }
-  }
-]
+  ],
+  "non_visible_changes": 0
+}
 ----
 
+If the `o=NON_VISIBLE_CHANGES` query parameter is not passed, then
+instead of a link:#submitted-together-info[SubmittedTogetherInfo]
+entity, the response is a list of changes, or a 403 response with a
+message if the set of changes to be submitted with this change
+includes changes the caller cannot read.
+
 
 [[publish-draft-change]]
 === Publish Draft Change
@@ -1533,7 +1723,7 @@
 ----
 
 [[check-change]]
-=== Check change
+=== Check Change
 --
 'GET /changes/link:#change-id[\{change-id\}]/check'
 --
@@ -1583,7 +1773,7 @@
 ----
 
 [[fix-change]]
-=== Fix change
+=== Fix Change
 --
 'POST /changes/link:#change-id[\{change-id\}]/check'
 --
@@ -1618,6 +1808,7 @@
     "status": "MERGED",
     "created": "2013-02-01 09:59:32.126000000",
     "updated": "2013-02-21 11:16:36.775000000",
+    "submitted": "2013-02-21 11:16:36.615000000",
     "mergeable": true,
     "insertions": 34,
     "deletions": 101,
@@ -1692,6 +1883,7 @@
        "subject":"Use an EventBus to manage star icons",
        "message":"Use an EventBus to manage star icons\n\nImage widgets that need to ..."
     },
+    "base_revision":"c35558e0925e6985c91f3a16921537d5e572b7a3"
   }
 ----
 
@@ -1835,6 +2027,9 @@
 If only the content type is required, callers should use HEAD to
 avoid downloading the encoded file contents.
 
+If the `base` parameter is set to true, the returned content is from the
+revision that the edit is based on.
+
 .Response
 ----
   HTTP/1.1 200 OK
@@ -1902,6 +2097,9 @@
 
 Retrieves commit message from change edit.
 
+If the `base` parameter is set to true, the returned message is from the
+revision that the edit is based on.
+
 .Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit:message HTTP/1.0
@@ -2068,13 +2266,15 @@
         "_account_id": 1000097,
         "name": "Jane Roe",
         "email": "jane.roe@example.com"
-      }
+      },
+      "count": 1
     },
     {
       "group": {
         "id": "4fd581c0657268f2bdcc26699fbf9ddb76e3a279",
         "name": "Joiner"
-      }
+      },
+      "count": 5
     }
   ]
 ----
@@ -2147,6 +2347,7 @@
   {
     "reviewers": [
       {
+        "input": "john.doe@example.com",
         "approvals": {
           "Verified": " 0",
           "Code-Review": " 0"
@@ -2184,6 +2385,7 @@
 
   )]}'
   {
+    "input": "MyProjectVerifiers",
     "error": "The group My Group has 15 members. Do you want to add them all as reviewers?",
     "confirm": true
   }
@@ -2198,7 +2400,7 @@
   Content-Type: application/json; charset=UTF-8
 
   {
-    "reviewer": "MyProjectVerifiers",
+    "input": "MyProjectVerifiers",
     "confirmed": true
   }
 ----
@@ -2221,6 +2423,74 @@
   HTTP/1.1 204 No Content
 ----
 
+[[list-votes]]
+=== List Votes
+--
+'GET /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/'
+--
+
+Lists the votes for a specific reviewer of the change.
+
+.Request
+----
+  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.
+The entries in the map are sorted by label name.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "Code-Review": -1,
+    "Verified": 1
+    "Work-In-Progress": 1,
+  }
+----
+
+[[delete-vote]]
+=== Delete Vote
+--
+'DELETE /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/link:#label-id[\{label-id\}]'
+'POST /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/link:#label-id[\{label-id\}]/delete'
+--
+
+Deletes a single vote from a change. Note, that even when the last vote of
+a reviewer is removed the reviewer itself is still listed on the change.
+
+Options can be provided in the request body as a
+link:#delete-vote-input[DeleteVoteInput] entity.
+
+.Request
+----
+  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/votes/Code-Review HTTP/1.0
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/votes/Code-Review/delete HTTP/1.0
+----
+
+Please note that some proxies prohibit request bodies for DELETE
+requests. In this case, if you want to specify options, use a POST
+request:
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/votes/Code-Review/delete HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "notify": "NONE"
+  }
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
 [[revision-endpoints]]
 == Revision Endpoints
 
@@ -2434,15 +2704,31 @@
         "email": "jane.roe@example.com"
       }
     ],
+    "reviewers": {
+      "REVIEWER": [
+        {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com"
+        },
+        {
+          "_account_id": 1000097,
+          "name": "Jane Roe",
+          "email": "jane.roe@example.com"
+        }
+      ]
+    },
     "current_revision": "674ac754f91e64a0efb8087e59a176484bd534d1",
     "revisions": {
       "674ac754f91e64a0efb8087e59a176484bd534d1": {
-      "_number": 2,
-      "ref": "refs/changes/65/3965/2",
-      "fetch": {
-        "http": {
-          "url": "http://gerrit/myProject",
-          "ref": "refs/changes/65/3965/2"
+        "kind": "REWORK",
+        "_number": 2,
+        "ref": "refs/changes/65/3965/2",
+        "fetch": {
+          "http": {
+            "url": "http://gerrit/myProject",
+            "ref": "refs/changes/65/3965/2"
+          }
         }
       }
     }
@@ -2541,6 +2827,7 @@
   Content-Type: application/json; charset=UTF-8
 
   {
+    "tag": "jenkins",
     "message": "Some nits need to be fixed.",
     "labels": {
       "Code-Review": -1
@@ -2589,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
 --
@@ -2640,6 +3013,7 @@
     "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
     "revisions": {
       "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
+        "kind": "REWORK",
         "_number": 2,
         "ref": "refs/changes/99/4799/2",
         "fetch": {
@@ -2827,7 +3201,8 @@
   )]}'
   {
     submit_type: "MERGE_IF_NECESSARY",
-    mergeable: true,
+    strategy: "recursive",
+    mergeable: true
   }
 ----
 
@@ -3264,12 +3639,14 @@
     "/COMMIT_MSG": {
       "status": "A",
       "lines_inserted": 7,
-      "size_delta": 551
+      "size_delta": 551,
+      "size": 551
     },
     "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": {
       "lines_inserted": 5,
       "lines_deleted": 3,
-      "size_delta": 98
+      "size_delta": 98,
+      "size": 23348
     }
   }
 ----
@@ -3283,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
@@ -3368,7 +3750,7 @@
 
 .Request
 ----
-  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/website%2Freleases%2Flogo.png/safe_content HTTP/1.0
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/website%2Freleases%2Flogo.png/download HTTP/1.0
 ----
 
 .Response
@@ -3382,7 +3764,7 @@
 
 .Request
 ----
-  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/safe_content?suffix=new HTTP/1.0
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/download?suffix=new HTTP/1.0
 ----
 
 .Response
@@ -3528,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.
 
@@ -3563,12 +3950,73 @@
   }
 ----
 
-The `ignore-whitespace` parameter can be specified to control how whitespace differences are
-reported in the result.  Valid values are `NONE`, `TRAILING`, `CHANGED` or `ALL`.
+The `whitespace` parameter can be specified to control how whitespace
+differences are reported in the result.  Valid values are `IGNORE_NONE`,
+`IGNORE_TRAILING`, `IGNORE_LEADING_AND_TRAILING` or `IGNORE_ALL`.
 
 The `context` parameter can be specified to control the number of lines of surrounding context
 in the diff.  Valid values are `ALL` or number of lines.
 
+[[get-blame]]
+=== Get Blame
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/blame'
+--
+
+Gets the blame of a file from a certain revision.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/blame HTTP/1.0
+----
+
+As response a link:#blame-info[BlameInfo] entity is returned that describes the
+blame.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]
+  {
+    [
+      {
+        "author": "Joe Daw",
+        "id": "64e140b4de5883a4dd74d06c2b62ccd7ffd224a7",
+        "time": 1421441349,
+        "commit_msg": "RST test\n\nChange-Id: I11e9e24bd122253f4bb10c36dce825ac2410d646\n",
+        "ranges": [
+          {
+            "start": 1,
+            "end": 10
+          },
+          {
+            "start": 16,
+            "end": 296
+          }
+        ]
+      },
+      {
+        "author": "Jane Daw",
+        "id": "8d52621a0e2ac6adec73bd3a49f2371cd53137a7",
+        "time": 1421825421,
+        "commit_msg": "add banner\n\nChange-Id: I2eced9b2691015ae3c5138f4d0c4ca2b8fb15be9\n",
+        "ranges": [
+          {
+            "start": 13,
+            "end": 13
+          }
+        ]
+      }
+    ]
+  }
+----
+
+The `base` parameter can be specified to control the base patch set from which
+the blame should be generated.
+
 [[set-reviewed]]
 === Set Reviewed
 --
@@ -3688,6 +4136,10 @@
 === \{draft-id\}
 UUID of a draft comment.
 
+[[label-id]]
+=== \{label-id\}
+The name of the label.
+
 [[file-id]]
 \{file-id\}
 ~~~~~~~~~~~~
@@ -3704,6 +4156,7 @@
 * an abbreviated commit ID that uniquely identifies one revision of the
   change ("674ac754"), at least 4 digits are required
 * a legacy numeric patch number ("1" for first patch set of the change)
+* "0" or the literal `edit` for a change edit
 
 [[json-entities]]
 == JSON Entities
@@ -3718,6 +4171,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]]
@@ -3754,9 +4212,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
@@ -3783,6 +4249,27 @@
 permitted to vote on that label.
 |`date`        |optional|
 The time and date describing when the approval was made.
+|`tag`                 |optional|
+Value of the `tag` field from link:#review-input[ReviewInput] set
+while posting the review.
+NOTE: To apply different tags on on different votes/comments multiple
+invocations of the REST call are required.
+|===========================
+
+[[blame-info]]
+=== BlameInfo
+The `BlameInfo` entity stores the commit metadata with the row coordinates where
+it applies.
+
+[options="header",cols="1,6"]
+|===========================
+|Field Name | Description
+|`author`     | The author of the commit.
+|`id`         | The id of the commit.
+|`time`       | Commit time.
+|`commit_msg` | The commit message.
+|`ranges`     |
+The blame row coordinates as link:#range-info[RangeInfo] entities.
 |===========================
 
 [[change-edit-input]]
@@ -3836,11 +4323,20 @@
 |`updated`            ||
 The link:rest-api.html#timestamp[timestamp] of when the change was last
 updated.
+|`submitted`          |only set for merged changes|
+The link:rest-api.html#timestamp[timestamp] of when the change was
+submitted.
 |`starred`            |not set if `false`|
-Whether the calling user has starred this change.
+Whether the calling user has starred this change with the default label.
+|`stars`              |optional|
+A list of star labels that are applied by the calling user to this
+change. The labels are lexicographically sorted.
 |`reviewed`           |not set if `false`|
 Whether the change was reviewed by the calling user.
 Only set if link:#reviewed[reviewed] is requested.
+|`submit_type`        |optional|
+The link:project-configuration.html#submit_type[submit type] of the change. +
+Not set for merged changes.
 |`mergeable`          |optional|
 Whether the change is mergeable. +
 Not set for merged changes, or if the change has not yet been tested.
@@ -3869,6 +4365,20 @@
 The reviewers that can be removed by the calling user as a list of
 link:rest-api-accounts.html#account-info[AccountInfo] entities. +
 Only set if link:#detailed-labels[detailed labels] are requested.
+|`reviewers`          ||
+The reviewers as a map that maps a reviewer state to a list of
+link:rest-api-accounts.html#account-info[AccountInfo] entities.
+Possible reviewer states are `REVIEWER`, `CC` and `REMOVED`. +
+`REVIEWER`: Users with at least one non-zero vote on the change. +
+`CC`: Users that were added to the change, but have not voted. +
+`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. +
@@ -3889,9 +4399,31 @@
 |`problems`           |optional|
 A list of link:#problem-info[ProblemInfo] entities describing potential
 problems with this change. Only set if link:#check[CHECK] is set.
+|==================================
+
+[[change-input]]
+=== ChangeInput
+The `ChangeInput` entity contains information about creating a new change.
+
+[options="header",cols="1,^1,5"]
+|==================================
+|Field Name           ||Description
+|`project`            ||The name of the project.
+|`branch`             ||
+The name of the target branch. +
+The `refs/heads/` prefix is omitted.
+|`subject`            ||
+The subject of the change (header line of the commit message).
+|`topic`              |optional|The topic to which this change belongs.
+|`status`             |optional, default to `NEW`|
+The status of the change (only `NEW` and `DRAFT` accepted here).
 |`base_change`        |optional|
 A link:#change-id[\{change-id\}] that identifies the base change for a create
-change operation. Only used for the link:#create-change[CreateChange] endpoint.
+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]]
@@ -3910,6 +4442,11 @@
 |`date`            ||
 The link:rest-api.html#timestamp[timestamp] this message was posted.
 |`message`            ||The text left by the user.
+|`tag`                 |optional|
+Value of the `tag` field from link:#review-input[ReviewInput] set
+while posting the review.
+NOTE: To apply different tags on on different votes/comments multiple
+invocations of the REST call are required.
 |`_revision_number`    |optional|
 Which patchset (if any) generated this message.
 |==================================
@@ -3943,6 +4480,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. +
@@ -3960,6 +4500,11 @@
 The author of the message as an
 link:rest-api-accounts.html#account-info[AccountInfo] entity. +
 Unset for draft comments, assumed to be the calling user.
+|`tag`                 |optional|
+Value of the `tag` field from link:#review-input[ReviewInput] set
+while posting the review.
+NOTE: To apply different tags on on different votes/comments multiple
+invocations of the REST call are required.
 |===========================
 
 [[comment-input]]
@@ -3998,6 +4543,10 @@
 The comment message. +
 If not set and an existing draft comment is updated, the existing draft
 comment is deleted.
+|`tag`         |optional, drafts only|
+Value of the `tag` field. Only allowed on link:#create-draft[draft comment] +
+inputs; for published comments, use the `tag` field in +
+link#review-input[ReviewInput]
 |===========================
 
 [[comment-range]]
@@ -4040,6 +4589,24 @@
 link:#web-link-info[WebLinkInfo] entities.
 |===========================
 
+[[delete-vote-input]]
+=== DeleteVoteInput
+The `DeleteVoteInput` entity contains options for the deletion of a
+vote.
+
+[options="header",cols="1,^1,5"]
+|=======================
+|Field Name||Description
+|`label`   |optional|
+The label for which the vote should be deleted. +
+If set, must match the label in the URL.
+|`notify`  |optional|
+Notify handling that defines to whom email notifications should be sent
+after the vote is deleted. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `ALL`.
+|=======================
+
 [[diff-content]]
 === DiffContent
 The `DiffContent` entity contains information about the content differences
@@ -4160,15 +4727,15 @@
 
 [options="header",cols="1,^1,5"]
 |===========================
-|Field Name    ||Description
-|`commit`      ||The commit of change edit as
+|Field Name     ||Description
+|`commit`       ||The commit of change edit as
 link:#commit-info[CommitInfo] entity.
-|`baseRevision`||The revision of the patch set change edit is based on.
-|`fetch`       ||
+|`base_revision`||The revision of the patch set the change edit is based on.
+|`fetch`        ||
 Information about how to fetch this patch set. The fetch information is
 provided as a map that maps the protocol name ("`git`", "`http`",
 "`ssh`") to link:#fetch-info[FetchInfo] entities.
-|`files`       |optional|
+|`files`        |optional|
 The files of the change edit as a map that maps the file names to
 link:#file-info[FileInfo] entities.
 |===========================
@@ -4212,6 +4779,8 @@
 Not set for binary files or if no lines were deleted.
 |`size_delta`    ||
 Number of bytes by which the file size increased/decreased.
+|`size`          ||
+File size in bytes.
 |=============================
 
 [[fix-input]]
@@ -4343,12 +4912,49 @@
 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.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name          ||Description
+|`destination_branch`||Destination branch
+|`message`           |optional|
+A message to be posted in this change's comments
+|===========================
+
 [[problem-info]]
 === ProblemInfo
 The `ProblemInfo` entity contains a description of a potential consistency problem
@@ -4385,6 +4991,17 @@
 link:rest-api-accounts.html#gpg-key-info[GpgKeyInfo] entity.
 |===========================
 
+[[range-info]]
+=== RangeInfo
+The `RangeInfo` entity stores the coordinates of a range.
+
+[options="header",cols="1,6"]
+|===========================
+|Field Name   | Description
+|`start`      | First index.
+|`end`        | Last index.
+|===========================
+
 [[rebase-input]]
 === RebaseInput
 The `RebaseInput` entity contains information for changing parent when rebasing.
@@ -4469,6 +5086,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
@@ -4479,6 +5116,11 @@
 |Field Name               ||Description
 |`message`                |optional|
 The message to be added as review comment.
+|`tag`                    |optional|
+Apply this tag to the review comment message, votes, and inline
+comments. Tags may be used by CI or other automated systems to
+distinguish them from human reviews. Comments with specific tag
+values can be filtered out in the web UI.
 |`labels`                 |optional|
 The votes that should be added to the revision as a map that maps the
 label names to the voting values.
@@ -4545,11 +5187,19 @@
 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
 link:config-gerrit.html#addreviewer.maxWithoutConfirmation[require a
 confirmation] when adding a group as reviewer that has many members.
+|`notify`  |optional|
+Notify handling that defines to whom email notifications should be sent
+after the reviewer is added. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `ALL`.
 |===========================
 
 [[revision-info]]
@@ -4563,6 +5213,8 @@
 |===========================
 |Field Name    ||Description
 |`draft`       |not set if `false`|Whether the patch set is a draft.
+|`kind`        ||The change kind. Valid values are `REWORK`, `TRIVIAL_REBASE`,
+`MERGE_FIRST_PARENT_UPDATE`, `NO_CODE_CHANGE`, and `NO_CHANGE`.
 |`_number`     ||The patch set number.
 |`created`     ||
 The link:rest-api.html#timestamp[timestamp] of when the patch set was
@@ -4633,18 +5285,14 @@
 |Field Name    ||Description
 |`status`      ||
 The status of the change after submitting is `MERGED`.
-+
-As `wait_for_merge` in the link:#submit-input[SubmitInput] is deprecated and
-the request always waits for the merge to be completed, you can expect
-`MERGED` to be returned here.
 |`on_behalf_of`|optional|
 The link:rest-api-accounts.html#account-id[\{account-id\}] of the user on
 whose behalf the action should be done. To use this option the caller must
 have been granted both `Submit` and `Submit (On Behalf Of)` permissions.
 The user named by `on_behalf_of` does not need to be granted the `Submit`
 permission. This feature is aimed for CI solutions: the CI account can be
-granted both permssions, so individual users don't need `Submit` permission
-themselves. Still the changes can be submited on behalf of real users and
+granted both permissions, so individual users don't need `Submit` permission
+themselves. Still the changes can be submitted on behalf of real users and
 not with the identity of the CI account.
 |==========================
 
@@ -4655,8 +5303,17 @@
 [options="header",cols="1,^1,5"]
 |===========================
 |Field Name      ||Description
-|`wait_for_merge`|Deprecated, always `true`|
-Whether the request should wait for the merge to complete.
+|`on_behalf_of`|optional|
+If set, submit the change on behalf of the given user. The value may take any
+format link:rest-api-accounts.html#account-id[accepted by the accounts REST
+API]. Using this option requires
+link:access-control.html#category_submit_on_behalf_of[Submit (On Behalf Of)]
+permission on the branch.
+|`notify`|optional|
+Notify handling that defines to whom email notifications should be sent after
+the change is submitted. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `ALL`.
 |===========================
 
 [[submit-record]]
@@ -4694,6 +5351,23 @@
 the failure of the rule predicate.
 |===========================
 
+[[submitted-together-info]]
+=== SubmittedTogetherInfo
+The `SubmittedTogetherInfo` entity contains information about a
+collection of changes that would be submitted together.
+
+[options="header",cols="1,6"]
+|===========================
+|Field Name           |Description
+|`changes`            |
+A list of ChangeInfo entities representing the changes to be submitted together.
+|`non_visible_changes`|
+The number of changes to be submitted together that the current user
+cannot see. (This count includes changes that are visible to the
+current user when their reason for being submitted together involves
+changes the user cannot see.)
+|===========================
+
 [[suggested-reviewer-info]]
 === SuggestedReviewerInfo
 The `SuggestedReviewerInfo` entity contains information about a reviewer
@@ -4704,6 +5378,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 b1b795c..7b96a1c 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -115,6 +115,7 @@
     "gerrit": {
       "all_projects": "All-Projects",
       "all_users": "All-Users"
+      "doc_search": true
     },
     "sshd": {},
     "suggest": {
@@ -935,6 +936,258 @@
   ]
 ----
 
+[[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
+
+--
+'GET /config/server/preferences.diff'
+--
+
+Returns the default diff preferences for the server.
+
+.Request
+----
+  GET /a/config/server/preferences.diff HTTP/1.0
+----
+
+As response a link:rest-api-accounts.html#diff-preferences-info[
+DiffPreferencesInfo] is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "context": 10,
+    "tab_size": 8,
+    "line_length": 100,
+    "cursor_blink_rate": 0,
+    "intraline_difference": true,
+    "show_line_endings": true,
+    "show_tabs": true,
+    "show_whitespace_errors": true,
+    "syntax_highlighting": true,
+    "auto_hide_diff_table_header": true,
+    "theme": "DEFAULT",
+    "ignore_whitespace": "IGNORE_NONE"
+  }
+----
+
+[[set-diff-preferences]]
+=== Set Default Diff Preferences
+
+--
+'PUT /config/server/preferences.diff'
+--
+
+Sets the default diff preferences for the server.
+
+The new diff preferences must be provided in the request body as a
+link:rest-api-accounts.html#diff-preferences-input[
+DiffPreferencesInput] entity.
+
+To be allowed to set default diff 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.diff HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "context": 10,
+    "tab_size": 8,
+    "line_length": 80,
+    "cursor_blink_rate": 0,
+    "intraline_difference": true,
+    "show_line_endings": true,
+    "show_tabs": true,
+    "show_whitespace_errors": true,
+    "syntax_highlighting": true,
+    "auto_hide_diff_table_header": true,
+    "theme": "DEFAULT",
+    "ignore_whitespace": "IGNORE_NONE"
+  }
+----
+
+As response a link:rest-api-accounts.html#diff-preferences-info[
+DiffPreferencesInfo] is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "context": 10,
+    "tab_size": 8,
+    "line_length": 80,
+    "cursor_blink_rate": 0,
+    "intraline_difference": true,
+    "show_line_endings": true,
+    "show_tabs": true,
+    "show_whitespace_errors": true,
+    "syntax_highlighting": true,
+    "auto_hide_diff_table_header": true,
+    "theme": "DEFAULT",
+    "ignore_whitespace": "IGNORE_NONE"
+  }
+----
+
 
 [[ids]]
 == IDs
@@ -1008,6 +1261,12 @@
 is used for Git over HTTP/HTTPS]. Only set if
 link:config-gerrit.html#auth.type[authentication type] is is `LDAP` or
 `LDAP_BIND`.
+|`git_basic_auth_policy`      |optional|
+The link:config-gerrit.html#auth.gitBasicAuthPolicy[policy] to authenticate
+Git over HTTP and REST API requests when
+link:config-gerrit.html#auth.type[authentication type] is `LDAP` and
+link:config-gerrit.html#auth.gitBasicAuth[basic authentication] is set to true.
+Can be `HTTP`, `LDAP` or `HTTP_LDAP`.
 |==========================================
 
 [[cache-info]]
@@ -1075,6 +1334,9 @@
 [options="header",cols="1,^1,5"]
 |=============================
 |Field Name           ||Description
+|`allow_blame`        |not set if `false`|
+link:config-gerrit.html#change.allowBlame[Whether blame on side by side diff is
+allowed].
 |`allow_drafts`       |not set if `false`|
 link:config-gerrit.html#change.allowDrafts[Whether draft workflow is
 allowed].
@@ -1189,6 +1451,8 @@
 |`all_users_name`    ||
 Name of the link:config-gerrit.html#gerrit.allUsers[project in which
 meta data of all users is stored].
+|`doc_search`        ||
+Whether documentation search is available.
 |`doc_url`           |optional|
 Custom base URL where Gerrit server documentation is located.
 (Documentation may still be available at /Documentation relative to the
@@ -1202,54 +1466,6 @@
 bugs link].
 |=================================
 
-[[git-web-info]]
-=== GitwebInfo
-The `GitwebInfo` entity contains information about the
-link:config-gerrit.html#gitweb[gitweb] configuration.
-
-[options="header",cols="1,6"]
-|=======================
-|Field Name |Description
-|`url`      |
-The link:config-gerrit.html#gitweb.url[gitweb base URL].
-|`type`     |
-The link:config-gerrit.html#gitweb.type[gitweb type] as
-link:#git-web-type-info[GitwebTypeInfo] entity.
-|=======================
-
-[[git-web-type-info]]
-=== GitwebTypeInfo
-The `GitwebTypeInfo` entity contains information about the
-link:config-gerrit.html#gitweb[gitweb] configuration.
-
-[options="header",cols="1,^1,5"]
-|=============================
-|Field Name      ||Description
-|`name`          ||
-The link:config-gerrit.html#gitweb.linkname[gitweb link name].
-|`revision`      |optional|
-The link:config-gerrit.html#gitweb.revision[gitweb revision pattern].
-|`project`       |optional|
-The link:config-gerrit.html#gitweb.project[gitweb project pattern].
-|`branch`        |optional|
-The link:config-gerrit.html#gitweb.branch[gitweb branch pattern].
-|`root_tree`     |optional|
-The link:config-gerrit.html#gitweb.roottree[gitweb root tree pattern].
-|`file`          |optional|
-The link:config-gerrit.html#gitweb.file[gitweb file pattern].
-|`file_history`  |optional|
-The link:config-gerrit.html#gitweb.filehistory[gitweb file history
-pattern].
-|`path_separator`||
-The link:config-gerrit.html#gitweb.pathSeparator[gitweb path separator].
-|`link_drafts`   |optional|
-link:config-gerrit.html#gitweb.linkDrafts[Whether Gerrit should provide
-links to gitweb on draft patch set.]
-|`url_encode`    |optional|
-link:config-gerrit.html#gitweb.urlEncode[Whether Gerrit should encode
-the generated viewer URL.]
-|=============================
-
 [[hit-ration-info]]
 === HitRatioInfo
 The `HitRatioInfo` entity contains information about the hit ratio of a
@@ -1361,8 +1577,8 @@
 Information about the configuration from the
 link:config-gerrit.html#gerrit[gerrit] section as link:#gerrit-info[
 GerritInfo] entity.
-|`gitweb `                 |optional|
-Information about the link:config-gerrit.html#gitweb[gitweb]
+|`note_db_enabled`         |not set if `false`|
+Whether the NoteDb storage backend is fully enabled.
 |`plugin `                 ||
 Information about Gerrit extensions by plugins as
 link:#plugin-config-info[PluginConfigInfo] entity.
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index e0df4ca..23d4c5b 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -174,13 +174,12 @@
 
 [[suggest-group]]
 ==== Suggest Group
-The `suggest` option indicates a user-entered string that
+The `suggest` or `s` option indicates a user-entered string that
 should be auto-completed to group names.
 If this option is set and `n` is not set, then `n` defaults to 10.
 
-When using this option,
-the `project` or `p` option can be used to name the current project,
-to allow context-dependent suggestions.
+When using this option, the `project` or `p` option can be used to
+name the current project, to allow context-dependent suggestions.
 
 Not compatible with `visible-to-all`, `owned`, `user`, `match`, `q`,
 or `S`.
@@ -1315,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 986ccd8..457a287 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -733,6 +733,7 @@
     "create_new_change_for_all_not_in_target": "INHERIT",
     "enable_signed_push": "INHERIT",
     "require_signed_push": "INHERIT",
+    "reject_implicit_merges": "INHERIT",
     "require_change_id": "TRUE",
     "max_object_size_limit": "10m",
     "submit_type": "REBASE_IF_NECESSARY",
@@ -786,6 +787,11 @@
       "configured_value": "INHERIT",
       "inherited_value": false
     },
+    "reject_implicit_merges": {
+      "value": false,
+      "configured_value": "INHERIT",
+      "inherited_value": false
+    },
     "max_object_size_limit": {
       "value": "10m",
       "configured_value": "10m",
@@ -844,6 +850,34 @@
   done.
 ----
 
+==== Asynchronous Execution
+
+The option `async` allows to schedule a background task that asynchronously
+executes a Git garbage collection.
+
+The `Location` header of the response refers to the link:rest-api-config.html#get-task[background task]
+which allows to inspect the progress of its execution. In case of asynchronous
+execution the `show_progress` option is ignored.
+
+.Request
+----
+  POST /projects/plugins%2Freplication/gc HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "async": true
+  }
+----
+
+The response is empty.
+
+.Response
+----
+  HTTP/1.1 202 Accepted
+  Content-Disposition: attachment
+  Location: https:<host>/a/config/server/tasks/383a0602
+----
+
 [[ban-commit]]
 === Ban Commit
 --
@@ -895,6 +929,143 @@
   }
 ----
 
+[[get-access]]
+=== List Access Rights for Project
+--
+'GET /projects/link:rest-api-projects.html#project-name[\{project-name\}]/access'
+--
+
+Lists the access rights for a single project.
+
+As result a link:#project-access-info[ProjectAccessInfo] entity is returned.
+
+.Request
+----
+  GET /projects/MyProject/access HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "revision": "61157ed63e14d261b6dca40650472a9b0bd88474",
+    "inherits_from": {
+      "id": "All-Projects",
+      "name": "All-Projects",
+      "description": "Access inherited by all other projects."
+    },
+    "local": {
+        "refs/*": {
+          "permissions": {
+            "read": {
+              "rules": {
+                "c2ce4749a32ceb82cd6adcce65b8216e12afb41c": {
+                  "action": "ALLOW",
+                  "force": false
+                },
+                "global:Anonymous-Users": {
+                  "action": "ALLOW",
+                  "force": false
+                }
+              }
+            }
+          }
+        }
+    },
+    "is_owner": true,
+    "owner_of": [
+      "refs/*"
+    ],
+    "can_upload": true,
+    "can_add": true,
+    "config_visible": true
+  }
+----
+
+[[set-access]]
+=== Add, Update and Delete Access Rights for Project
+--
+'POST /projects/link:rest-api-projects.html#project-name[\{project-name\}]/access'
+--
+
+Sets access rights for the project using the diff schema provided by
+link:#project-access-input[ProjectAccessInput]. Deductions are used to
+remove access sections, permissions or permission rules. The backend will remove
+the entity with the finest granularity in the request, meaning that if an
+access section without permissions is posted, the access section will be
+removed; if an access section with a permission but no permission rules is
+posted, the permission will be removed; if an access section with a permission
+and a permission rule is posted, the permission rule will be removed.
+
+Additionally, access sections and permissions will be cleaned up after applying
+the deductions by removing items that have no child elements.
+
+After removals have been applied, additions will be applied.
+
+As result a link:#project-access-info[ProjectAccessInfo] entity is returned.
+
+.Request
+----
+  POST /projects/MyProject/access HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "remove": [
+      "refs/*": {
+        "permissions": {
+          "read": {
+            "rules": {
+              "c2ce4749a32ceb82cd6adcce65b8216e12afb41c": {
+                "action": "ALLOW"
+              }
+            }
+          }
+        }
+      }
+    ]
+  }
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "revision": "61157ed63e14d261b6dca40650472a9b0bd88474",
+    "inherits_from": {
+      "id": "All-Projects",
+      "name": "All-Projects",
+      "description": "Access inherited by all other projects."
+    },
+    "local": {
+        "refs/*": {
+          "permissions": {
+            "read": {
+              "rules": {
+                "global:Anonymous-Users": {
+                  "action": "ALLOW",
+                  "force": false
+                }
+              }
+            }
+          }
+        }
+    },
+    "is_owner": true,
+    "owner_of": [
+      "refs/*"
+    ],
+    "can_upload": true,
+    "can_add": true,
+    "config_visible": true
+  }
+----
+
 [[branch-endpoints]]
 == Branch Endpoints
 
@@ -1189,6 +1360,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
 --
@@ -1403,6 +1669,57 @@
 [[tag-endpoints]]
 == Tag Endpoints
 
+[[create-tag]]
+=== Create Tag
+
+--
+'PUT /projects/link:#project-name[\{project-name\}]/tags/link:#tag-id[\{tag-id\}]'
+--
+
+Create a new tag on the project.
+
+In the request body additional data for the tag can be provided as
+link:#tag-input[TagInput].
+
+If a message is provided in the input, the tag is created as an
+annotated tag with the current user as tagger. Signed tags are not
+supported.
+
+.Request
+----
+  PUT /projects/MyProject/tags/v1.0 HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "message": "annotation",
+    "revision": "c83117624b5b5d8a7f86093824e2f9c1ed309d63"
+  }
+----
+
+As response a link:#tag-info[TagInfo] entity is returned that describes
+the created tag.
+
+.Response
+----
+  HTTP/1.1 201 Created
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+
+  "object": "d48d304adc4b6674e11dc2c018ea05fcbdda32fd",
+  "message": "annotation",
+  "tagger": {
+    "name": "David Pursehouse",
+    "email": "dpursehouse@collab.net",
+    "date": "2016-06-06 01:22:03.000000000",
+    "tz": 540
+  },
+  "ref": "refs/tags/v1.0",
+  "revision": "c83117624b5b5d8a7f86093824e2f9c1ed309d63"
+  }
+----
+
 [[list-tags]]
 === List Tags
 --
@@ -1988,14 +2305,15 @@
 valid link:user-changeid.html[Change-Id] footer in any commit uploaded
 for review is required. This does not apply to commits pushed directly
 to a branch or tag.
-|`enable_signed_push`|
-optional, not set if signed push is disabled|
+|`enable_signed_push`|optional, not set if signed push is disabled|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 signed push validation is enabled on the project.
-|`require_signed_push`|
-optional, not set if signed push is disabled
+|`require_signed_push`|optional, not set if signed push is disabled|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 signed push validation is required on the project.
+|`reject_implicit_merges`|optional|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+implicit merges should be rejected on changes pushed to the project.
 |`max_object_size_limit`     ||
 The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
 limit] of this project as a link:#max-object-size-limit-info[
@@ -2064,6 +2382,11 @@
 directly to a branch or tag. +
 Can be `TRUE`, `FALSE` or `INHERIT`. +
 If not set, this setting is not updated.
+|`reject_implicit_merges`                  |optional|
+Whether a check for implicit merges will be performed when changes
+are pushed for review. +
+Can be `TRUE`, `FALSE` or `INHERIT`. +
+If not set, this setting is not updated.
 |`max_object_size_limit`                   |optional|
 The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
 limit] of this project as a link:#max-object-size-limit-info[
@@ -2108,7 +2431,7 @@
 `configured_value` and `inherited_value`.
 |`values`          |optional|
 The list of values. Only set if the `type` is `ARRAY`.
-`editable`         |`false` if not set|
+|`editable`         |`false` if not set|
 Whether the value is editable.
 |`permitted_values`|optional|
 The list of permitted values. Only set if the `type` is `LIST`.
@@ -2212,6 +2535,8 @@
 Whether progress information should be shown.
 |`aggressive`    |`false` if not set|
 Whether an aggressive garbage collection should be done.
+|`async`         |`false` if not set|
+Whether the garbage collection should run asynchronously.
 |=============================
 
 [[head-input]]
@@ -2265,6 +2590,27 @@
 Not set if there is no global limit for the object size.
 |===============================
 
+[[project-access-input]]
+=== ProjectAccessInput
+The `ProjectAccessInput` describes changes that should be applied to a project
+access config.
+
+[options="header",cols="1,^2,4"]
+|=============================
+|Field Name          |        |Description
+|`remove`            |optional|
+A list of deductions to be applied to the project access as
+link:rest-api-access.html#project-access-info[ProjectAccessInfo] entities.
+|`add`               |optional|
+A list of additions to be applied to the project access as
+link:rest-api-access.html#project-access-info[ProjectAccessInfo] entities.
+|`message`           |optional|
+A commit message for this change.
+|`parent`            |optional|
+A new parent for the project to inherit from. Changing the parent project
+requires administrative privileges.
+|=============================
+
 [[project-description-input]]
 === ProjectDescriptionInput
 The `ProjectDescriptionInput` entity contains information for setting a
@@ -2429,6 +2775,22 @@
 link:rest-api-changes.html#git-person-info[GitPersonInfo] entity.
 |=========================
 
+[[tag-input]]
+=== TagInput
+
+The `TagInput` entity contains information for creating a tag.
+
+[options="header",cols="1,^2,4"]
+|=========================
+|Field Name  ||Description
+|`ref`       ||The name of the tag. The leading `refs/tags/` is optional.
+|`revision`  |optional|The revision to which the tag should point. If not
+specified, the project's `HEAD` will be used.
+|`message`   |optional|The tag message. When set, the tag will be created
+as an annotated tag.
+|=========================
+
+
 [[theme-info]]
 === ThemeInfo
 The `ThemeInfo` entity describes a theme.
@@ -2444,6 +2806,7 @@
 The path to the `GerritSiteFooter.html` file.
 |=============================
 
+----
 
 GERRIT
 ------
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-inline-edit.txt b/Documentation/user-inline-edit.txt
index 05932df..f3c8b00 100644
--- a/Documentation/user-inline-edit.txt
+++ b/Documentation/user-inline-edit.txt
@@ -173,7 +173,7 @@
 change it, save it, close edit screen and select next file from the file table to edit.
 "<-" | "->" icons in header of edit screen could be used to navigate to the next file to
 change from the file table. This would behave like the navigation icons in side by side
-with thefollowing logic on click:
+with the following logic on click:
 
 ** "save-when-file-was-changed" or
 ** "close-when-no-changes"
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-review-ui.txt b/Documentation/user-review-ui.txt
index 859765c..838a433 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -187,6 +187,19 @@
 the link:access-control.html#category_submit[Submit] access right is
 assigned.
 
+** [[revert]]`Revert`:
++
+Reverts the change via creating a new one.
++
+The `Revert` button is available if the change has been submitted.
++
+When the `Revert` button is pressed, a panel will appear to allow
+the user to enter a commit message for the reverting change.
++
+Once a revert change is created, the original author and any reviewers
+of the original change are added as reviewers and a message is posted
+to the original change linking to the revert.
+
 ** [[abandon]]`Abandon`:
 +
 Abandons the change.
@@ -387,6 +400,10 @@
 
 image::images/user-review-ui-change-screen-patch-sets.png[width=800, link="images/user-review-ui-change-screen-patch-sets.png"]
 
+Another indication is a highlighted drop-down label.
+
+image::images/user-review-ui-change-screen-not-current.png[width=800, link="images/user-review-ui-change-screen-not-current.png"]
+
 [[patch-set-drop-down]]
 The patch set drop-down list shows the list of patch sets and allows to
 switch between them. The patch sets are sorted in descending order so
@@ -591,8 +608,12 @@
 
 Clicking on the `Reply...` button opens a popup panel.
 
+[[summary-comment]]
 A text box allows to type a summary comment for the currently viewed
-patch set.
+patch set. Some basic markdown-like syntax is supported which renders
+indented lines preformatted, lines starting with "- " or "* " as list
+items, and lines starting with "> " as block quotes (also see replying to
+link:#reply-to-message[messages] and link:#reply-inline-comment[inline comments]).
 
 Note that you can set the text and tooltip of the button in
 link:config-gerrit.html#change.replyLabel[gerrit.config].
@@ -649,7 +670,7 @@
 reply icon in the right upper corner of a change message. This opens
 the reply popup panel and prefills the text box with the quoted comment.
 Then the reply can be written below the quoted comment or inserted
-inline. Lines starting with " > " will be rendered as a block quote.
+inline. Lines starting with "> " will be rendered as a block quote.
 Please note that for a correct rendering it is important to leave a blank
 line between a quoted block and the reply to it.
 
@@ -818,7 +839,7 @@
 Clicking on the `Reply` button opens an editor to type the reply.
 
 Quoting is supported, but only by manually copying & pasting the old
-comment that should be quoted and prefixing every line by " > ". Please
+comment that should be quoted and prefixing every line by "> ". Please
 note that for a correct rendering it is important to leave a blank line
 between a quoted block and the reply to it.
 
diff --git a/Documentation/user-search-accounts.txt b/Documentation/user-search-accounts.txt
new file mode 100644
index 0000000..15d87b0
--- /dev/null
+++ b/Documentation/user-search-accounts.txt
@@ -0,0 +1,83 @@
+= Gerrit Code Review - Searching Accounts
+
+== Basic Change Search
+
+Similar to many popular search engines on the web, just enter some
+text and let Gerrit figure out the meaning:
+
+[options="header"]
+|=============================================================
+|Description                      | Examples
+|Name                             | John
+|Email address                    | jdoe@example.com
+|Username                         | jdoe
+|Account-Id                       | 1000096
+|Own account                      | self
+|=============================================================
+
+[[search-operators]]
+== Search Operators
+
+Operators act as restrictions on the search. As more operators
+are added to the same query string, they further restrict the
+returned results. Search can also be performed by typing only a
+text with no operator, which will match against a variety of fields.
+
+[[email]]
+email:'EMAIL'::
++
+Matches accounts that have the email address 'EMAIL' or an email
+address that starts with 'EMAIL'.
+
+[[is]]
+[[is-active]]
+is:active::
++
+Matches accounts that are active.
+
+[[is-inactive]]
+is:inactive::
++
+Matches accounts that are inactive.
+
+[[name]]
+name:'NAME'::
++
+Matches accounts that have any name part 'NAME'. The name parts consist
+of any part of the full name and the email addresses.
+
+[[username]]
+username:'USERNAME'::
++
+Matches accounts that have the username 'USERNAME'.
+
+== Magical Operators
+
+[[is-visible]]
+is:visible::
++
+Magical internal flag to prove the current user has access to read
+the change. This flag is always added to any query.
+
+[[is-active-magic]]
+is:active::
++
+Matches accounts that are active. If neither link:#is-active[is:active]
+nor link:#is-inactive[is:inactive] is contained in a query, `is:active`
+is automatically added so that by default only active accounts are
+matched.
+
+[[limit]]
+limit:'CNT'::
++
+Limit the returned results to no more than 'CNT' records. This is
+automatically set to the page size configured in the current user's
+preferences. Including it in a web query may lead to unpredictable
+results with regards to pagination.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index b2b3614..b04898e 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -247,25 +247,43 @@
 Regular expression matching can be enabled by starting the string
 with `^`. In this mode `file:` is an alias of `path:` (see above).
 
+[[star]]
+star:'LABEL'::
++
+Matches any change that was starred by the current user with the label
+'LABEL'.
++
+E.g. if changes that are not interesting are marked with an `ignore`
+star, they could be filtered out by '-star:ignore'.
++
+'star:star' is the same as 'has:star' and 'is:starred'.
+
 [[has]]
 has:draft::
 +
 True if there is a draft comment saved by the current user.
 
+[[has-star]]
 has:star::
 +
-Same as 'is:starred', true if the change has been starred by the
-current user.
+Same as 'is:starred' and 'star:star', true if the change has been
+starred by the current user with the default label.
+
+[[has-stars]]
+has:stars::
++
+True if the change has been starred by the current user with any label.
 
 has:edit::
 +
 True if the change has inline edit created by the current user.
 
 [[is]]
+[[is-starred]]
 is:starred::
 +
 Same as 'has:star', true if the change has been starred by the
-current user.
+current user with the default label.
 
 is:watched::
 +
@@ -510,7 +528,7 @@
 
 starredby:'USER'::
 +
-Matches changes that have been starred by 'USER'.
+Matches changes that have been starred by 'USER' with the default label.
 The special case `starredby:self` applies to the caller.
 
 watchedby:'USER'::
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index 151ac71..2754b45 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -1,78 +1,127 @@
 = Gerrit Code Review - Superproject subscription to submodules updates
 
+[[automatic_update]]
 == Description
-
 Gerrit supports a custom git superproject feature for tracking submodules.
 This feature is useful for automatic updates on superprojects whenever
-a change is merged on tracked submodules. To take advantage of this
-feature, one should add submodule(s) to a local working copy of a
-superproject, edit the created .gitmodules configuration file to
-have a branch field on each submodule section with the value of the
-submodule branch it is subscribing to, commit the changes, push and
-merge the commit.
+a change is merged on tracked submodules.
 
-When a commit is merged to a project, the commit content is scanned
-to identify if it registers git submodules (if the commit registers
-any gitlinks and .gitmodules file with required info) and if so,
-a new submodule subscription is registered.
+When a superproject is subscribed to a submodule, it is not
+required to push/merge commits to this superproject to update the
+gitlink to the submodule. Whenever a commit is merged in a submodule,
+its subscribed superproject is updated by Gerrit.
 
-When a new commit of a registered submodule is merged, Gerrit
-automatically updates the subscribers to the submodule with a new
-commit having the updated gitlinks.
+Imagine a superproject called 'super' having a branch called 'dev'
+having subscribed to a submodule 'sub' on a branch 'dev-of-sub'. When a commit
+is merged in branch 'dev-of-sub' of 'sub' project, Gerrit automatically
+creates a new commit on branch 'dev' of 'super' updating the gitlink
+to point to the just merged commit.
 
-== Git Submodules Overview
+To take advantage of this feature, one should:
 
-Submodules are a git feature that allows an external repository to be
+. ensure superproject subscriptions are enabled on the server via
+  link:config-gerrit.html#submodule.enableSuperProjectSubscriptions[submodule.enableSuperProjectSubscriptions]
+. configure the submodule to allow having a superproject subscribed
+. ensure the .gitmodules file of the superproject includes
+.. a branch field
+.. a url that starts with the link:config-gerrit.html#gerrit.canonicalWebUrl[`gerrit.canonicalWebUrl`]
+
+When a commit in a project is merged, Gerrit checks for superprojects
+that are subscribed to the the project and automatically updates those
+superprojects with a commit that updates the gilink for the project.
+
+This feature is enabled by default and can be disabled
+via link:config-gerrit.html#submodule.enableSuperProjectSubscriptions[submodule.enableSuperProjectSubscriptions]
+in the server configuration.
+
+== Git submodules overview
+
+Submodules are a Git feature that allows an external repository to be
 attached inside a repository at a specific path. The objective here
 is to provide a brief overview, further details can be found
-in the official git submodule command documentation.
+in the official Git submodule documentation.
 
-Imagine a repository called 'super' and another one called 'a'.
-Also consider 'a' available in a running Gerrit instance on "server".
-With this feature, one could attach 'a' inside of 'super' repository
-at path 'a' by executing the following command when being inside
+Imagine a repository called 'super' and another one called 'sub'.
+Also consider 'sub' available in a running Gerrit instance on "server".
+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/a a
-=====
+----
+git submodule add ssh://server/sub sub
+----
 
 Still considering the above example, after its execution notice that
-inside the local repository 'super' the 'a' folder is considered a
-gitlink to the external repository 'a'. Also notice a file called
+inside the local repository 'super' the 'sub' folder is considered a
+gitlink to the external repository 'sub'. Also notice a file called
 .gitmodules is created (it is a configuration file containing the
-subscription of 'a'). To provide the SHA-1 each gitlink points to in
+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 'a' is updated and 'super' is supposed
-to see the latest SHA-1 (considering here 'a' has only the master
-branch), one should then commit the modified gitlink for 'a' in
+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
+branch), one should then commit the modified gitlink for 'sub' in
 the 'super' project. Actually it would not even need to be an
-external update, one could move to 'a' folder (insider 'super'),
+external update, one could move to 'sub' folder (inside 'super'),
 modify its content, commit, then move back to 'super' and
-commit the modified gitlink for 'a'.
+commit the modified gitlink for 'sub'.
 
-== Creating a New Subscription
+== Creating a new subscription
 
-=== Defining the Submodule Branch
+=== Ensure the subscription is allowed
 
-This is required because submodule subscription is actually the
-subscription of a submodule project and one of its branches for
-a branch of a super project.
+Gerrit has a complex access control system, where different repositories
+can be accessed by different groups of people. To ensure that the submodule
+related information is allowed to be exposed in the superproject,
+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>"]
+    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"]
+        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.
+
+=== Defining the submodule branch
 
 Since Gerrit manages subscriptions in the branch scope, we could have
 a scenario having a project called 'super' having a branch 'integration'
-subscribed to a project called 'a' in branch 'integration', and also
-having the same 'super' project but in branch 'dev' subscribed to the 'a'
+subscribed to a project called 'sub' in branch 'integration', and also
+having the same 'super' project but in branch 'dev' subscribed to the 'sub'
 project in a branch called 'local-dev'.
 
 After adding the git submodule to a super project, one should edit
 the .gitmodules file to add a branch field to each submodule
 section which is supposed to be subscribed.
 
-As the branch field is a Gerrit specific field it will not be filled
+As the branch field is a Gerrit-specific field it will not be filled
 automatically by the git submodule command, so one needs to edit it
 manually. Its value should indicate the branch of a submodule project
 that when updated will trigger automatic update of its registered
@@ -90,45 +139,78 @@
 .gitmodules file, Gerrit will not create a subscription for the
 submodule and there will be no automatic updates to the superproject.
 
-=== Detecting and Subscribing Submodules
+Whenever a commit is merged to a project, its project config is checked
+to see if any potential superprojects are allowed to subscribe to it.
+If so, the superproject is checked if a valid subscription exists
+by checking the .gitmodules file for the a submodule which includes
+a `branch` field and a url pointing to this server.
 
-Whenever a commit is merged to a project, its content is scanned
-to identify if it registers any submodules (if the commit contains new
-gitlinks and a .gitmodules file with all required info) and if so,
-a new submodule subscription is registered.
+[[acl_refspec]]
+=== The RefSpec in the allowSuperproject section
+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.
 
-[[automatic_update]]
-== Automatic Update of Superprojects
+The most restrictive refspec is allowing one specific branch of the
+submodule to be subscribed to one specific branch of the superproject:
+----
+  [allowSuperproject "<superproject>"]
+    matching = refs/heads/<submodule-branch>:refs/heads/<superproject-branch>
+----
 
-After a superproject is subscribed to a submodule, it is not
-required to push/merge commits to this superproject to update the
-gitlink to the submodule.
+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>"]
+    matching = refs/heads/*:refs/heads/*
+----
 
-Whenever a commit is merged in a submodule, its subscribed superproject
-is updated.
+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>"]
+     all = refs/heads/<submodule-branch>:refs/heads/*
+----
 
-Imagine a superproject called 'super' having a branch called 'dev'
-having subscribed to a submodule 'a' on a branch 'dev-of-a'. When a commit
-is merged in branch 'dev-of-a' of 'a' project, Gerrit automatically
-creates a new commit on branch 'dev' of 'super' updating the gitlink
-to point to the just merged commit.
+To make all branches available for subscription from all branches of
+the superproject:
+----
+  [allowSuperproject "<superproject>"]
+     all = refs/heads/*:refs/heads/*
+----
 
 === Subscription Limitations
 
 Gerrit will only automatically update superprojects where the
 submodules are hosted on the same Gerrit instance as the
-superproject. Gerrit determines this by checking the hostname of the
-submodule specified in the .gitmodules file and comparing it to the
-hostname from the canonical web URL.
+superproject. Gerrit determines this by checking that the URL of the
+submodule specified in the .gitmodules file starts with
+link:config-gerrit.html#gerrit.canonicalWebUrl[`gerrit.canonicalWebUrl`].
+The protocol part is ignored in this check.
 
 It is currently not possible to use the submodule subscription feature
-with a canonical web URL hostname that differs from the hostname of
-the submodule. Instead relative submodules should be used.
+with a canonical web URL that differs from the first part  of
+the submodule URL. Instead relative submodules should be used.
 
-The Gerrit instance administrator group should always certify to
-provide the canonical web URL value in its configuration file. Users
-should certify to use the correct hostname of the running Gerrit
-instance to add/subscribe submodules.
+The Gerrit instance administrator should ensure that the canonical web
+URL value is specified in its configuration file. Users should ensure
+that they use the correct hostname of the running Gerrit instance when
+adding submodule subscriptions.
+
+When converting an existing submodule to use subscription by adding
+a `branch` field into the .gitmodules file, Gerrit does not change
+the revision of the submodule (i.e. update the superproject's gitlink)
+until the next time the branch of the submodule advances. In other words,
+if the currently used revision of the submodule is not the branch's head,
+adding a subscription will not cause an immediate update to the head. In
+this case the revision must be manually updated at the same time as adding
+the subscription.
 
 === Relative submodules
 
@@ -170,10 +252,9 @@
 
 == Removing Subscriptions
 
-If one has added a submodule subscription and drops it, it is
-required to merge a commit updating the subscribed super
-project/branch to remove the gitlink and the submodule section
-of the .gitmodules file.
+To remove a subscription, either disable the subscription from the
+submodules configuration or remove the submodule or information thereof
+(such as the branch field) in the superproject.
 
 GERRIT
 ------
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 25665a3..ba3445a 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -19,11 +19,17 @@
 user must authenticate via HTTP/HTTPS.
 
 When link:config-gerrit.html#auth.gitBasicAuth[gitBasicAuth] is enabled,
-the user is authenticated using standard BasicAuth and credentials validated
-using the randomly generated HTTP password on the `HTTP Password` tab
-in the user settings page or against LDAP when configured for the Gerrit Web UI.
+the user is authenticated using standard BasicAuth. Depending on the value of
+link:#auth.gitBasicAuthPolicy[auth.gitBasicAuthPolicy], credentials are
+validated using:
 
-When gitBasicAuth is not configured, the user's HTTP credentials can be
+* The randomly generated HTTP password on the `HTTP Password` tab
+  in the user settings page if `gitBasicAuthPolicy` is `HTTP`.
+* The LDAP password if `gitBasicAuthPolicy` is `LDAP`
+* Both, the HTTP and the LDAP passwords (in this order) if `gitBasicAuthPolicy`
+  is `HTTP_LDAP`.
+
+When gitBasicAuthPolicy is not `LDAP`, the user's HTTP credentials can be
 accessed within Gerrit by going to `Settings`, and then accessing the `HTTP
 Password` tab.
 
@@ -54,16 +60,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 +86,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 +99,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 +109,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 +129,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
@@ -146,38 +150,89 @@
 notify them of new changes will be automatically sent an email
 message when the push is completed.
 
+[[push_options]]
+=== Push Options
+
+Additional options may be specified when pushing changes.
+
+[[notify]]
+==== Email Notifications
+
+Uploaders can control to whom email notifications are sent by setting
+the `notify` option:
+
+* `NONE`: No email notification will be sent to anyone.
+* `OWNER`: Only the change owner is notified.
+* `OWNER_REVIEWERS`: Only owners and reviewers will be  notified. This
+  includes all reviewers, existing reviewers of the change and new
+  reviewers that are added by the `reviewer` option or by mentioning
+  in the commit message.
+* `ALL`: All email notifications will be sent. This includes
+  notifications to watchers, users that have starred the change, CCs
+  and the committer and author of the uploaded commit.
+
+By default all email notifications are sent.
+
+----
+  git push ssh://bot@git.example.com:29418/kernel/common HEAD:refs/for/master%notify=NONE
+----
+
 [[topic]]
+==== Topic
+
 To include a short tag associated with all of the changes in the
 same group, such as the local topic branch name, append it after
 the destination branch name. In this example the short topic tag
 '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]]
+==== 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]
+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
+
 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).
 
 [[change_edit]]
+==== Change Edits
+
 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.
@@ -193,7 +248,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
@@ -202,15 +257,18 @@
     User john.doe
 
   $ git push tr:kernel/common HEAD:refs/for/experimental
-====
+----
+
+[[reviewers]]
+==== Reviewers
 
 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
@@ -222,7 +280,7 @@
 branches, consider adding a custom remote block to your project's
 `.git/config` file:
 
-====
+----
   $ cat .git/config
   ...
   [remote "exp"]
@@ -230,7 +288,7 @@
     push = HEAD:refs/for/experimental%r=a@a.com,cc=b@o.com
 
   $ git push exp
-====
+----
 
 
 [[push_replace]]
@@ -254,8 +312,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
@@ -263,7 +321,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
@@ -271,9 +329,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
@@ -281,7 +339,7 @@
 
 For example, consider the following sequence of events:
 
-====
+----
   $ git commit -m A                    ; # create 3 commits
   $ git commit -m B
   $ git commit -m C
@@ -298,7 +356,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'
@@ -363,18 +421,21 @@
 Changes can be directly submitted on push.  This is primarily useful
 for teams that don't want to do code review but want to use Gerrit's
 submit strategies to handle contention on busy branches.  Using
-`%submit` creates a change and submits it immediately, if the caller
-has link:access-control.html#category_submit[Submit] permission on
-`refs/for/<ref>` (e.g. on `refs/for/refs/heads/master`).
+`%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
 set the merge can be reattempted by using `%submit` again.
 
+This requires the caller to have link:access-control.html#category_submit[Submit]
+permission on `refs/for/<ref>` (e.g. on `refs/for/refs/heads/master`).
+Note how this is different from the `Submit` permission on `refs/heads/<ref>`,
+and in particular you typically do not want to apply the `Submit` permission
+on `refs/*` (unless you are ok with bypassing submit rules).
 
 [[base]]
 === Selecting Merge Base
@@ -384,18 +445,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/README.md b/README.md
index b8de6e2..020602f 100644
--- a/README.md
+++ b/README.md
@@ -71,9 +71,3 @@
 
 _NOTE: release is optional. Last released package of the version is installed if the release
 number is omitted._
-
-## Events
-
-- November 7-8 2015: Gerrit User Conference, Mountain View. ([Register](http://goo.gl/forms/fifi2YQTc7)).
-- November 9-13 2015: Gerrit Hackathon, Mountain View. (Invitation Only).
-- March 2016: Gerrit Hackathon, Berlin. (Details to be confirmed).
diff --git a/ReleaseNotes/BUCK b/ReleaseNotes/BUCK
new file mode 100644
index 0000000..0f47808
--- /dev/null
+++ b/ReleaseNotes/BUCK
@@ -0,0 +1,19 @@
+include_defs('//Documentation/asciidoc.defs')
+include_defs('//ReleaseNotes/config.defs')
+
+DIR = 'ReleaseNotes'
+
+SRCS = glob(['*.txt'])
+
+
+genasciidoc(
+  name = 'html',
+  out = 'html.zip',
+  directory = DIR,
+  srcs = SRCS,
+  attributes = release_notes_attributes(),
+  backend = 'html5',
+  searchbox = False,
+  resources = False,
+  visibility = ['PUBLIC'],
+)
diff --git a/ReleaseNotes/Makefile b/ReleaseNotes/Makefile
deleted file mode 100644
index 3081600..0000000
--- a/ReleaseNotes/Makefile
+++ /dev/null
@@ -1,47 +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.
-
-ASCIIDOC       ?= asciidoc
-ASCIIDOC_EXTRA ?=
-
-DOC_HTML      := $(patsubst %.txt,%.html,$(wildcard ReleaseNotes*.txt))
-
-all: html
-
-html: index.html $(DOC_HTML)
-
-clean:
-	rm -f *.html
-
-index.html: index.txt
-	@echo FORMAT $@
-	@rm -f $@+ $@
-	@$(ASCIIDOC) --unsafe \
-		-a toc \
-		-b xhtml11 -f asciidoc.conf \
-		$(ASCIIDOC_EXTRA) -o $@+ $<
-	@mv $@+ $@
-
-$(DOC_HTML): %.html : %.txt
-	@echo FORMAT $@
-	@rm -f $@+ $@
-	@v=$$(echo $< | sed 's/^ReleaseNotes-//;s/.txt$$//;') && \
-	 n=$$(git describe HEAD) && \
-	 if ! git diff-index --quiet v$$v -- $< 2>/dev/null; then v="$$v (from $$n)"; fi && \
-	 $(ASCIIDOC) --unsafe \
-		-a toc \
-		-a "revision=$$v" \
-		-b xhtml11 -f asciidoc.conf \
-		$(ASCIIDOC_EXTRA) -o $@+ $<
-	@mv $@+ $@
diff --git a/ReleaseNotes/ReleaseNotes-2.0.10.txt b/ReleaseNotes/ReleaseNotes-2.0.10.txt
index 695be4f..33078d9 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.10.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.10.txt
@@ -1,13 +1,11 @@
-Release notes for Gerrit 2.0.10
-===============================
+= Release notes for Gerrit 2.0.10
 
 Gerrit 2.0.10 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 
-New Features
-------------
+== New Features
 
 * GERRIT-129  Make the browser window title reflect the current scre...
 +
@@ -25,8 +23,7 @@
 +
 Minor enhancement to the way submitted emails are formatted.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * GERRIT-91   Delay updating the UI until a Screen instance is fully...
 +
@@ -46,8 +43,7 @@
 * GERRIT-135  Enable Save button after paste in a comment editor
 * GERRIT-137  Error out if a user forgets to squash when replacing a...
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.10 development
 * Add missing super.onSign{In,Out} calls to ChangeScreen
 * Remove the now pointless sign in callback support
diff --git a/ReleaseNotes/ReleaseNotes-2.0.11.txt b/ReleaseNotes/ReleaseNotes-2.0.11.txt
index 62f2a18..5bd6ca0 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.11.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.11.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.11
-===============================
+= Release notes for Gerrit 2.0.11
 
 Gerrit 2.0.11 is now available in the usual location:
 
@@ -12,11 +11,9 @@
   java -jar gerrit.war --cat sql/upgrade009_010.sql | psql reviewdb
 ----
 
-Important Notes
----------------
+== Important Notes
 
-Cache directory
-~~~~~~~~~~~~~~~
+=== Cache directory
 
 Gerrit now prefers having a temporary directory to store a disk-based content cache.  This cache used to be in the PostgreSQL database, and was the primary reason for the rather large size of the Gerrit schema.  In 2.0.11 the cache has been moved to the local filesystem, and now has automatic expiration management to prevent it from growing too large.  As this is only a cache, making backups of this directory is not required.
 
@@ -30,13 +27,11 @@
 
 link:http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html[http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html]
 
-Protocol change
-~~~~~~~~~~~~~~~
+=== Protocol change
 
 The protocol between the browser based JavaScript and the server has changed.  After installing 2.0.11 users need to load the site page again to ensure they are running 2.0.11 or later.  Users can verify they have the new version by checking the version number in the footer in the lower right.  Users who don't load the new version (e.g. are using a stale tab from a week ago) will see errors when trying to view patches.
 
-New Features
-------------
+== New Features
 
 * GERRIT-8    Add 'Whole File' as a context preference in the user s...
 * GERRIT-9    Honor user's "Default Context" preference
@@ -65,8 +60,7 @@
 +
 Simple DWIMery: users can now do `repo upload --reviewer=who` to have the reviewer email automatically expand according to the email_format column in system_config, e.g. by expanding `who` to `who@example.com`.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * GERRIT-81   Can't repack a repository while Gerrit is running
 +
@@ -80,8 +74,7 @@
 +
 Service users created by manually inserting into the accounts table didn't permit using their preferred_email in commits or tags; administrators had to also insert a dummy record into the account_external_ids table.  The dummy account_external_ids record is no longer necessary.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.11 development
 * Include the 'Google Format' style we selected in our p...
 * Upgrade JGit to v0.4.0-310-g3da8761
diff --git a/ReleaseNotes/ReleaseNotes-2.0.12.txt b/ReleaseNotes/ReleaseNotes-2.0.12.txt
index eb28e2e..0e1df04 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.12.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.12.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.12
-===============================
+= Release notes for Gerrit 2.0.12
 
 Gerrit 2.0.12 is now available in the usual location:
 
@@ -12,21 +11,17 @@
   java -jar gerrit.war --cat sql/upgrade010_011.sql | psql reviewdb
 ----
 
-Important Notes
----------------
+== Important Notes
 
-Java 6 Required
-~~~~~~~~~~~~~~~
+=== Java 6 Required
 
 Gerrit now requires running within a Java 6 (or later) JVM.
 
-Protocol change
-~~~~~~~~~~~~~~~
+=== Protocol change
 
 The protocol between the browser based JavaScript and the server has changed.  After installing 2.0.12 users need to load the site page again to ensure they are running 2.0.12 or later.  Users can verify they have the new version by checking the version number in the footer in the lower right.  Users who don't load the new version (e.g. are using a stale tab from a week ago) will see errors when trying to view patches.
 
-New Features
-------------
+== New Features
 * Honor --reviewer=not.preferred.email during upload
 * Also scan by preferred email for --reviewers and --cc ...
 +
@@ -73,8 +68,7 @@
 +
 Keyboard bindings have been completely overhauled in this release, and should now work on every browser.  Press '?' in any context to see the available actions.  Please note that this help is context sensitive, so you will only see keys that make sense in the current context.  Actions in a user dashboard screen differ from actions in a patch (for example), but where possible the same key is used when the logical meaning is unchanged.
 
-Bug Fixes
----------
+== Bug Fixes
 * Ignore "SshException: Already closed" errors
 +
 Hides some non-errors from the log file.
@@ -83,8 +77,7 @@
 +
 Should be a minor improvement for MSIE 6 users.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.12 development
 * Report what version we want on a schema version mismat...
 * Remove unused imports in SshServlet
diff --git a/ReleaseNotes/ReleaseNotes-2.0.13.txt b/ReleaseNotes/ReleaseNotes-2.0.13.txt
index 8ec13a8..7589568 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.13.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.13.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.13, 2.0.13.1
-=========================================
+= Release notes for Gerrit 2.0.13, 2.0.13.1
 
 Gerrit 2.0.13.1 is now available in the usual location:
 
@@ -23,8 +22,7 @@
   java -jar gerrit.war --cat sql/upgrade011_012_part2.sql | psql reviewdb
 ----
 
-Configuration Mapping
----------------------
+== Configuration Mapping
 || *system_config*                || *$site_path/gerrit.config*     ||
 || max_session_age                || auth.maxSessionAge             ||
 || canonical_url                  || gerrit.canonicalWebUrl         ||
@@ -45,8 +43,7 @@
 
 See also [http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html Gerrit2 Configuration].
 
-New Features
-------------
+== New Features
 * GERRIT-180  Rewrite outgoing email to be more user friendly
 +
 A whole slew of feature improvements on outgoing email formatting was closed by this one (massive) rewrite of the outgoing email implementation.
@@ -81,8 +78,7 @@
 +
 The new `sendemail` section of `$site_path/gerrit.config` now controls the configuration of the outgoing SMTP server, rather than relying upon a JNDI resource.  See [http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html configuration] section sendemail for more details.
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix file browser in patch that is taller than the wind...
 * GERRIT-184  Make 'f' toggle the file browser popup closed
 * GERRIT-188  Fix key bindings in patch when changing the old or new...
@@ -123,8 +119,7 @@
 +
 Bug fixes identified after release of 2.0.13, rolled into 2.0.13.1.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.13 development
 * Use gwtexpui 1.1.1-SNAPSHOT
 * Document the Patch.PatchType and Patch.ChangeType enum
diff --git a/ReleaseNotes/ReleaseNotes-2.0.14.txt b/ReleaseNotes/ReleaseNotes-2.0.14.txt
index de58035..128036d 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.14.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.14.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.14, 2.0.14.1
-=========================================
+= Release notes for Gerrit 2.0.14, 2.0.14.1
 
 Gerrit 2.0.14.1 is now available in the usual location:
 
@@ -13,8 +12,7 @@
   java -jar gerrit.war --cat sql/upgrade012_013_mysql.sql | mysql reviewdb
 ----
 
-New Features
-------------
+== New Features
 * GERRIT-177  Display branch name next to project in change list
 +
 Now its easier to see from your Mine>Changes what branch each change goes to.  For some users this may help prioritize reviews.
@@ -53,8 +51,7 @@
 +
 This is really for the server admin, the Git reflogs are now more likely to contain actual user information in them, rather than generic "gerrit2@localhost" identities.  This may help if you are mining "WTF happened to this branch" data from Git directly.
 
-Bug Fixes
----------
+== Bug Fixes
 * GERRIT-213  Fix n/p on a file with only one edit
 * GERRIT-66   Always show comments in patch views, even if no edit e...
 * Correctly handle comments after last hunk of patch
@@ -81,8 +78,7 @@
 +
 Fixed run-on addresses when more than one user was listed in To/CC headers.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.14 development (again)
 * Small doc updates.
 * Merge change 10282
diff --git a/ReleaseNotes/ReleaseNotes-2.0.15.txt b/ReleaseNotes/ReleaseNotes-2.0.15.txt
index a87cba1..a8d60a4 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.15.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.15.txt
@@ -1,23 +1,19 @@
-Release notes for Gerrit 2.0.15
-===============================
+= Release notes for Gerrit 2.0.15
 
 Gerrit 2.0.15 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 None.  For a change.  :-)
 
-New Features
-------------
+== New Features
 * Allow other ignore whitespace settings beyond IGNORE_S...
 +
 Now you can ignore whitespace inside the middle of a line, in addition to on the ends.
 
-Bug Fixes
----------
+== Bug Fixes
 * Update SSHD to include SSHD-28 (deadlock on close) bug...
 +
 Fixes a major stability problem with the internal SSHD.  Without this patch the daemon can become unresponsive, requiring a complete JVM restart to recover the daemon.  The symptom is connections appear to work sporadically... some connections are fine while others freeze during setup, or during data transfer.
@@ -31,8 +27,7 @@
 +
 Stupid bugs in the patch viewing code.  Random server errors and/or client UI crashes.
 
-Other Changes
--------------
+== Other Changes
 * Restart 2.0.15 development
 * Update JGit to 0.4.0-411-g8076bdb
 * Remove dead isGerrit method from AbstractGitCommand
diff --git a/ReleaseNotes/ReleaseNotes-2.0.16.txt b/ReleaseNotes/ReleaseNotes-2.0.16.txt
index 4d0252d..4f5a5ba 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.16.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.16.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.16
-===============================
+= Release notes for Gerrit 2.0.16
 
 Gerrit 2.0.16 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING: This version contains a schema change* (since 2.0.14)
 
@@ -16,8 +14,7 @@
   java -jar gerrit.war --cat sql/upgrade013_014_mysql.sql | mysql reviewdb
 ----
 
-New Features
-------------
+== New Features
 * Search for changes created or reviewed by a user
 +
 The search box in the upper right corner now accepts "owner:email" and "reviewer:email", in addition to change numbers and commit SHA-1s.  Using owner: and reviewer: is not the most efficient query plan, as potentially the entire database is scanned.  We hope to improve on that as we move to a pure git based backend.
@@ -42,8 +39,7 @@
 +
 The "/Gerrit" suffix is no longer necessary in the URL.  Gerrit now favors just "/" as its path location.  This drops one redirection during initial page loading, slightly improving page loading performance, and making all URLs 6 characters shorter.  :-)
 
-Bug Fixes
----------
+== Bug Fixes
 * Don't create reflogs for patch set refs
 +
 Previously Gerrit created pointless 1 record reflogs for each change ref under refs/changes/.  These waste an inode on the local filesystem and provide no metadata value, as the same information is also stored in the metadata database.  These reflogs are no longer created.
@@ -64,8 +60,7 @@
 +
 If the hostname is "localhost" or "127.0.0.1", such as might happen when a user tries to proxy through an SSH tunnel, we honor the hostname anyway if OpenID is not being used.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.16 development
 * Update JGit to 0.4.9-18-g393ad45
 * Name replication threads by their remote name
diff --git a/ReleaseNotes/ReleaseNotes-2.0.17.txt b/ReleaseNotes/ReleaseNotes-2.0.17.txt
index 493a64b..8a24b22 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.17.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.17.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.17
-===============================
+= Release notes for Gerrit 2.0.17
 
 Gerrit 2.0.17 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING: This version contains a schema change* (since 2.0.16)
 
@@ -22,8 +20,7 @@
   java -jar gerrit.war --cat sql/upgrade014_015_part2.sql | mysql reviewdb
 ----
 
-New Features
-------------
+== New Features
 * Add '[' and ']' shortcuts to PatchScreen.
 +
 The keys '[' and ']' can be used to navigate to previous and next file in a patch set.
@@ -56,8 +53,7 @@
 +
 The owner of a project was moved from the General tab to the Access Rights tab, under a new category called Owner.  This permits multiple groups to be designated the Owner of the project (simply grant Owner status to each group).
 
-Bug Fixes
----------
+== Bug Fixes
 * Permit author Signed-off-by to be optional
 +
 If a project requires Signed-off-by tags to appear the author tag is now optional, only the committer/uploader must provide a Signed-off-by tag.
@@ -83,8 +79,7 @@
 +
 Instead of crashing on a criss-cross merge case, Gerrit unsubmits the change and attaches a message, like it does when it encounters a path conflict.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.17 development
 * Move '[' and ']' key bindings to Navigation category
 * Use gwtexpui 1.1.2-SNAPSHOT to fix navigation keys
diff --git a/ReleaseNotes/ReleaseNotes-2.0.18.txt b/ReleaseNotes/ReleaseNotes-2.0.18.txt
index df635d9..1028185 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.18.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.18.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.18
-===============================
+= Release notes for Gerrit 2.0.18
 
 Gerrit 2.0.18 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Important Notices
------------------
+== Important Notices
 
 Please ensure you read the following important notices about this release; .18 is a much larger release than usual.
 
@@ -41,8 +39,7 @@
 the SSH authentication system.  More details can be found in the
 [http://android.git.kernel.org/?p=tools/gerrit.git;a=commit;h=080b40f7bbe00ac5fc6f2b10a861b63ce63e8add commit message].
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING: This version contains a schema change* (since 2.0.17)
 
@@ -71,15 +68,13 @@
   java -jar gerrit.war --cat sql/upgrade015_016_part2.sql | mysql reviewdb
 ----
 
-New Bugs
---------
+== New Bugs
 * Memory leaks during warm restarts
 
 2.0.18 includes [http://code.google.com/p/google-guice/ Google Guice], which leaves a finalizer thread dangling when the Gerrit web application is halted by the servlet container.  As this thread does not terminate, the web context stays loaded in memory indefinitely, creating a memory leak.  Cold restarting the container in order to restart Gerrit is highly recommended.
 
 
-New Features
-------------
+== New Features
 * GERRIT-104  Allow end-users to select their own SSH username
 +
 End users may now select their own SSH username through the web interface.  The username must be unique within a Gerrit server installation.  During upgrades from 2.0.17 duplicate users are resolved by giving the username to the user who most recently logged in under it; other users will need to login through the web interface and select a unique username.  This change was necessary to fix a very minor security bug (see above).
@@ -113,8 +108,7 @@
 +
 As noted above in the section about cache changes, the disk cache is now completely optional.
 
-Bug Fixes
----------
+== Bug Fixes
 * GERRIT-5    Remove PatchSetInfo from database and get it always fr...
 +
 A very, very old bug.  We no longer mirror the commit data into the SQL database, but instead pull it directly from Git when needed.  Removing duplicated data simplifies the data store model, something that is important as we shift from an SQL database to a Git backed database.
@@ -139,8 +133,7 @@
 +
 The database schema changed, adding `patch_set_id` to the approval object, and renaming the approval table to `patch_set_approvals`.  If you have external code writing to this table, uh, sorry, its broken with this release, you'll have to update that code first.  :-\
 
-Other Changes
--------------
+== Other Changes
 
 This release is really massive because the internal code moved from some really ugly static data variables to doing almost everything through Guice injection.  Nothing user visible, but code cleanup that needed to occur before we started making additional changes to the system.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.0.19.txt b/ReleaseNotes/ReleaseNotes-2.0.19.txt
index 0e114c8..c9d9c56 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.19.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.19.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.19, 2.0.19.1, 2.0.19.2
-===================================================
+= Release notes for Gerrit 2.0.19, 2.0.19.1, 2.0.19.2
 
 Gerrit 2.0.19.2 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Important Notices
------------------
+== Important Notices
 
 * Prior User Sessions
 +
@@ -25,8 +23,7 @@
 set [http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html#cache.directory cache.directory] in gerrit.config.  This allows Gerrit to flush the set
 of active sessions to disk during shutdown, and load them back during startup.
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING: This version contains a schema change* (since 2.0.18)
 
@@ -44,8 +41,7 @@
 ----
 
 
-New Features
-------------
+== New Features
 * New ssh create-project command
 +
 Thanks to Ulrik Sjölin we now have `gerrit create-project`
@@ -171,8 +167,7 @@
 For more details, please see the docs:
 link:http://gerrit.googlecode.com/svn/documentation/2.0/user-changeid.html[http://gerrit.googlecode.com/svn/documentation/2.0/user-changeid.html]
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix yet another ArrayIndexOutOfBounds during side-by-s...
 +
 We found yet another bug with the side-by-side view failing
@@ -230,8 +225,7 @@
 wrong project, e.g. uploading a replacement commit to project
 B while picking a change number from project A.  Fixed.
 
-=Fixes in 2.0.19.1=
--------------------
+== =Fixes in 2.0.19.1=
 
 * Fix NPE during direct push to branch closing a change
 +
@@ -258,8 +252,7 @@
 +
 HTTP_LDAP broke using local usernames to match an account.  Fixed.
 
-=Fixes in 2.0.19.2=
--------------------
+== =Fixes in 2.0.19.2=
 * Don't line wrap project or group names in admin panels
 +
 Line wrapping group names like "All Users" when the description column
@@ -283,8 +276,7 @@
 As reported on repo-discuss, recursive search is sometimes necessary,
 and is now the default.
 
-Removed Features
-----------------
+== Removed Features
 
 * Remove support for /user/email style URLs
 +
@@ -292,8 +284,7 @@
 discoverable.  Its unlikely anyone is really using it, but if
 they are, they could try using "#q,owner:email,n,z" instead.
 
-Other Changes
--------------
+== Other Changes
 
 * Start 2.0.19 development
 * Document the Failure and UnloggedFailure classes in Ba...
diff --git a/ReleaseNotes/ReleaseNotes-2.0.2.txt b/ReleaseNotes/ReleaseNotes-2.0.2.txt
index b2d5b98..eb8546c 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.2.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.2
-==============================
+= Release notes for Gerrit 2.0.2
 
 Gerrit 2.0.2 is now available for download:
 
 link:https://www.gerritcodereview.com/[https://www.gerritcodereview.com/]
 
-Important Notes
----------------
+== Important Notes
 
 Starting with this version, Gerrit is now packaged as a single WAR file.
 Just download and drop into your webapps directory for easier deployment.
@@ -31,16 +29,14 @@
 and insert it into your container's CLASSPATH.  But I think all known
 instances are on PostgreSQL, so this is probably not a concern to anyone.
 
-New Features
-------------
+== New Features
 
 * Trailing whitespace is highlighted in diff views
 * SSHD upgraded with "faster connection" patch discussed on list
 * Git reflogs now contain the Gerrit account information of who did the push
 * Insanely long change subjects are now clipped at 80 characters
 
-All Changes
------------
+== All Changes
 
 * Switch back to -SNAPSHOT builds
 * Overhaul our build system to only create a WAR file
diff --git a/ReleaseNotes/ReleaseNotes-2.0.20.txt b/ReleaseNotes/ReleaseNotes-2.0.20.txt
index 527de8e..4f15bb0 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.20.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.20.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.20
-===============================
+= Release notes for Gerrit 2.0.20
 
 Gerrit 2.0.20 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 A prior bug (GERRIT-262) permitted some invalid data to enter into some databases.  Administrators should consider running the following update statement as part of their upgrade to .20 to make any comments which were created with this bug visible:
 ----
@@ -14,8 +12,7 @@
 ----
 Unfortunately the correct position of the comment has been lost, and the statement above will simply position them on the first line of the file.  Fortunately the lost comments were only on the wrong side of an insertion or deletion, and are generally rare.  (On my servers only 0.33% of the comments were created like this.)
 
-New Features
-------------
+== New Features
 * New ssh command approve
 +
 Patch sets can now be approved remotely via SSH.  For more
@@ -31,8 +28,7 @@
 administrators may permit automatically updating an existing
 account with a new identity by matching on the email address.
 
-Bug Fixes
----------
+== Bug Fixes
 * GERRIT-262  Disallow creating comments on line 0
 +
 Users were able to create comments in dead regions of a file.
@@ -59,8 +55,7 @@
 +
 MySQL schema upgrade scripts had a few bugs, fixed.
 
-Other Changes
--------------
+== Other Changes
 * Restart 2.0.20
 * Update MINA SSHD to 0.2.0 release
 * Update args4j to snapshot built from current CVS
diff --git a/ReleaseNotes/ReleaseNotes-2.0.21.txt b/ReleaseNotes/ReleaseNotes-2.0.21.txt
index 34ab581..5de84ff 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.21.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.21.txt
@@ -1,13 +1,11 @@
-Release notes for Gerrit 2.0.21
-===============================
+= Release notes for Gerrit 2.0.21
 
 Gerrit 2.0.21 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING: This version contains a schema change* (since 2.0.19)
 
@@ -48,8 +46,7 @@
 ----
 
 
-Important Notices
------------------
+== Important Notices
 
 * Prior User Sessions
 +
@@ -106,8 +103,7 @@
 a supported provider.  I just want to make it clear that I no
 longer recommend it in production.
 
-New Features
-------------
+== New Features
 
 * GERRIT-189  Show approval status in account dashboards
 +
@@ -203,8 +199,7 @@
 to `USER` in gerrit.config.  For more details see
 link:http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html#sendemail.from[sendemail.from]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Fix ReviewDb to actually be per-request scoped
 +
@@ -276,8 +271,7 @@
 the same email address, or to have the same OpenID auth token.
 Fixed by asserting a unique constraint on the column.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.21 development
 * Support cleaning up a Commons DBCP connection pool
 * Clarify which Factory we are importing in ApproveComma...
diff --git a/ReleaseNotes/ReleaseNotes-2.0.22.txt b/ReleaseNotes/ReleaseNotes-2.0.22.txt
index faaca81..5e2f8b5 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.22.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.22.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.22
-===============================
+= Release notes for Gerrit 2.0.22
 
 Gerrit 2.0.22 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 There is no schema change in this release.
 
@@ -32,8 +30,7 @@
    login over SSH until they return and select a new name.
 
 
-New Features
-------------
+== New Features
 * GERRIT-280  create-project: Add --branch and cleanup arguments
 +
 The --branch option to create-project can be used to setup the
@@ -94,8 +91,7 @@
 +
 Sample git pull lines are now included in email notifications.
 
-Bug Fixes
----------
+== Bug Fixes
 * create-project: Document needing to double quote descr...
 +
 The --description flag to create-project require two levels
@@ -141,8 +137,7 @@
 Merge commits created by Gerrit were still using the older style
 integer change number; changed to use the abbreviated Change-Id.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.22 development
 * Configure Maven to build with UTF-8 encoding
 * Document minimum build requirement for Mac OS X
diff --git a/ReleaseNotes/ReleaseNotes-2.0.23.txt b/ReleaseNotes/ReleaseNotes-2.0.23.txt
index 16488d4..a3f28a7 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.23.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.23.txt
@@ -1,18 +1,15 @@
-Release notes for Gerrit 2.0.23
-===============================
+= Release notes for Gerrit 2.0.23
 
 Gerrit 2.0.23 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 There is no schema change in this release.
 
 
-New Features
-------------
+== New Features
 
 * Adding support to list merged and abandoned changes
 +
@@ -22,8 +19,7 @@
 changes in the same project while merged changes link to all merged
 changes in the same project.  These links are bookmarkable.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Fix new change email to always have SSH pull URL
 * Move git pull URL to bottom of email notifications
@@ -39,8 +35,7 @@
 
 * Fix MySQL CREATE USER example in install documentation
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.23 development
 * Move Jetty 6.x resources into a jetty6 directory
 * Move the Jetty 6.x start script to our extra directory
diff --git a/ReleaseNotes/ReleaseNotes-2.0.24.txt b/ReleaseNotes/ReleaseNotes-2.0.24.txt
index 1f08582..7da1693 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.24.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.24.txt
@@ -1,13 +1,11 @@
-Release notes for Gerrit 2.0.24, 2.0.24.1, 2.0.24.2
-===================================================
+= Release notes for Gerrit 2.0.24, 2.0.24.1, 2.0.24.2
 
 Gerrit 2.0.24 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING: This version contains a schema change* (since 2.0.21)
 
@@ -18,8 +16,7 @@
 ----
 
 
-LDAP Change
------------
+== LDAP Change
 
 LDAP groups are now bound via their full distinguished name, and not
 by their common name.  Sites using LDAP groups will need to have the
@@ -34,8 +31,7 @@
 create identically named groups.
 
 
-New Features
-------------
+== New Features
 * Check if the user has permission to upload changes
 +
 The new READ +2 permission is required to upload a change to a
@@ -69,8 +65,7 @@
 Encrypted SMTP is now supported natively within Gerrit, see
 link:http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html#sendemail.smtpEncryption[sendemail.smtpEncryption]
 
-Bug Fixes
----------
+== Bug Fixes
 * issue 290    Fix invalid drop index in upgrade017_018_mysql
 +
 Minor syntax error in SQL script.
@@ -142,8 +137,7 @@
 identities when the claimed identity is just a delegate to the
 delegate provider.  We now store both in the account.
 
-Fixes in 2.0.24.1
------------------
+== Fixes in 2.0.24.1
 * Fix unused import in OpenIdServiceImpl
 * dev-readme: Fix formatting of initdb command
 +
@@ -159,8 +153,7 @@
 Fixes sendemail configuration to use the documented smtppass
 variable and not the undocumented smtpuserpass variable.
 
-Fixes in 2.0.24.2
------------------
+== Fixes in 2.0.24.2
 * Fix CreateSchema to create Administrators group
 * Fix CreateSchema to set type of Registered Users group
 * Default AccountGroup instances to type INTERNAL
@@ -180,8 +173,7 @@
 Added unit tests to validate CreateSchema works properly, so we
 don't have a repeat of breakage here.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.24 development
 * Merge change Ie16b8ca2
 * Switch to the new org.eclipse.jgit package
diff --git a/ReleaseNotes/ReleaseNotes-2.0.3.txt b/ReleaseNotes/ReleaseNotes-2.0.3.txt
index 6bf3510..d319b35 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.3.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.3
-==============================
+= Release notes for Gerrit 2.0.3
 
 Gerrit 2.0.3 is now available in the usual location:
 
@@ -10,8 +9,7 @@
 existing change".  This has been an open issue in the bug tracker for a
 while, and its finally closed thanks to his work.
 
-New Features
-------------
+== New Features
 
 * GERRIT-37  Add additional reviewers to an existing change
 * Display old and new image line numbers in unified diff
@@ -19,8 +17,7 @@
 * Allow up/down arrow keys to scroll the page in patch view
 * Use a Java applet to help users load public SSH keys
 
-Bug Fixes
----------
+== Bug Fixes
 
 * GERRIT-72  Make review comments standout more from the surrounding text
 * GERRIT-7   Restart the merge queue when Gerrit starts up
@@ -36,8 +33,7 @@
 Gerrit UI.  Such a display might be able to convince a user they are
 clicking on one thing, while doing something else entirely.
 
-Other Changes
--------------
+== Other Changes
 
 * Restore -SNAPSHOT suffix after 2.0.2
 * Add a document describing Gerrit's high level design
diff --git a/ReleaseNotes/ReleaseNotes-2.0.4.txt b/ReleaseNotes/ReleaseNotes-2.0.4.txt
index fec2425..0b10756 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.4.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.4
-==============================
+= Release notes for Gerrit 2.0.4
 
 Gerrit 2.0.4 is now available in the usual location:
 
@@ -31,8 +30,7 @@
 individual user's privacy by strongly encrypting their contact
 information, and storing it "off site".
 
-Other Changes
--------------
+== Other Changes
 * Change to 2.0.3-SNAPSHOT
 * Correct grammar in the patch conflict messages
 * Document how to create branches through SSH and web
diff --git a/ReleaseNotes/ReleaseNotes-2.0.5.txt b/ReleaseNotes/ReleaseNotes-2.0.5.txt
index 70116d3..8006e12 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.5.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.5
-==============================
+= Release notes for Gerrit 2.0.5
 
 Gerrit 2.0.5 is now available in the usual location:
 
@@ -15,8 +14,7 @@
 
 link:http://gerrit.googlecode.com/svn/documentation/2.0/config-sso.html[http://gerrit.googlecode.com/svn/documentation/2.0/config-sso.html]
 
-New Features
-------------
+== New Features
 
 * GERRIT-62  Work around IE6's inability to set innerHTML on a tbody ...
 * GERRIT-62  Upgrade to gwtjsonrpc 1.0.2 for ie6 support
@@ -35,14 +33,12 @@
 +
 These features make it easier to copy patch download commands.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * GERRIT-79  Error out with more useful message on "push :refs/change...
 * Invalidate all SSH keys when otherwise flushing all cach...
 
-Other Changes
--------------
+== Other Changes
 
 * Set version 2.0.4-SNAPSHOT
 * Correct note in developer setup about building SSHD
diff --git a/ReleaseNotes/ReleaseNotes-2.0.6.txt b/ReleaseNotes/ReleaseNotes-2.0.6.txt
index 9d0af33..1e28da8 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.6.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.0.6
-==============================
+= Release notes for Gerrit 2.0.6
 
 Gerrit 2.0.6 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-New Features
-------------
+== New Features
 
 * GERRIT-41  Add support for abandoning a dead change
 +
@@ -14,8 +12,7 @@
 
 * Bold substrings which match query when showing completi...
 
-Bug Fixes
----------
+== Bug Fixes
 
 * GERRIT-43  Work around Safari 3.2.1 OpenID login problems
 * GERRIT-43  Suggest boosting the headerBufferSize when deploying un...
@@ -24,8 +21,7 @@
 * GERRIT-76  Upgrade to JGit 0.4.0-209-g9c26a41
 * Ensure branches modified through web UI replicate
 
-Other Changes
--------------
+== Other Changes
 
 * Start 2.0.6 development
 * Generate the id for the iframe used during OpenID login
diff --git a/ReleaseNotes/ReleaseNotes-2.0.7.txt b/ReleaseNotes/ReleaseNotes-2.0.7.txt
index afc7784..d1bc38f 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.7.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.7
-==============================
+= Release notes for Gerrit 2.0.7
 
 Gerrit 2.0.7 is now available in the usual location:
 
@@ -11,15 +10,13 @@
 
 Gerrit is still Apache 2/MIT/BSD licensed, despite the switch of a dependency.
 
-New Features
-------------
+== New Features
 
 * GERRIT-103  Display our server host keys for the client to copy an...
 +
 For the paranoid user, they can check the key fingerprint, or even copy the complete host key line for ~/.ssh/known_hosts, directly from Settings > SSH Keys.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * GERRIT-98   Require that a change be open in order to abandon it
 * GERRIT-101  Switch OpenID relying party to openid4java
@@ -34,8 +31,7 @@
 
 * Fix a NullPointerException in OpenIdServiceImpl on res...
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.7 development
 * Upgrade JGit to 0.4.0-212-g9057f1b
 * Make the sign in dialog a bit taller to avoid clipping...
diff --git a/ReleaseNotes/ReleaseNotes-2.0.8.txt b/ReleaseNotes/ReleaseNotes-2.0.8.txt
index 4b2d10a5..89e7fdd 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.8.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.8
-==============================
+= Release notes for Gerrit 2.0.8
 
 Gerrit 2.0.8 is now available in the usual location:
 
@@ -14,8 +13,7 @@
 This version has some major bug fixes for JGit.  I strongly encourage people to upgrade, we had a number of JGit bugs identified last week, all of them should be fixed in this release.
 
 
-New Features
-------------
+== New Features
 * Allow users to subscribe to submitted change events
 +
 Someone asked me on an IRC channel to have Gerrit send emails when changes are actually merged into a project.  This is what triggered the schema change; there is a new checkbox on the Watched Projects list under Settings to subscribe to these email notifications.
@@ -33,15 +31,13 @@
 +
 The reflogs now contain the remote user's IP address when Gerrit makes edits, resulting in slightly more detail than was there before.
 
-Bug Fixes
----------
+== Bug Fixes
 * Make sure only valid ObjectIds can be passed into git d...
 * GERRIT-92  Upgrade JGit to 0.4.0-262-g3c268c8
 +
 The JGit bug fixes are rather major.  I would strongly encourage upgrading.
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.8 development
 * Upgrade MINA SSHD to SVN trunk 755651
 * Fix a minor whitespace error in ChangeMail
diff --git a/ReleaseNotes/ReleaseNotes-2.0.9.txt b/ReleaseNotes/ReleaseNotes-2.0.9.txt
index d2a9196..1f683cf 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.9.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.9.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.0.9
-==============================
+= Release notes for Gerrit 2.0.9
 
 Gerrit 2.0.9 is now available in the usual location:
 
@@ -20,8 +19,7 @@
 
 The SQL statement to insert a new project into the database has been changed.  Please see [http://gerrit.googlecode.com/svn/documentation/2.0/project-setup.html Project Setup] for the modified statement.
 
-New Features
-------------
+== New Features
 * GERRIT-69   Make the merge commit message more detailed when mergi...
 * Show the user's starred/not-starred icon in the change...
 * Modify Push Annotated Tag to require signed tags, or r...
@@ -34,8 +32,7 @@
 These last two changes move the hidden gerrit.fastforwardonly feature to the database and the user interface, so project owners can make use of it (or not).  Please see the new 'Change Submit Action' section in the user documentation:
 link:http://gerrit.googlecode.com/svn/documentation/2.0/project-setup.html[http://gerrit.googlecode.com/svn/documentation/2.0/project-setup.html]
 
-Bug Fixes
----------
+== Bug Fixes
 * Work around focus bugs in WebKit based browsers
 * Include our license list in the WAR file
 * Whack any prior submit approvals by myself when replac...
@@ -43,8 +40,7 @@
 * GERRIT-85   ie6: Correct rendering of commit messages
 * GERRIT-89   ie6: Fix date line wrapping in messages
 
-Other Changes
--------------
+== Other Changes
 * Start 2.0.9 development
 * Always show the commit SHA-1 next to the patch set hea...
 * Silence more non-critical log messages from openid4java
diff --git a/ReleaseNotes/ReleaseNotes-2.1.1.txt b/ReleaseNotes/ReleaseNotes-2.1.1.txt
index 9d795b6..38b6caf 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.1.txt
@@ -1,20 +1,17 @@
-Release notes for Gerrit 2.1.1, 2.1.1.1
-=======================================
+= Release notes for Gerrit 2.1.1, 2.1.1.1
 
 Gerrit 2.1.1.1 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING* This release contains a schema change.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
 ----
 
-Patch 2.1.1.1
--------------
+== Patch 2.1.1.1
 
 * Update MINA SSHD to SVN 897374
 +
@@ -28,8 +25,7 @@
 Discarding a comment from the publish comments screen caused
 a ConcurrentModificationException.  Fixed.
 
-New Features
-------------
+== New Features
 
 * issue 322    Update to GWT 2.0.0
 +
@@ -106,8 +102,7 @@
 by an accidental click.  This is especially useful when there
 is a merge error during submit.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * issue 359    Allow updates of commits where only the parent changes
 +
@@ -158,8 +153,7 @@
 to leak file descriptors, as pipes to the external CGI were not
 always closed.  Fixed.
 
-Other
------
+== Other
 * Switch to ClientBundle
 * Update to gwtexpui-1.2.0-SNAPSHOT
 * Merge branch 'master' into gwt-2.0
diff --git a/ReleaseNotes/ReleaseNotes-2.1.10.txt b/ReleaseNotes/ReleaseNotes-2.1.10.txt
index 5464267..5c5bcc6 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.10.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.10.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.10
-===============================
+= Release notes for Gerrit 2.1.10
 
 There are no schema changes from link:ReleaseNotes-2.1.9.html[2.1.9].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.10.war[https://www.gerritcodereview.com/download/gerrit-2.1.10.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix clone for modern Git clients
 +
 The security fix in 2.1.9 broke clone for recent Git clients,
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.1.txt b/ReleaseNotes/ReleaseNotes-2.1.2.1.txt
index ae5d912..b181fee 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.1.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.2.1
-================================
+= Release notes for Gerrit 2.1.2.1
 
 Gerrit 2.1.2.1 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Include smart http:// URLs in gitweb
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.2.txt b/ReleaseNotes/ReleaseNotes-2.1.2.2.txt
index 6565833..305e3e1 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.2.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.2.2
-================================
+= Release notes for Gerrit 2.1.2.2
 
 Gerrit 2.1.2.2 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Add ',' to be encoded in email headers.
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.3.txt b/ReleaseNotes/ReleaseNotes-2.1.2.3.txt
index 3cfbdd1..f81092c 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.3.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.2.3
-================================
+= Release notes for Gerrit 2.1.2.3
 
 Gerrit 2.1.2.3 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * issue 528 gsql: Fix escaping of quotes in JSON
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.4.txt b/ReleaseNotes/ReleaseNotes-2.1.2.4.txt
index 5e863f7..45fcb40 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.4.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.2.4
-================================
+= Release notes for Gerrit 2.1.2.4
 
 Gerrit 2.1.2.4 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-New Features
-------------
+== New Features
 
 * Add 'checkout' download command to patch sets
 +
@@ -14,8 +12,7 @@
 and checkout the patch set on a detached HEAD.  This is more suitable
 for building and testing the change locally.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * issue 545 Fallback to ISO-8859-1 if charset isn't supported
 +
@@ -45,8 +42,7 @@
 in the directory, Gerrit crashed during sign-in while trying to
 clear out the user name.  Fixed.
 
-Documentation Corrections
-~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Documentation Corrections
 
 * documentation: Elaborate on branch level Owner
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.5.txt b/ReleaseNotes/ReleaseNotes-2.1.2.5.txt
index 6e9a49e..eece1e7 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.5.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.2.5
-================================
+= Release notes for Gerrit 2.1.2.5
 
 Gerrit 2.1.2.5 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * issue 390 Resolve objects going missing
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.txt b/ReleaseNotes/ReleaseNotes-2.1.2.txt
index e0d8c12..8e7cd5c 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.2
-==============================
+= Release notes for Gerrit 2.1.2
 
 Gerrit 2.1.2 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING* This release contains multiple schema changes.  To upgrade:
 ----
@@ -14,8 +12,7 @@
 ----
 
 
-Breakages
----------
+== Breakages
 
 * issue 421 Force validation of the author and committer lines
 +
@@ -33,11 +30,9 @@
 exists, and Forge Identity +2 where Push Branch >= +1 exists.
 
 
-New Features
-------------
+== New Features
 
-UI - Diff Viewer
-~~~~~~~~~~~~~~~~
+=== UI - Diff Viewer
 
 * issue 169 Highlight line-level (aka word) differences in files
 +
@@ -110,8 +105,7 @@
 * Use RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK for tabs
 * Use a tooltip to explain whitespace errors
 
-UI - Other
-~~~~~~~~~~
+=== UI - Other
 
 * issue 408 Show summary of code review, verified on all open changes
 +
@@ -153,8 +147,7 @@
 Site administrators can now theme the UI with local site colors
 by setting theme variables in gerrit.config.
 
-Permissions
-~~~~~~~~~~~
+=== Permissions
 
 * issue 60 Change permissions to be branch based
 +
@@ -172,8 +165,7 @@
 See link:http://gerrit.googlecode.com/svn/documentation/2.1.2/access-control.html#function_MaxNoBlock[access control]
 for more details on this function.
 
-Remote Access
-~~~~~~~~~~~~~
+=== Remote Access
 
 * Enable smart HTTP under /p/ URLs
 +
@@ -210,8 +202,7 @@
 addition to single quotes.  This can make it easier to intermix
 quoting styles with the shell that is calling the SSH client .
 
-Server Administration
-~~~~~~~~~~~~~~~~~~~~~
+=== Server Administration
 
 * issue 383 Add event hook support
 +
@@ -272,8 +263,7 @@
 software driven queries over SSH easier.  The -c option accepts
 one query, executes it, and returns.
 
-Other
-~~~~~
+=== Other
 
 * Warn when a commit message isn't wrapped
 +
@@ -290,11 +280,9 @@
 rather than from the user's preferred account information.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
-UI
-~~
+=== UI
 
 * Change "Publish Comments" to "Review"
 +
@@ -380,8 +368,7 @@
 * Update URL for GitHub's SSH key guide
 * issue 314 Hide group type choice if LDAP is not enabled
 
-Email
-~~~~~
+=== Email
 
 * Send missing dependencies to owners if they are the only reviewer
 +
@@ -401,8 +388,7 @@
 An additional from line is now injected at the start of the email
 body to indicate the actual user.
 
-Remote Access
-~~~~~~~~~~~~~
+=== Remote Access
 
 * issue 385 Delete session cookie when session is expired
 +
@@ -443,8 +429,7 @@
 whitespace, removing a common source of typos that lead to users
 being automatically assigned more than one Gerrit user account.
 
-Server Administration
-~~~~~~~~~~~~~~~~~~~~~
+=== Server Administration
 
 * daemon: Really allow httpd.listenUrl to end with /
 +
@@ -532,8 +517,7 @@
 sometimes failed.  Fixed by executing an implicit reload in these
 cases, reducing the number of times a user sees a failure.
 
-Development
-~~~~~~~~~~~
+=== Development
 
 * issue 427 Adjust SocketUtilTest to be more likely to pass
 +
@@ -569,8 +553,7 @@
 removed from the license file.
 
 
-Schema Changes in Detail
-------------------------
+== Schema Changes in Detail
 
 * Remove Project.Id and use only Project.NameKey
 +
@@ -594,8 +577,7 @@
 aren't possible, or to run on MySQL MyISAM tables.
 
 
-Other Changes
--------------
+== Other Changes
 * Update gwtorm to 1.1.4-SNAPSHOT
 * Add unique column ids to every column
 * Remove unused byName @SecondaryKey from ApprovalCategory
diff --git a/ReleaseNotes/ReleaseNotes-2.1.3.txt b/ReleaseNotes/ReleaseNotes-2.1.3.txt
index f4faf32..6226b93 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.3.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.3
-==============================
+= Release notes for Gerrit 2.1.3
 
 Gerrit 2.1.3 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING* This release contains multiple schema changes.  To upgrade:
 ----
@@ -14,11 +12,9 @@
 ----
 
 
-New Features
-------------
+== New Features
 
-Web UI
-~~~~~~
+=== Web UI
 
 * issue 289 Remove reviewers (or self) from a change
 +
@@ -63,8 +59,7 @@
 Access tabs.  Editing is obviously disabled, unless the user has
 owner level access to the project, or one of its branches.
 
-Access Controls
-~~~~~~~~~~~~~~~
+=== Access Controls
 
 * Branch-level read access is now supported
 +
@@ -97,8 +92,7 @@
 inherited by default, but the old exclusive behavior can be obtained
 by prefixing the reference with '-'.
 
-SSH Commands
-~~~~~~~~~~~~
+=== SSH Commands
 
 * create-account: Permit creation of batch user accounts over SSH
 * issue 269 Enable create-project for non-Administrators
@@ -125,8 +119,7 @@
 The old `gerrit approve` name will be kept around as an alias to
 provide time to migrate hooks/scripts/etc.
 
-Hooks / Stream Events
-~~~~~~~~~~~~~~~~~~~~~
+=== Hooks / Stream Events
 
 * \--change-url parameter passed to hooks
 +
@@ -147,13 +140,11 @@
 set is now included in the stream-events record, making it possible
 for a monitor to easily pull down a patch set and compile it.
 
-Contrib
-~~~~~~~
+=== Contrib
 
 * Example hook to auto-re-approve a trivial rebase
 
-Misc.
-~~~~~
+=== Misc.
 
 * transfer.timeout: Support configurable timeouts for dead clients
 +
@@ -195,11 +186,9 @@
 Apache Commons DBCP to 1.4.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
-Web UI
-~~~~~~
+=== Web UI
 
 * issue 396 Prevent 'no-score' approvals from being recorded
 +
@@ -237,8 +226,7 @@
 'Register New Email...' button.  A cancel button was added to
 close the dialog.
 
-Server Programs
-~~~~~~~~~~~~~~~
+=== Server Programs
 
 * init: Import non-standardly named Git repositories
 +
@@ -265,8 +253,7 @@
 were not properly logged by `gerrit approve` (now gerrit review).
 Fixed by logging the root cause of the failure.
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 * Display error when HTTP authentication isn't configured
 +
@@ -282,8 +269,7 @@
 during sign-in.  Administrators can enable following by adding
 `ldap.referral = follow` to their gerrit.config file.
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
 * documentation: Clarified the ownership of '\-- All Projects \--'
 +
@@ -308,7 +294,6 @@
 the current stable version of the Maven plugin.
 
 
-Version
--------
+== Version
 
 e8fd49f5f7481e2f916cb0d8cfbada79309562b4
diff --git a/ReleaseNotes/ReleaseNotes-2.1.4.txt b/ReleaseNotes/ReleaseNotes-2.1.4.txt
index 3e25163..72eec55 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.4.txt
@@ -1,23 +1,19 @@
-Release notes for Gerrit 2.1.4
-==============================
+= Release notes for Gerrit 2.1.4
 
 Gerrit 2.1.4 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING* This release contains multiple schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
 ----
 
-New Features
-------------
+== New Features
 
-Change Management
-~~~~~~~~~~~~~~~~~
+=== Change Management
 
 * issue 504 Implement full query operators
 +
@@ -51,8 +47,7 @@
 watched project list, and a new menu item was added under the My menu
 to select open changes matching these watched projects.
 
-Web UI
-~~~~~~
+=== Web UI
 
 * issue 579 Remember diff formatting preferences
 +
@@ -82,8 +77,7 @@
 
 * issue 509 Make branch columns link to changes on that branch
 
-Email Notifications
-~~~~~~~~~~~~~~~~~~~
+=== Email Notifications
 
 * issue 311 No longer CC a user by default
 +
@@ -116,8 +110,7 @@
 New fields in the email footer provide additional detail, enabling
 better filtering and classification of messages.
 
-Access Control
-~~~~~~~~~~~~~~
+=== Access Control
 
 * Support regular expressions for ref access rules
 +
@@ -136,8 +129,7 @@
 Groups can now be created over SSH by administrators using the
 `gerrit create-group` command.
 
-Authentication
-~~~~~~~~~~~~~~
+=== Authentication
 
 * Remove password authentication over SSH
 +
@@ -157,8 +149,7 @@
 setting, rather than expiring when the browser closes.  (Previously
 sessions expired when the browser exited.)
 
-Misc.
-~~~~~
+=== Misc.
 
 * Add topic, lastUpdated, sortKey to ChangeAttribute
 +
@@ -184,11 +175,9 @@
 Updated JGit to 0.8.4.89-ge2f5716, log4j to 1.2.16, GWT to 2.0.4,
 sfl4j to 1.6.1, easymock to 3.0, JUnit to 4.8.1.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Web UI
-~~~~~~
+=== Web UI
 
 * issue 352 Confirm branch deletion in web UI
 +
@@ -211,15 +200,13 @@
 Keyboard navigation to standard links like 'Google Accounts'
 wasn't supported.  Fixed.
 
-Misc.
-~~~~~
+=== Misc.
 
 * issue 614 Fix 503 error when Jetty cancels a request
 +
 A bug was introduced in 2.1.3 that caused a server 503 error
 when a fetch/pull/clone or push request timed out.  Fixed.
 
-Version
--------
+== Version
 
 ae59d1bf232bba16d4d03ca924884234c68be0f2
diff --git a/ReleaseNotes/ReleaseNotes-2.1.5.txt b/ReleaseNotes/ReleaseNotes-2.1.5.txt
index 4934223..88288e2 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.5.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.1.5
-==============================
+= Release notes for Gerrit 2.1.5
 
 Gerrit 2.1.5 is now available:
 
@@ -8,8 +7,7 @@
 This is primarily a bug fix release to 2.1.4, but some additional
 new features were included so its named 2.1.5 rather than 2.1.4.1.
 
-Upgrade Instructions
---------------------
+== Upgrade Instructions
 
 If upgrading from version 2.1.4, simply replace the WAR file in
 `'site_path'/bin/gerrit.war` and restart Gerrit.
@@ -18,11 +16,9 @@
 `java -jar gerrit.war init -d 'site_path'` to upgrade the schema,
 and restart Gerrit.
 
-New Features
-------------
+== New Features
 
-Web UI
-~~~~~~
+=== Web UI
 * issue 361 Enable commenting on commit messages
 +
 The commit message of a change can now be commented on inline, and
@@ -49,8 +45,7 @@
 A 'diffstat' is shown for each file, summarizing the size of the
 change on that file in terms of number of lines added or deleted.
 
-Email Notifications
-~~~~~~~~~~~~~~~~~~~
+=== Email Notifications
 * issue 452 Include a quick summary of the size of a change in email
 +
 After the file listing, a summary totaling the number of files
@@ -58,11 +53,9 @@
 help reviewers to get a quick estimation on the time required for
 them to review the change.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Web UI
-~~~~~~
+=== Web UI
 * issue 639 Fix keyboard shortcuts under Chrome/Safari
 +
 Keyboard shortcuts didn't work properly on modern WebKit browsers
@@ -91,8 +84,7 @@
 is present and will toggle the user's starred flag for that change.
 Fixed.
 
-Access Control
-~~~~~~~~~~~~~~
+=== Access Control
 * issue 672 Fix branch owner adding exclusive ACL
 +
 Branch owners could not add exclusive ACLs within their branch
@@ -118,8 +110,7 @@
 bug which failed to consider the project inheritance if any branch
 (not just the one being uploaded to) denied upload access.
 
-Misc.
-~~~~~
+=== Misc.
 * issue 641 Don't pass null arguments to hooks
 +
 Some hooks crashed inside of the server during invocation because the
@@ -162,12 +153,10 @@
 Gerrit Code Review was only accepting the ';' syntax.  Fixed
 to support both.
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 * Fixed example for gerrit create-account.
 * gerrit.sh: Correct /etc/default path in error message
 
-Version
--------
+== Version
 
 2765ff9e5f821100e9ca671f4d502b5c938457a5
diff --git a/ReleaseNotes/ReleaseNotes-2.1.6.1.txt b/ReleaseNotes/ReleaseNotes-2.1.6.1.txt
index a490c0a..4626c7b 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.6.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.6.1.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.6.1
-================================
+= Release notes for Gerrit 2.1.6.1
 
 Gerrit 2.1.6.1 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.6.1.war[https://www.gerritcodereview.com/download/gerrit-2.1.6.1.war]
 
-Schema Change
--------------
+== Schema Change
 
 If upgrading from 2.1.6, there are no schema changes.  Replace the
 WAR and restart the daemon.
@@ -17,8 +15,7 @@
   java -jar gerrit.war init -d site_path
 ----
 
-New Features
-------------
+== New Features
 * Display the originator of each access rule
 +
 The project access panel now shows which project each rule inherits
@@ -37,8 +34,7 @@
 project, provided that the parent project is not the root level
 \-- All Projects \--.
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix disabled intraline difference checkbox
 +
 Intraline difference couldn't be enabled once it was disabled by
diff --git a/ReleaseNotes/ReleaseNotes-2.1.6.txt b/ReleaseNotes/ReleaseNotes-2.1.6.txt
index 520b2a6..83689e7 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.6.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.6
-==============================
+= Release notes for Gerrit 2.1.6
 
 Gerrit 2.1.6 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.6.war[https://www.gerritcodereview.com/download/gerrit-2.1.6.war]
 
-Schema Change
--------------
+== Schema Change
 
 *WARNING* This release contains multiple schema changes.  To upgrade:
 ----
@@ -14,11 +12,9 @@
 ----
 
 
-New Features
-------------
+== New Features
 
-Web UI
-~~~~~~
+=== Web UI
 * issue 312 Abandoned changes can now be restored.
 * issue 698 Make date and time fields customizable
 * issue 556 Preference to display patch sets in reverse order
@@ -39,8 +35,7 @@
 This is built upon experimental merge code inherited from JGit,
 and is therefore still experimental in Gerrit.
 
-Change Query
-~~~~~~~~~~~~
+=== Change Query
 * issue 688 Match branch, topic, project, ref by regular expressions
 +
 Similar to other features in Gerrit Code Review, starting any of these
@@ -80,8 +75,7 @@
 Gerrit change submission, Gerrit will now send a new "ref-updated"
 event to the event stream.
 
-User Management
-~~~~~~~~~~~~~~~
+=== User Management
 * SSO via client SSL certificates
 +
 A new auth.type of CLIENT_SSL_CERT_LDAP supports authenticating users
@@ -123,8 +117,7 @@
 The internal SSH daemon now supports additional configuration
 settings to reduce the risk of abuse.
 
-Administration
-~~~~~~~~~~~~~~
+=== Administration
 * issue 558 Allow Access rights to be edited by clicking on them.
 
 * New 'Project Owner' system group to define default rights
@@ -186,11 +179,9 @@
 prevent the older (pre-filter-branch) history from being reintroduced
 into the repository.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Web UI
-~~~~~~
+=== Web UI
 * issue 498 Enable Keyboard navigation after change submit
 * issue 691 Make ']' on last file go up to change
 * issue 741 Make ENTER work for 'Create Group'
@@ -224,13 +215,11 @@
 are not project owners may now only view access rights for branches
 they have at least READ +1 permission on.
 
-Change Query
-~~~~~~~~~~~~
+=== Change Query
 * issue 689 Fix age:4days to parse correctly
 * Make branch: operator slightly less ambiguous
 
-Push Support
-~~~~~~~~~~~~
+=== Push Support
 * issue 695 Permit changing only the author of a commit
 +
 Correcting only the author of a change failed to upload the new patch
@@ -257,8 +246,7 @@
 create changes for review were unable to push to a project.  Fixed.
 This (finally) makes Gerrit a replacement for Gitosis or Gitolite.
 
-Replication
-~~~~~~~~~~~
+=== Replication
 * issue 683 Don't assume authGroup = "Registered Users" in replication
 +
 Previously a misconfigured authGroup in replication.config may have
@@ -281,8 +269,7 @@
 
 * issue 658 Allow refspec shortcuts (push = master) for replication
 
-User Management
-~~~~~~~~~~~~~~~
+=== User Management
 * Ensure proper escaping of LDAP group names
 +
 Some special characters may appear in LDAP group names, these must be
@@ -295,8 +282,7 @@
 but cannot because it is already in use by another user on this
 server, the new account won't be created.
 
-Administration
-~~~~~~~~~~~~~~
+=== Administration
 * gerrit.sh: actually verify running processes
 +
 Previously `gerrit.sh check` claimed a server was running if the
@@ -317,6 +303,5 @@
 child project.  Permissions can now be overidden if the category,
 group name and reference name all match.
 
-Version
--------
+== Version
 ef16a1816f293d00c33de9f90470021e2468a709
diff --git a/ReleaseNotes/ReleaseNotes-2.1.7.2.txt b/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
index fdd7725..9c9e6e1 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.7.2
-================================
+= Release notes for Gerrit 2.1.7.2
 
 Gerrit 2.1.7.2 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.7.2.war[https://www.gerritcodereview.com/download/gerrit-2.1.7.2.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * issue 997 Resolve Project Owners when checking access rights
 +
 Members of the 'Project Owners' magical group did not always have
diff --git a/ReleaseNotes/ReleaseNotes-2.1.7.txt b/ReleaseNotes/ReleaseNotes-2.1.7.txt
index 5123279..ad440b5 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.7.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.7
-==============================
+= Release notes for Gerrit 2.1.7
 
 Gerrit 2.1.7 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.7.war[https://www.gerritcodereview.com/download/gerrit-2.1.7.war]
 
-Schema Change
--------------
+== Schema Change
 *WARNING* This release contains multiple schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -18,8 +16,7 @@
   java -jar gerrit.war ExportReviewNotes -d site_path
 ----
 
-Memory Usage Increase
----------------------
+== Memory Usage Increase
 *WARNING* The JGit delta base cache, whose size is controlled by
 `core.deltaBaseCacheLimit`, has changed in this release from being a
 JVM-wide singleton to per-thread. This alters the memory usage, going
@@ -27,17 +24,14 @@
 change improves performance on big repositories, but may need a larger
 `container.heapLimit` if the number of concurrent operations is high.
 
-New Features
-------------
+== New Features
 
-Change Data
-~~~~~~~~~~~
+=== Change Data
 * issue 64 Create Git notes for submitted changes
 +
 Git notes are automatically added to the `refs/notes/review`.
 
-Query
-~~~~~
+=== Query
 * Search project names by substring
 +
 Entering a word with no operator (for example `gerrit`) will be
@@ -49,8 +43,7 @@
 search for changes whose owner or that has a reviewer in (or not
 in if prefixed with `-`) the specified group.
 
-Web UI
-~~~~~~
+=== Web UI
 * Add reviewer/verifier name beside check/plus/minus
 +
 Change lists (such as from a search result, or in a user's dashboard)
@@ -90,17 +83,14 @@
 SSH public key files by hand.
 
 
-SSH Commands
-~~~~~~~~~~~~
+=== SSH Commands
 * issue 674 Add abandon/restore to `gerrit review`
 * Add `gerrit version` command
 
-Change Upload
-~~~~~~~~~~~~~
+=== Change Upload
 * Display a more verbose "you are not author/committer" message
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 * Detailed error message explanations
 +
 Most common error messages are now described in detail in the
@@ -111,8 +101,7 @@
 * issue 905 Document reverse proxy using Nginx
 * Updated system scaling data in 'System Design'
 
-Outgoing Mail
-~~~~~~~~~~~~~
+=== Outgoing Mail
 * Optionally add Importance and Expiry-Days headers
 +
 New gerrit.config variable `sendemail.importance` can be set to `high`
@@ -122,8 +111,7 @@
 
 * Add support for SMTP AUTH LOGIN
 
-Administration
-~~~~~~~~~~~~~~
+=== Administration
 * Group option to make group visible to all users
 +
 A new group option permits the group to be visible to all users,
@@ -181,8 +169,7 @@
 path used for the authentication cookie, which may be necessary if
 a reverse proxy maps requests to the managed gitweb.
 
-Replication
-~~~~~~~~~~~
+=== Replication
 * Add adminUrl to replication for repository creation
 +
 Replication remotes can be configured with `remote.name.adminUrl` to
@@ -196,8 +183,7 @@
 Replication can now be performed over an authenticated smart HTTP
 transport, in addition to anonymous Git and authenticated SSH.
 
-Misc.
-~~~~~
+=== Misc.
 * Alternative URL for Gerrit's managed Gitweb
 +
 The internal gitweb served from `/gitweb` can now appear to be from a
@@ -210,11 +196,9 @@
 to 1.6, Apache Commons Net to 2.2, Apache Commons Pool to 1.5.5, JGit
 to 0.12.1.53-g5ec4977, MINA SSHD to 0.5.1-r1095809.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Web UI
-~~~~~~
+=== Web UI
 * issue 853 Incorrect side-by-side display of modified lines
 +
 A bug in JGit lead to the side-by-side view displaying wrong and
@@ -261,12 +245,10 @@
 * Always display button text in black
 * Always disable content merge option if user can't change project
 
-commit-msg Hook
-~~~~~~~~~~~~~~~
+=== commit-msg Hook
 * issue 922 Fix commit-msg hook to run on Solaris
 
-Outgoing Mail
-~~~~~~~~~~~~~
+=== Outgoing Mail
 * issue 780 E-mail about failed merge should not use Anonymous Coward
 +
 Some email was sent as Anonymous Coward, even when the user had a
@@ -281,8 +263,7 @@
 * Do not email reviewers adding themselves as reviewers
 * Fix comma/space separation in email templates
 
-Pushing Changes
-~~~~~~~~~~~~~~~
+=== Pushing Changes
 * Avoid huge pushes during refs/for/BRANCH push
 +
 With Gerrit 2.1.6, clients started to push possibly hundreds of
@@ -356,8 +337,7 @@
 no mention if it on the server error log. Now it is reported so the
 site administrator also knows about it.
 
-SSH Commands
-~~~~~~~~~~~~
+=== SSH Commands
 * issue 755 Send new patchset event after its available
 * issue 814 Evict initial members of group created by SSH
 * issue 879 Fix replication of initial empty commit in new project
@@ -365,8 +345,7 @@
 * Automatically create user account(s) as necessary
 * Move SSH command creation off NioProcessor threads
 
-Administration
-~~~~~~~~~~~~~~
+=== Administration
 * Enable git reflog for all newly created projects
 +
 Previously branch updates were not being recorded in the native Git
@@ -388,8 +367,7 @@
 * gerrit.sh: Fix issues on Solaris
 * gerrit.sh: Support spaces in JAVA_HOME
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 * issue 800 documentation: Show example of review -m
 * issue 896 Clarify that $\{name\} is required for replication.
 * Fix spelling mistake in 'Searching Changes' documentation
diff --git a/ReleaseNotes/ReleaseNotes-2.1.8.txt b/ReleaseNotes/ReleaseNotes-2.1.8.txt
index 476e312..e1ed11c 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.8.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.8
-==============================
+= Release notes for Gerrit 2.1.8
 
 Gerrit 2.1.8 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.8.war[https://www.gerritcodereview.com/download/gerrit-2.1.8.war]
 
-New Features
-------------
+== New Features
 * Add cache for tag advertisements
 +
 When READ level access controls are used on references/branches, this
@@ -39,8 +37,7 @@
 MS-DOS compatibility may have permitted access to special device
 files in any directory, rather than just the "\\.\" device namespace.
 
-Bug Fixes
----------
+== Bug Fixes
 * issue 518 Fix MySQL counter resets
 +
 MySQL databases lost their change_id, account_id counters after
diff --git a/ReleaseNotes/ReleaseNotes-2.1.9.txt b/ReleaseNotes/ReleaseNotes-2.1.9.txt
index 2efc5b6..63bcb20 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.9.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.9.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.1.9
-==============================
+= Release notes for Gerrit 2.1.9
 
 There are no schema changes from link:ReleaseNotes-2.1.8.html[2.1.8].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.1.9.war[https://www.gerritcodereview.com/download/gerrit-2.1.9.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Patch JGit security hole
 +
 The security hole may permit a modified Git client to gain access
diff --git a/ReleaseNotes/ReleaseNotes-2.1.txt b/ReleaseNotes/ReleaseNotes-2.1.txt
index 127ab09..28cc90d 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.txt
@@ -1,20 +1,17 @@
-Release notes for Gerrit 2.1
-============================
+= Release notes for Gerrit 2.1
 
 Gerrit 2.1 is now available in the usual location:
 
 link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 
-New site_path Layout
---------------------
+== New site_path Layout
 
 The layout of the `$site_path` directory has been changed in 2.1.
 Configuration files are now stored within the `etc/` subdirectory
 and will be automatically moved there by the init subcommand.
 
-Upgrading From 2.0.x
---------------------
+== Upgrading From 2.0.x
 
   If the server is running a version older than 2.0.24, upgrade the
   database schema to the current schema version of 19.  Download
@@ -37,8 +34,7 @@
 sendemail.smtpPass and ldap.password out of gerrit.config into a
 read-protected secure.config file.
 
-New Daemon Mode
----------------
+== New Daemon Mode
 
 Gerrit 2.1 and later embeds the Jetty servlet container, and
 runs it automatically as part of `java -jar gerrit.war daemon`.
@@ -57,8 +53,7 @@
 link:http://gerrit.googlecode.com/svn/documentation/2.1/index.html[http://gerrit.googlecode.com/svn/documentation/2.1/index.html]
 
 
-New Features
-------------
+== New Features
 
 * issue 19     Link to issue tracker systems from commits
 +
@@ -184,8 +179,7 @@
 +
 Most dependencies were updated to their current stable versions.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * issue 259    Improve search hint to include owner:email
 +
@@ -268,8 +262,7 @@
 `$HOME/.gerritcodereview/tmp`, which should be isolated from
 the host system's /tmp cleaner.
 
-Other=
-------
+== Other=
 
 * Pick up gwtexpui 1.1.4-SNAPSHOT
 * Merge change Ia64286d3
diff --git a/ReleaseNotes/ReleaseNotes-2.10.1.txt b/ReleaseNotes/ReleaseNotes-2.10.1.txt
index 3065492..72d26d1 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.1
-===============================
+= Release notes for Gerrit 2.10.1
 
 There are no schema changes from link:ReleaseNotes-2.10.html[2.10].
 
@@ -7,8 +6,7 @@
 link:https://www.gerritcodereview.com/download/gerrit-2.10.1.war[
 https://www.gerritcodereview.com/download/gerrit-2.10.1.war]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2260[Issue 2260]:
 LDAP horrendous login time due to recursive lookup.
@@ -19,8 +17,7 @@
 * link:https://code.google.com/p/gerrit/issues/detail?id=3211[Issue 3211]:
 Intermittent Null Pointer Exception when showing process queue.
 
-LDAP
-----
+== LDAP
 
 * Several performance improvements when using LDAP, both in the number of LDAP
 requests and in the amount of data transferred.
@@ -28,13 +25,11 @@
 * Sites using LDAP for authentication but otherwise rely on local Gerrit groups
 should set the new `ldap.fetchMemberOfEagerly` option to `false`.
 
-OAuth
------
+== OAuth
 
 * Expose extension point for generic OAuth providers.
 
-OpenID
-------
+== OpenID
 
 * Add support for Launchpad on the login form.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.10.2.txt b/ReleaseNotes/ReleaseNotes-2.10.2.txt
index ac7c866..49be04e 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.2.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.2
-===============================
+= Release notes for Gerrit 2.10.2
 
 There are no schema changes from link:ReleaseNotes-2.10.1.html[2.10.1].
 
@@ -7,8 +6,7 @@
 link:https://www.gerritcodereview.com/download/gerrit-2.10.2.war[
 https://www.gerritcodereview.com/download/gerrit-2.10.2.war]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Work around MyersDiff infinite loop in PatchListLoader. If the MyersDiff diff
 doesn't finish within 5 seconds, interrupt it and fall back to a different diff
@@ -16,15 +14,13 @@
 loop is detected is that the files in the commit will not be compared in-depth,
 which will result in bigger edit regions.
 
-Secondary Index
----------------
+== Secondary Index
 
 * Online reindexing: log the number of done/failed changes in the error_log.
 Administrators can use the logged information to decide whether to activate the
 new index version or not.
 
-Gitweb
-------
+== Gitweb
 
 * Do not return `Forbidden` when clicking on Gitweb breadcrumb. Now when the
 user clicks on the parent folder, redirect to Gerrit projects list screen with
diff --git a/ReleaseNotes/ReleaseNotes-2.10.3.1.txt b/ReleaseNotes/ReleaseNotes-2.10.3.1.txt
index 39312eb..7777bd8 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.3.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.3.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.3.1
-=================================
+= Release notes for Gerrit 2.10.3.1
 
 There are no schema changes from link:ReleaseNotes-2.10.3.html[2.10.3].
 
diff --git a/ReleaseNotes/ReleaseNotes-2.10.3.txt b/ReleaseNotes/ReleaseNotes-2.10.3.txt
index f7a69c3..1dd96e7 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.3.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.10.3
-===============================
+= Release notes for Gerrit 2.10.3
 
 Download:
 link:https://www.gerritcodereview.com/download/gerrit-2.10.3.war[
 https://www.gerritcodereview.com/download/gerrit-2.10.3.war]
 
-Important Notes
----------------
+== Important Notes
 
 *WARNING:* There are no schema changes from
 link:ReleaseNotes-2.10.2.html[2.10.2], but Bouncycastle was upgraded to 1.51.
@@ -26,8 +24,7 @@
   java -jar gerrit.war init -d site_path
 ----
 
-New Features
-------------
+== New Features
 
 * Support hybrid OpenID and OAuth2 authentication
 +
@@ -36,15 +33,13 @@
 Particularly, linking of user identities across protocol boundaries and even from
 one OAuth2 identity to another OAuth2 identity wasn't implemented yet.
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 * Allow to configure
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10.3/config-gerrit.html#sshd.rekeyBytesLimit[
 SSHD rekey parameters].
 
-SSH
----
+== SSH
 
 * Update SSHD to 0.14.0.
 +
@@ -56,8 +51,7 @@
 * link:https://code.google.com/p/gerrit/issues/detail?id=2797[Issue 2797]:
 Add support for ECDSA based public key authentication.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Prevent wrong content type for CSS files.
 +
@@ -69,8 +63,7 @@
 * link:https://code.google.com/p/gerrit/issues/detail?id=3289[Issue 3289]:
 Prevent NullPointerException in Gitweb servlet.
 
-Replication plugin
-~~~~~~~~~~~~~~~~~~
+=== Replication plugin
 
 * Set connection timeout to 120 seconds for SSH remote operations.
 +
@@ -78,8 +71,7 @@
 operation. By setting a timeout, we ensure the operation does not get stuck
 forever, essentially blocking all future remote git creation operations.
 
-OAuth extension point
-~~~~~~~~~~~~~~~~~~~~~
+=== OAuth extension point
 
 * Respect servlet context path in URL for login token
 +
@@ -90,34 +82,29 @@
 +
 After web session cache expiration there is no way to re-sign-in into Gerrit.
 
-Daemon
-~~~~~~
+=== Daemon
 
 * Print proper names for tasks in output of `show-queue` command.
 +
 Some tasks were not displayed with the proper name.
 
-Web UI
-~~~~~~
+=== Web UI
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3044[Issue 3044]:
 Remove stripping `#` in login redirect.
 
-SSH
-~~~
+=== SSH
 
 * Prevent double authentication for the same public key.
 
 
-Performance
------------
+== Performance
 
 * Improved performance when creating a new branch on a repository with a large
 number of changes.
 
 
-Upgrades
---------
+== Upgrades
 
 * Update Bouncycastle to 1.51.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.10.4.txt b/ReleaseNotes/ReleaseNotes-2.10.4.txt
index c16e7e9..c69a946 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.4.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.4
-===============================
+= Release notes for Gerrit 2.10.4
 
 There are no schema changes from link:ReleaseNotes-2.10.3.1.html[2.10.3.1].
 
@@ -7,8 +6,7 @@
 link:https://www.gerritcodereview.com/download/gerrit-2.10.4.war[
 https://www.gerritcodereview.com/download/gerrit-2.10.4.war]
 
-New Features
-------------
+== New Features
 
 * Support identity linking in hybrid OpenID and OAuth2 authentication.
 +
@@ -20,8 +18,7 @@
 Linking of user identities from one OAuth2 identity to another OAuth2
 identity is supported.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3300[Issue 3300]:
 Fix >10x performance degradation for Git push and replication operations.
@@ -35,15 +32,13 @@
 The padding was not flushed, which caused the downloaded patch to not be
 valid base64.
 
-OAuth extension point
-~~~~~~~~~~~~~~~~~~~~~
+=== OAuth extension point
 
 * Check for session validity during logout.
 +
 When user was trying to log out, after Gerrit restart, the session was
 invalidated and IllegalStateException was recorded in the error_log.
 
-Updates
--------
+== Updates
 
 * Update jgit to 4.0.0.201505050340-m2.
diff --git a/ReleaseNotes/ReleaseNotes-2.10.5.txt b/ReleaseNotes/ReleaseNotes-2.10.5.txt
index eb48c31..a221b58 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.5.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.5
-===============================
+= Release notes for Gerrit 2.10.5
 
 There are no schema changes from link:ReleaseNotes-2.10.4.html[2.10.4].
 
@@ -7,8 +6,7 @@
 link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.5.war[
 https://gerrit-releases.storage.googleapis.com/gerrit-2.10.5.war]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Update JGit to include a memory leak fix as discussed
 link:https://groups.google.com/forum/#!topic/repo-discuss/RRQT_xCqz4o[here]
@@ -21,7 +19,6 @@
 * Fixed a regression caused by the defaultValue feature which broke the ability
 to remove labels in subprojects
 
-Updates
--------
+== Updates
 
 * Update JGit to v4.0.0.201506090130-r
diff --git a/ReleaseNotes/ReleaseNotes-2.10.6.txt b/ReleaseNotes/ReleaseNotes-2.10.6.txt
index 94a95bd..7c12d11 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.6.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.6
-===============================
+= Release notes for Gerrit 2.10.6
 
 There are no schema changes from link:ReleaseNotes-2.10.5.html[2.10.5].
 
@@ -7,8 +6,7 @@
 link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.6.war[
 https://gerrit-releases.storage.googleapis.com/gerrit-2.10.6.war]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Fix generation of licenses in documentation.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.10.7.txt b/ReleaseNotes/ReleaseNotes-2.10.7.txt
index 28cf37b..f369999 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.7.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10.7
-===============================
+= Release notes for Gerrit 2.10.7
 
 There are no schema changes from link:ReleaseNotes-2.10.6.html[2.10.6].
 
@@ -7,8 +6,7 @@
 link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.7.war[
 https://gerrit-releases.storage.googleapis.com/gerrit-2.10.7.war]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3361[Issue 3361]:
 Synchronize Myers diff and Histogram diff invocations to prevent pack file
diff --git a/ReleaseNotes/ReleaseNotes-2.10.txt b/ReleaseNotes/ReleaseNotes-2.10.txt
index f6bd951..4f068cc 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.10
-=============================
+= Release notes for Gerrit 2.10
 
 
 Gerrit 2.10 is now available:
@@ -14,8 +13,7 @@
 link:ReleaseNotes-2.9.4.html[Gerrit 2.9.4].
 These bug fixes are *not* listed in these release notes.
 
-Important Notes
----------------
+== Important Notes
 
 
 *WARNING:* This release contains schema changes.  To upgrade:
@@ -44,8 +42,7 @@
 link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
 
 
-Release Highlights
-------------------
+== Release Highlights
 
 
 * Support for externally loaded plugins.
@@ -62,16 +59,13 @@
 can configure the default contents of the menu.
 
 
-New Features
-------------
+== New Features
 
 
-Web UI
-~~~~~~
+=== Web UI
 
 
-Global
-^^^^^^
+==== Global
 
 * Add 'All-Users' project to store meta data for all users.
 
@@ -82,8 +76,7 @@
 * Allow UiActions to perform redirects without JavaScript.
 
 
-Change Screen
-^^^^^^^^^^^^^
+==== Change Screen
 
 
 * Display avatar for author, committer, and change owner.
@@ -104,8 +97,7 @@
 Allow to customize Submit button label and tooltip.
 
 
-Side-by-Side Diff Screen
-^^^^^^^^^^^^^^^^^^^^^^^^
+==== Side-by-Side Diff Screen
 
 * Allow the user to select the syntax highlighter.
 
@@ -116,8 +108,7 @@
 * Add syntax highlighting of the commit message.
 
 
-Change List / Dashboards
-^^^^^^^^^^^^^^^^^^^^^^^^
+==== Change List / Dashboards
 
 * Remove age operator when drilling down from a dashboard to a query.
 
@@ -132,8 +123,7 @@
 when 'R' is pressed.  The same binding is added for custom dashboards.
 
 
-Project Screens
-^^^^^^^^^^^^^^^
+==== Project Screens
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2751[Issue 2751]:
 Add support for filtering by regex in project list screen.
@@ -142,8 +132,7 @@
 
 * Add branch actions to 'Projects > Branches' view.
 
-User Preferences
-^^^^^^^^^^^^^^^^
+==== User Preferences
 
 
 * Users can customize the contents of the 'My' menu from the preferences
@@ -156,8 +145,7 @@
 names and 'Show Username' to show usernames in the change list.
 
 
-Secondary Index / Search
-~~~~~~~~~~~~~~~~~~~~~~~~
+=== Secondary Index / Search
 
 
 * Allow to search projects by prefix.
@@ -177,8 +165,7 @@
 rather than just the project name.
 
 
-ssh
-~~~
+=== ssh
 
 
 * Expose SSHD backend in
@@ -187,12 +174,10 @@
 
 * Add support for JCE (Java Cryptography Extension) ciphers.
 
-REST API
-~~~~~~~~
+=== REST API
 
 
-General
-^^^^^^^
+==== General
 
 
 * Remove `kind` attribute from REST containers.
@@ -201,8 +186,7 @@
 
 * Accept `HEAD` in RestApiServlet.
 
-Accounts
-^^^^^^^^
+==== Accounts
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-accounts.html#get-user-preferences[
@@ -211,8 +195,7 @@
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-accounts.html#set-user-preferences[
 Set user preferences].
 
-Changes
-^^^^^^^
+==== Changes
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2338[Issue 2338]:
@@ -226,8 +209,7 @@
 If the `other-branches` option is specified, the mergeability will also be
 checked for all other branches.
 
-Config
-^^^^^^
+==== Config
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#list-tasks[
@@ -254,8 +236,7 @@
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#get-summary[
 Get server summary].
 
-Projects
-^^^^^^^^
+==== Projects
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-projects.html#ban-commit[
@@ -276,8 +257,7 @@
 list projects endpoint] to support query offset.
 
 
-Daemon
-~~~~~~
+=== Daemon
 
 
 * Add change subject to output of change URL on push.
@@ -292,8 +272,7 @@
 Add change kind to PatchSetCreatedEvent.
 
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 * Use
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-gerrit.html#core.useRecursiveMerge[
@@ -344,8 +323,7 @@
 configure Tomcat] to allow embedded slashes.
 
 
-Misc
-~~~~
+=== Misc
 
 * Don't allow empty user name and passwords in InternalAuthBackend.
 
@@ -353,8 +331,7 @@
 Add change-owner parameter to gerrit hooks.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
 * Support for externally loaded plugins.
 +
@@ -428,11 +405,9 @@
 ** Star/unstar changes
 ** Check if revision needs rebase
 
-Bug Fixes
----------
+== Bug Fixes
 
-General
-~~~~~~~
+=== General
 
 * Use fixed rate instead of fixed delay for log file compression.
 +
@@ -453,8 +428,7 @@
 made, but one was not being closed. This eventually caused resource exhaustion
 and LDAP authentications failed.
 
-Access Permissions
-~~~~~~~~~~~~~~~~~~
+=== Access Permissions
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2995[Issue 2995]:
 Fix faulty behaviour in `BLOCK` permission.
@@ -464,11 +438,9 @@
 case the `BLOCK` would always win for the child, even though the `BLOCK` was
 overruled in the parent.
 
-Web UI
-~~~~~~
+=== Web UI
 
-General
-^^^^^^^
+==== General
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2595[Issue 2595]:
 Make gitweb redirect to login.
@@ -486,8 +458,7 @@
 if a site administrator ran `java -war gerrit.war init -d /some/existing/site --batch`.
 
 
-Change Screen
-^^^^^^^^^^^^^
+==== Change Screen
 
 * Don't linkify trailing dot or comma in messages.
 +
@@ -525,8 +496,7 @@
 * Fix exception when clicking on a binary file without being signed in.
 
 
-Side-By-Side Diff
-^^^^^^^^^^^^^^^^^
+==== Side-By-Side Diff
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2970[Issue 2970]:
 Fix misalignment of side A and side B for long insertion/deletion blocks.
@@ -556,30 +526,25 @@
 * Include content on identical files with mode change.
 
 
-User Settings
-^^^^^^^^^^^^^
+==== User Settings
 
 * Avoid loading all SSH keys when adding a new one.
 
 
-Secondary Index / Search
-~~~~~~~~~~~~~~~~~~~~~~~~
+=== Secondary Index / Search
 
 
 * Omit corrupt changes from search results.
 
 * Allow illegal label names from default search predicate.
 
-REST
-~~~~
+=== REST
 
-General
-^^^^^^^
+==== General
 
 * Fix REST API responses for 3xx and 4xx classes.
 
-Changes
-^^^^^^^
+==== Changes
 
 * Fix inconsistent behaviour in the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#add-reviewer[
@@ -589,8 +554,7 @@
 to add a user who had no visibility to the change or whose account was invalid.
 
 
-Changes
-^^^^^^^
+==== Changes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2583[Issue 2583]:
 Reject inline comments on files that do not exist in the patch set.
@@ -620,8 +584,7 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#list-comments[
 List Comments] endpoint.
 
-SSH
-~~~
+=== SSH
 
 
 * Prevent double authentication for the same public key.
@@ -641,8 +604,7 @@
 from being logged when `git receive-pack` was executed instead of `git-receive-pack`.
 
 
-Daemon
-~~~~~~
+=== Daemon
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2284[Issue 2284]:
@@ -661,11 +623,9 @@
 directly pushed changes.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
-General
-^^^^^^^
+==== General
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2895[Issue 2895]:
@@ -680,8 +640,7 @@
 
 * Fix ChangeListener auto-registered implementations.
 
-Replication
-^^^^^^^^^^^
+==== Replication
 
 
 * Move replication logs into a separate file.
@@ -691,8 +650,7 @@
 * Show replication ID in the log and in show-queue command.
 
 
-Upgrades
---------
+== Upgrades
 
 
 * Update Guava to 17.0
diff --git a/ReleaseNotes/ReleaseNotes-2.11.1.txt b/ReleaseNotes/ReleaseNotes-2.11.1.txt
index be19fc5..3583421 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.1
-===============================
+= Release notes for Gerrit 2.11.1
 
 Gerrit 2.11.1 is now available:
 
@@ -14,8 +13,7 @@
 There are no schema changes from link:ReleaseNotes-2.11.html[2.11].
 
 
-New Features
-------------
+== New Features
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=321[Issue 321]:
 Use in-memory Lucene index for a better reviewer suggestion.
@@ -27,11 +25,9 @@
 suggest.fullTextSearchRefresh] parameter.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
-Performance
-~~~~~~~~~~~
+=== Performance
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3363[Issue 3363]:
 Fix performance degrade in background mergeability checks.
@@ -57,8 +53,7 @@
 +
 The change edit information was being loaded twice.
 
-Index
-~~~~~
+=== Index
 
 * Fix `PatchLineCommentsUtil.draftByChangeAuthor`.
 +
@@ -67,8 +62,7 @@
 
 * Don't show stack trace when failing to build BloomFilter during reindex.
 
-Permissions
-~~~~~~~~~~~
+=== Permissions
 
 * Require 'View Plugins' capability to list plugins through SSH.
 +
@@ -83,8 +77,7 @@
 edit the `project.config` file.
 
 
-Change Screen / Diff / Inline Edit
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Change Screen / Diff / Inline Edit
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3191[Issue 3191]:
 Always show 'Not Current' as state when looking at old patch set.
@@ -103,8 +96,7 @@
 In the side-by-side diff, the cursor is placed on the first column of the diff,
 rather than at the end.
 
-Web Container
-~~~~~~~~~~~~~
+=== Web Container
 
 * Fix `gc_log` when running in a web container.
 +
@@ -117,8 +109,7 @@
 web container with the site path configured using the `gerrit.site_path`
 property.
 
-Plugins
-~~~~~~~
+=== Plugins
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3310[Issue 3310]:
 Fix disabling plugins when Gerrit is running on Windows.
@@ -137,8 +128,7 @@
 When `replicateOnStartup` is enabled, the plugin was not emitting the status
 events after the initial sync.
 
-Miscellaneous
-~~~~~~~~~~~~~
+=== Miscellaneous
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3328[Issue 3328]:
 Allow to push a tag that points to a non-commit object.
@@ -168,8 +158,7 @@
 
 * Assume change kind is 'rework' if `LargeObjectException` occurs.
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3325[Issue 3325]:
 Add missing `--newrev` parameter to the
@@ -185,8 +174,7 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11.1/config-gerrit.html#auth.registerUrl[
 auth types].
 
-Updates
--------
+== Updates
 
 * Update CodeMirror to 5.0.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.11.10.txt b/ReleaseNotes/ReleaseNotes-2.11.10.txt
index 9ad34b6..a352aac 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.10.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.10.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.10
-================================
+= Release notes for Gerrit 2.11.10
 
 Gerrit 2.11.10 is now available:
 
@@ -8,8 +7,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.11.9.html[2.11.9].
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Fix synchronization of Myers diff and Histogram diff invocations.
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.11.2.txt b/ReleaseNotes/ReleaseNotes-2.11.2.txt
index 07f99ae..98e66b0 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.2.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.2
-===============================
+= Release notes for Gerrit 2.11.2
 
 Gerrit 2.11.2 is now available:
 
@@ -12,8 +11,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.11.1.html[2.11.1].
 
-New Features
-------------
+== New Features
 
 New SSH commands:
 
@@ -29,8 +27,7 @@
 problems.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2761[Issue 2761]:
 Fix incorrect file list when comparing patchsets.
@@ -96,8 +93,7 @@
 
 * Print proper name for reindex after update tasks in `show-queue` command.
 
-Updates
--------
+== Updates
 
 * Update JGit to 4.0.1.201506240215-r.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.11.3.txt b/ReleaseNotes/ReleaseNotes-2.11.3.txt
index 0df3a29..f705d1e 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.3.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.3
-===============================
+= Release notes for Gerrit 2.11.3
 
 Gerrit 2.11.3 is now available:
 
@@ -9,8 +8,7 @@
 There are no schema changes from link:ReleaseNotes-2.11.2.html[2.11.2].
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Do not suggest inactive accounts.
 +
@@ -86,8 +84,7 @@
 caused by plugins not loading that an admin should pay attention to and try to
 resolve.
 
-Updates
--------
+== Updates
 
 * Update Guice to 4.0.
 * Replace parboiled 1.1.7 with grappa 1.0.4.
diff --git a/ReleaseNotes/ReleaseNotes-2.11.4.txt b/ReleaseNotes/ReleaseNotes-2.11.4.txt
index 6037edd..cfa8576 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.4.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.4
-===============================
+= Release notes for Gerrit 2.11.4
 
 Gerrit 2.11.4 is now available:
 
@@ -13,8 +12,7 @@
 There are no schema changes from link:ReleaseNotes-2.11.3.html[2.11.3].
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Fix NullPointerException in `ls-project` command with `--has-acl-for` option.
 +
@@ -131,8 +129,7 @@
 was a merge commit, or if the change being viewed conflicted with an open merge
 commit.
 
-Plugin Bugfixes
----------------
+== Plugin Bugfixes
 
 * singleusergroup: Allow to add a user to a project's ACL using `user/username`.
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.11.5.txt b/ReleaseNotes/ReleaseNotes-2.11.5.txt
index d7758cb..6957827 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.5.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.5
-===============================
+= Release notes for Gerrit 2.11.5
 
 Gerrit 2.11.5 is now available:
 
@@ -9,8 +8,7 @@
 There are no schema changes from link:ReleaseNotes-2.11.4.html[2.11.4].
 
 
-Important Notes
----------------
+== Important Notes
 
 *WARNING:* This release uses a forked version of buck.
 
@@ -25,8 +23,7 @@
 ----
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3442[Issue 3442]:
 Handle commit validation errors when creating/editing changes via REST.
diff --git a/ReleaseNotes/ReleaseNotes-2.11.6.txt b/ReleaseNotes/ReleaseNotes-2.11.6.txt
index d6f939f..977ea14 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.6.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.6
-===============================
+= Release notes for Gerrit 2.11.6
 
 Gerrit 2.11.6 is now available:
 
@@ -8,11 +7,9 @@
 
 There are no schema changes from link:ReleaseNotes-2.11.5.html[2.11.5].
 
-Bug Fixes
----------
+== Bug Fixes
 
-General
-~~~~~~~
+=== General
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3742[Issue 3742]:
 Use merge strategy for mergeability testing on 'Rebase if Necessary' strategy.
@@ -51,8 +48,7 @@
 couldn't be added as a reviewer by selecting it from the suggested list of
 accounts.
 
-Authentication
-~~~~~~~~~~~~~~
+=== Authentication
 
 * Fix handling of lowercase HTTP username.
 +
@@ -69,8 +65,7 @@
 of the user for the claimed identity would fail, causing a new account to be
 created.
 
-UI
-~~
+=== UI
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3714[Issue 3714]:
 Improve visibility of comments on dark themes.
@@ -78,8 +73,7 @@
 * Fix highlighting of search results and trailing whitespaces in intraline
 diff chunks.
 
-Plugins
-~~~~~~~
+=== Plugins
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3768[Issue 3768]:
 Fix usage of `EqualsFilePredicate` in plugins.
@@ -115,8 +109,7 @@
 
 ** Allow to use GWTORM `Key` classes.
 
-Documentation Updates
----------------------
+== Documentation Updates
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=412[Issue 412]:
 Update documentation of `commentlink.match` regular expression to clarify
diff --git a/ReleaseNotes/ReleaseNotes-2.11.7.txt b/ReleaseNotes/ReleaseNotes-2.11.7.txt
index 7a0de2d..6742279 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.7.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.7
-===============================
+= Release notes for Gerrit 2.11.7
 
 Gerrit 2.11.7 is now available:
 
@@ -8,8 +7,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.11.6.html[2.11.6].
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3882[Issue 3882]:
 Fix 'No user on email thread' exception when label with group parameter is
diff --git a/ReleaseNotes/ReleaseNotes-2.11.8.txt b/ReleaseNotes/ReleaseNotes-2.11.8.txt
index 0f9dc21..0aa8dfc 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.8.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.8
-===============================
+= Release notes for Gerrit 2.11.8
 
 Gerrit 2.11.8 is now available:
 
@@ -8,8 +7,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.11.7.html[2.11.7].
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Upgrade Apache commons-collections to version 3.2.2.
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.11.9.txt b/ReleaseNotes/ReleaseNotes-2.11.9.txt
index c5b431f..52ee3fe 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.9.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.9.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11.9
-===============================
+= Release notes for Gerrit 2.11.9
 
 Gerrit 2.11.9 is now available:
 
@@ -8,8 +7,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.11.8.html[2.11.8].
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=4070[Issue 4070]:
 Don't return current patch set in queries if the current patch set is not
diff --git a/ReleaseNotes/ReleaseNotes-2.11.txt b/ReleaseNotes/ReleaseNotes-2.11.txt
index 44c5398..1ca6825 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.11
-=============================
+= Release notes for Gerrit 2.11
 
 
 Gerrit 2.11 is now available:
@@ -14,8 +13,7 @@
 These bug fixes are *not* listed in these release notes.
 
 
-Important Notes
----------------
+== Important Notes
 
 
 *WARNING:* This release contains schema changes.  To upgrade:
@@ -82,8 +80,7 @@
 
 *WARNING:* The deprecated '/query' URL is removed and will now return `Not Found`.
 
-Release Highlights
-------------------
+== Release Highlights
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=505[Issue 505]:
@@ -95,16 +92,13 @@
 * The old change screen is removed.
 
 
-New Features
-------------
+== New Features
 
 
-Web UI
-~~~~~~
+=== Web UI
 
 [[inline-editing]]
-Inline Editing
-^^^^^^^^^^^^^^
+==== Inline Editing
 
 Refer to the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/user-inline-edit.html[
@@ -133,8 +127,7 @@
 
 * Files can be added, deleted, restored and modified directly in browser.
 
-Change Screen
-^^^^^^^^^^^^^
+==== Change Screen
 
 * Remove the 'Edit Message' button from the change screen.
 +
@@ -175,8 +168,7 @@
 * Show changes across all projects and branches in the `Same Topic` tab.
 
 
-Side-By-Side Diff
-^^^^^^^^^^^^^^^^^
+==== Side-By-Side Diff
 
 * New button to switch between side-by-side diff and unified diff.
 
@@ -206,8 +198,7 @@
 ** Soy
 
 
-Projects Screen
-^^^^^^^^^^^^^^^
+==== Projects Screen
 
 * Add pagination and filtering on the branch list page.
 
@@ -220,17 +211,14 @@
 browser, which is useful since it is possible that not all configuration options
 are available in the UI.
 
-REST
-~~~~
+=== REST
 
-Accounts
-^^^^^^^^
+==== Accounts
 
 * Add new link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-accounts.html#suggest-account[
 Suggest Account endpoint].
 
-Changes
-^^^^^^^
+==== Changes
 
 * The link:https://gerrit-review.googlesource.com/Documentation/2.10/rest-api-changes.html#message[
 Edit Commit Message] endpoint is removed in favor of the new
@@ -260,8 +248,7 @@
 Get Change Detail] endpoint.
 
 
-Change Edits
-^^^^^^^^^^^^
+==== Change Edits
 
 Several new endpoints are added to support the inline edit feature.
 
@@ -296,8 +283,7 @@
 Delete Change Edit].
 
 
-Projects
-^^^^^^^^
+==== Projects
 
 * Add new
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-projects.html#delete-branches[
@@ -316,8 +302,7 @@
 Get Tag] endpoint.
 
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 * Add support for
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#auth.httpExternalIdHeader[
@@ -395,8 +380,7 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#container.daemonOpt[
 options to pass to the daemon].
 
-Daemon
-~~~~~~
+=== Daemon
 
 * Allow to enable the http daemon when running in slave mode.
 +
@@ -416,8 +400,7 @@
 * Don't send 'new patch set' notification emails for trivial rebases.
 
 
-SSH
-~~~
+=== SSH
 
 * Add new commands
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/cmd-logging-ls-level.html[
@@ -448,8 +431,7 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/cmd-set-account.html[
 `set-account` SSH command].
 
-Email
-~~~~~
+=== Email
 
 * Add `$change.originalSubject` field for email templates.
 +
@@ -464,11 +446,9 @@
 field during first use.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
-General
-^^^^^^^
+==== General
 
 * Plugins can listen to account group membership changes.
 +
@@ -518,17 +498,14 @@
 ** Get comments and drafts.
 ** Get change edit.
 
-Replication
-^^^^^^^^^^^
+==== Replication
 
 * Projects can be specified with wildcard in the `start` command.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
-Daemon
-~~~~~~
+=== Daemon
 
 * Change 'Merge topic' to 'Merge changes from topic'.
 +
@@ -564,8 +541,7 @@
 of a repository with a space in its name was impossible.
 
 
-Secondary Index / Search
-~~~~~~~~~~~~~~~~~~~~~~~~
+=== Secondary Index / Search
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2822[Issue 2822]:
 Improve Lucene analysis of words linked with underscore or dot.
@@ -576,8 +552,7 @@
 * Fix support for `change~branch~id` in query syntax.
 
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 [[remove-generate-http-password-capability]]
 * Remove the 'Generate HTTP Password' capability.
@@ -608,17 +583,14 @@
 read from `hooks.changeMerged`. Fix to use `hooks.changeMergedHook` as
 documented.
 
-Web UI
-~~~~~~
+=== Web UI
 
-Change List
-^^^^^^^^^^^
+==== Change List
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3304[Issue 3304]:
 Always show a tooltip on the label column entries.
 
-Change Screen
-^^^^^^^^^^^^^
+==== Change Screen
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3147[Issue 3147]:
 Allow to disable muting of common path prefixes in the file list.
@@ -742,8 +714,7 @@
 Align parent weblinks with parent commits in the commit box.
 
 
-Side-By-Side Diff
-^^^^^^^^^^^^^^^^^
+==== Side-By-Side Diff
 
 * Return to normal mode after editing a draft comment.
 +
@@ -756,18 +727,15 @@
 highlighter.
 
 
-Project Screen
-^^^^^^^^^^^^^^
+==== Project Screen
 
 * Fix alignment of checkboxes on project access screen.
 +
 The 'Exclusive' checkbox was not aligned with the other checkboxes.
 
-REST API
-~~~~~~~~
+=== REST API
 
-Changes
-^^^^^^^
+==== Changes
 
 * Remove the administrator restriction on the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#index-change[
@@ -800,8 +768,7 @@
 `409 Conflict`.
 
 
-Projects
-^^^^^^^^
+==== Projects
 
 * Make it mandatory to specify at least one of the `--prefix`, `--match` or `--regex`
 options in the
@@ -826,11 +793,9 @@
 others.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
-Replication
-^^^^^^^^^^^
+==== Replication
 
 * Create missing repositories on the remote when replicating with the git
 protocol.
@@ -843,8 +808,7 @@
 create a project on the remote.
 
 
-Upgrades
---------
+== Upgrades
 
 * Update Antlr to 3.5.2.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.12.1.txt b/ReleaseNotes/ReleaseNotes-2.12.1.txt
index f49de7d..e746d6e 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.12.1
-===============================
+= Release notes for Gerrit 2.12.1
 
 Gerrit 2.12.1 is now available:
 
@@ -11,8 +10,7 @@
 link:ReleaseNotes-2.11.7.html[Gerrit 2.11.7]. These bug fixes are *not*
 listed in these release notes.
 
-Schema Upgrade
---------------
+== Schema Upgrade
 
 *WARNING:* This version includes a manual schema upgrade when upgrading
 from 2.12.
@@ -48,11 +46,9 @@
 necessary and should be omitted.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
-General
-~~~~~~~
+=== General
 
 * Fix column type for signed push certificates.
 +
@@ -160,8 +156,7 @@
 * link:https://code.google.com/p/gerrit/issues/detail?id=3883[Issue 3883]:
 Respect the `core.commentchar` setting from `.gitconfig` in `commit-msg` hook.
 
-UI
-~~
+=== UI
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3894[Issue 3894]:
 Fix display of 'Related changes' after change is rebased in web UI:
@@ -194,8 +189,7 @@
 * Improve tooltip on 'Submit' button when 'Submit whole topic' is enabled
 and the topic can't be submitted due to some changes not being ready.
 
-Plugins
-~~~~~~~
+=== Plugins
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3821[Issue 3821]:
 Fix repeated reloading of plugins when running on OpenJDK 8.
@@ -223,14 +217,12 @@
 Allow plugins to suggest reviewers based on either change or project
 resources.
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
 * Update documentation of `commentlink` to reflect changed search URL.
 
 * Add missing documentation of valid `database.type` values.
 
-Upgrades
---------
+== Upgrades
 
 * Upgrade JGit to 4.1.2.201602141800-r.
diff --git a/ReleaseNotes/ReleaseNotes-2.12.2.txt b/ReleaseNotes/ReleaseNotes-2.12.2.txt
index 500b015..8292eb5 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.2.txt
@@ -1,13 +1,11 @@
-Release notes for Gerrit 2.12.2
-===============================
+= Release notes for Gerrit 2.12.2
 
 Gerrit 2.12.2 is now available:
 
 link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.2.war[
 https://gerrit-releases.storage.googleapis.com/gerrit-2.12.2.war]
 
-Schema Upgrade
---------------
+== Schema Upgrade
 
 *WARNING:* There are no schema changes from link:ReleaseNotes-2.12.1.html[
 2.12.1] but a manual schema upgrade is necessary when upgrading from 2.12.
@@ -43,8 +41,7 @@
 done the migration, this manual step is not necessary and should be omitted.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Upgrade Apache commons-collections to version 3.2.2.
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.12.3.txt b/ReleaseNotes/ReleaseNotes-2.12.3.txt
index dba10e9..f51d739 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.3.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.12.3
-===============================
+= Release notes for Gerrit 2.12.3
 
 Gerrit 2.12.3 is now available:
 
@@ -11,8 +10,7 @@
 link:ReleaseNotes-2.11.9.html[Gerrit 2.11.9]. These bug fixes are *not*
 listed in these release notes.
 
-Schema Upgrade
---------------
+== 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.
@@ -49,8 +47,7 @@
 should be omitted.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Fix SSL security issue in the SMTP email relay.
 +
@@ -111,7 +108,6 @@
 
 * Show an error message when trying to add a non-existent group to an ACL.
 
-Updates
--------
+== Updates
 
 * Update commons-validator to 1.5.1.
diff --git a/ReleaseNotes/ReleaseNotes-2.12.txt b/ReleaseNotes/ReleaseNotes-2.12.txt
index e8e8aec..84644e8 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.12
-=============================
+= Release notes for Gerrit 2.12
 
 
 Gerrit 2.12 is now available:
@@ -7,8 +6,7 @@
 link:https://www.gerritcodereview.com/download/gerrit-2.12.war[
 https://www.gerritcodereview.com/download/gerrit-2.12.war]
 
-Important Notes
----------------
+== Important Notes
 
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
@@ -39,8 +37,7 @@
 `refs/*/master` instead of `Plain` and `master`.
 
 
-Release Highlights
-------------------
+== Release Highlights
 
 This release includes the following new features. See the sections below for
 further details.
@@ -50,11 +47,9 @@
 * Support for GPG Keys and signed pushes.
 
 
-New Features
-------------
+== New Features
 
-New Change Submission Workflows
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New Change Submission Workflows
 
 * New 'Submit Whole Topic' setting.
 +
@@ -76,8 +71,7 @@
 enter the 'Submitted, Merge Pending' state.
 
 
-GPG Keys and Signed Pushes
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== GPG Keys and Signed Pushes
 
 * Signed push can be enabled by setting
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.enableSignedPush[
@@ -102,8 +96,7 @@
 `receive.certNonceSlop`].
 
 
-Secondary Index
-~~~~~~~~~~~~~~~
+=== Secondary Index
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3333[Issue 3333]:
 Support searching for changes by author and committer.
@@ -141,11 +134,9 @@
 sense to enforce all changes to be written to disk ASAP.
 
 
-UI
-~~
+=== UI
 
-General
-^^^^^^^
+==== General
 
 * Edit and diff preferences can be modified from the user preferences screen.
 +
@@ -165,14 +156,12 @@
 users.
 
 
-Project Screen
-^^^^^^^^^^^^^^
+==== Project Screen
 
 * New tab to list the project's tags, similar to the branch list.
 
 
-Inline Editor
-^^^^^^^^^^^^^
+==== Inline Editor
 
 * Store and load edit preferences in git.
 +
@@ -187,8 +176,7 @@
 * Add support for Emacs and Vim key maps.
 
 
-Change Screen
-^^^^^^^^^^^^^
+==== Change Screen
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3318[Issue 3318]:
 Highlight 'Reply' button if there are draft comments on any patch set.
@@ -216,8 +204,7 @@
 This helps to identify changes when the subject is truncated in the list.
 
 
-Side-By-Side Diff
-^^^^^^^^^^^^^^^^^
+==== Side-By-Side Diff
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3293[Issue 3293]:
 Add syntax highlighting for Puppet.
@@ -226,40 +213,34 @@
 Add syntax highlighting for VHDL.
 
 
-Group Screen
-^^^^^^^^^^^^
+==== Group Screen
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=1479[Issue 1479]:
 The group screen now includes an 'Audit Log' panel showing member additions,
 removals, and the user who made the change.
 
 
-API
-~~~
+=== API
 
 Several new APIs are added.
 
-Accounts
-^^^^^^^^
+==== Accounts
 
 * Suggest accounts.
 
-Tags
-^^^^
+==== Tags
 
 * List tags.
 
 * Get tag.
 
 
-REST API
-~~~~~~~~
+=== REST API
 
 New REST API endpoints and new options on existing endpoints.
 
 
-Accounts
-^^^^^^^^
+==== Accounts
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#set-username[
 Set Username]: Set the username of an account.
@@ -276,8 +257,7 @@
 account.
 
 
-Changes
-^^^^^^^
+==== Changes
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#set-review[
 Set Review]: Add an option to omit duplicate comments.
@@ -294,8 +274,7 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#set-review[
 Set Review]: Add an option to publish draft comments on all revisions.
 
-Config
-^^^^^^
+==== Config
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-config.html#get-info[
 Get Server Info]: Return information about the Gerrit server configuration.
@@ -304,8 +283,7 @@
 Confirm Email]: Confirm that the user owns an email address.
 
 
-Groups
-^^^^^^
+==== Groups
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-groups.html#list-group[
 List Groups]: Add option to suggest groups.
@@ -317,8 +295,7 @@
 additions, removals, and the user who made the change.
 
 
-Projects
-^^^^^^^^
+==== Projects
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-projects.html#run-gc[
 Run GC]: Add `aggressive` option to specify whether or not to run an aggressive
@@ -329,8 +306,7 @@
 `--start` and `--end`.
 
 
-SSH
-~~~
+=== SSH
 
 * Add support for ZLib Compression.
 +
@@ -340,11 +316,9 @@
 
 * Add support for hmac-sha2-256 and hmac-sha2-512 as MACs.
 
-Plugins
-~~~~~~~
+=== Plugins
 
-General
-^^^^^^^
+==== General
 
 * Gerrit client can now pass JavaScriptObjects to extension panels.
 
@@ -387,8 +361,7 @@
 ** Allow to use GWTORM `Key` classes.
 
 
-Other
-~~~~~
+=== Other
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3401[Issue 3401]:
 Add option to
@@ -433,8 +406,7 @@
 and queues, and invoke the index REST API on changes.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=3499[Issue 3499]:
 Fix syntax highlighting of raw string literals in go.
@@ -536,8 +508,7 @@
 +
 Under some circumstances it was possible to fail with an IO error.
 
-Documentation Updates
----------------------
+== Documentation Updates
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=412[Issue 412]:
 Update documentation of `commentlink.match` regular expression to clarify
@@ -549,8 +520,7 @@
 
 * Document that `ldap.groupBase` and `ldap.accountBase` are repeatable.
 
-Upgrades
---------
+== Upgrades
 
 * Upgrade Asciidoctor to 1.5.2
 
diff --git a/ReleaseNotes/ReleaseNotes-2.13.1.txt b/ReleaseNotes/ReleaseNotes-2.13.1.txt
new file mode 100644
index 0000000..958e726
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.13.1.txt
@@ -0,0 +1,21 @@
+= Release notes for Gerrit 2.13.1
+
+Gerrit 2.13.1 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.13.1.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.13.1.war]
+
+== Schema Upgrade
+
+There are no schema changes from link:ReleaseNotes-2.13.html[2.13].
+
+== Bug Fixes
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4618[Issue 4618]:
+Fix internal server error after online reindexing completed.
+
+* Fix internal server error when cloning from slaves and not all refs are
+visible.
+
+* Fix JSON deserialization error causing stream event client to no longer receive
+events.
diff --git a/ReleaseNotes/ReleaseNotes-2.13.2.txt b/ReleaseNotes/ReleaseNotes-2.13.2.txt
new file mode 100644
index 0000000..c7be976
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.13.2.txt
@@ -0,0 +1,46 @@
+= Release notes for Gerrit 2.13.2
+
+Gerrit 2.13.2 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.13.2.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.13.2.war]
+
+== Schema Upgrade
+
+There are no schema changes from link:ReleaseNotes-2.13.1.html[2.13.1].
+
+== Bug Fixes
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4630[Issue 4630]:
+Fix server error when navigating up to change while 'Working' is displayed.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4631[Issue 4631]:
+Read project watches from database.
++
+Project watches were being read from the git backend by default, but the
+migration to git is not yet completed.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4632[Issue 4632]:
+Fix server error when deleting multiple SSH keys from the Web UI.
++
+Attempting to delete multiple keys in parallel resulted in a lock failure
+when removing the keys from the git backend.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4645[Issue 4645]:
+Fix malformed account suggestions.
++
+If the query contained several query terms and one of the query terms was
+a substring of 'strong', the suggestion was malformed.
+
+* Hooks plugin: Fix incorrect value passed to `--change-url` parameter.
++
+The URL was being generated using the change's Change-Id rather than the
+change number.
+
+* Check for CLA when creating project config changes from the web UI.
++
+If contributor agreements were enabled and required for a project, and
+the user had not signed a CLA, it was still possible to upload changes
+for review on `refs/meta/config` by making changes in the project access
+editor and pressing 'Save for Review'.
+
diff --git a/ReleaseNotes/ReleaseNotes-2.13.txt b/ReleaseNotes/ReleaseNotes-2.13.txt
new file mode 100644
index 0000000..0afca1a
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.13.txt
@@ -0,0 +1,471 @@
+= Release notes for Gerrit 2.13
+
+
+Gerrit 2.13 is now available:
+
+link:https://www.gerritcodereview.com/download/gerrit-2.13.war[
+https://www.gerritcodereview.com/download/gerrit-2.13.war]
+
+
+== Important Notes
+
+*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 the online reindexer from
+working.
+
+Gerrit 2.13 introduces a new secondary index for accounts, and this 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/2.13/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.
+
+=== Access control for git submodule subscriptions
+
+To prevent potential security breaches as described in
+link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3311[issue 3311],
+it is now only possible for a project to subscribe to a submodule if the
+submodule explicitly allows itself to be subscribed.
+
+Please see the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/user-submodules.html[
+submodules user guide] for details.
+
+Note that when upgrading from an earlier version of Gerrit, permissions for
+any existing subscriptions will be automatically added during the database
+schema migration.
+
+=== Metrics
+
+Metrics about Gerrit's internal state can be sent to external
+monitoring systems.
+
+Plugins can provide implementations of the metrics interface to
+report metrics to different monitoring systems. The following
+plugins are available:
+
+* link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-jmx[
+JMX]
+
+* link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-graphite[
+Graphite]
+
+* link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-elasticsearch[
+Elasticsearch]
+
+Plugins can also provide their own metrics.
+
+See the link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/metrics.html[
+metrics documentation] for further details.
+
+=== 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 merge commit's 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
+
+In order to avoid potentially confusing behavior, when submitting changes in a
+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
+
+* Secure settings
++
+Plugins may now store secure settings in `etc/$PLUGIN.secure.config` where they
+will be decoded by the Secure Store implementation.
+
+* Exported dependencies
++
+Gson is now an exported dependency. Plugins no longer need to explicitly add
+a dependency on it.
+
+=== Misc
+
+* New project option to reject implicit merge commits.
++
+The 'Reject Implicit Merges' option can be enabled to prevent non-merge commits
+from implicitly bringing unwanted changes into a branch. This can happen for
+example when a commit is made based on one branch but is mistakenly pushed to
+another, for example based on `refs/heads/master` but pushed to `refs/for/stable`.
+
+* New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/access-control.html#category_add_patch_set[
+Add Patch Set capability] to control who is allowed to upload a new patch
+set to an existing change.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4015[Issue 4015]:
+Allow setting a
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/user-upload.html#message[
+comment message] when uploading a change.
+
+* Allow to specify
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/user-upload.html#notify[
+who should be notified by email] when uploading a change.
+
+* 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.
+
+* New configuration options to configure JGit repository cache parameters.
++
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#core.repositoryCacheCleanupDelay[
+core.repositoryCacheCleanupDelay] and
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#core.repositoryCacheExpireAfter[
+core.repositoryCacheExpireAfter] can be configured.
+
+* Accept `-b` as an alias of `--batch` in the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/pgm-init.html[
+init program].
+
+
+== Bug Fixes
+
+* Don't add the same SSH key multiple times.
++
+If an already existing SSH key was added, a duplicate entry was added to the
+list of user's SSH keys.
+
+* Respect the 'Require a valid contributor agreement to upload' setting
+when creating changes via the UI.
++
+If a user had not signed a CLA, it was still possible for them to create a new
+change with the 'Revert' or 'Cherry Pick' button.
+
+* Make Lucene index more stable when being interrupted.
+
+* 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.
+
+* 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.
+
+* 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.
+
+* Make `gitweb.type` default to `disabled` when not explicitly set.
++
+Previously the behavior was not documented and it would default to type
+`gitweb`. In cases where there was no gitweb config at all, this would
+result in broken links due to `null` being used as the URL.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4488[Issue 4488]:
+Improve error message when `Change-Id` line is missing in commit message.
++
+The error message now includes the sha1 of the commit, so that it is
+easier to track down which commit failed validation when multiple commits
+are pushed at the same time.
+
+* Don't check mergeability of draft changes.
++
+Draft changes can be deleted but not abandoned so there is no way for
+an administrator to get rid of the them on behalf of the users. This can
+become a problem when there many draft changes because the mergeability
+check can be costly.
++
+The mergeability check is no longer done for draft changes, but will be
+done when the draft change is published.
+
+* Fix internal server error when plugin-provided file history weblink
+is null.
++
+It is valid for a plugin to provide a null weblink, but doing so resulted
+in an internal server error.
+
+== 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.7
+
+* Upgrade Guice to 4.1.0
+
+* Upgrade gwtjsonrpc to 1.9
+
+* Upgrade gwtorm to 1.15
+
+* Upgrade javassist to 3.20.0-GA
+
+* Upgrade Jetty to 9.2.14.v20151106
+
+* Upgrade JGit to 4.5.0.201609210915-r
+
+* Upgrade joda-convert to 1.8.1
+
+* 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
diff --git a/ReleaseNotes/ReleaseNotes-2.2.0.txt b/ReleaseNotes/ReleaseNotes-2.2.0.txt
index 5938f66..5cc54f9 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.0.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.0.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.2.0
-==============================
+= Release notes for Gerrit 2.2.0
 
 Gerrit 2.2.0 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.2.0.war[https://www.gerritcodereview.com/download/gerrit-2.2.0.war]
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* Upgrading to 2.2.0 requires the server be first upgraded
 to 2.1.7, and then to 2.2.0.
 
@@ -21,11 +19,9 @@
 Git repository. The init based upgrade tool will automatically
 export the current table contents and create the Git data.
 
-New Features
-------------
+== New Features
 
-Project Administration
-~~~~~~~~~~~~~~~~~~~~~~
+=== Project Administration
 * issue 436 List projects by scanning the managed Git directory
 +
 Instead of generating the list of projects from SQL database, the
@@ -52,11 +48,9 @@
 The Access panel of the project administration has been rewritten
 with a new UI that reflects the new Git based storage format.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Project Administration
-~~~~~~~~~~~~~~~~~~~~~~
+=== Project Administration
 * Avoid unnecessary updates to $GIT_DIR/description
 +
 Gerrit always tried to rewrite the gitweb "description" file when the
diff --git a/ReleaseNotes/ReleaseNotes-2.2.1.txt b/ReleaseNotes/ReleaseNotes-2.2.1.txt
index 6a4829e..26aa8db 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.1.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.2.1
-==============================
+= Release notes for Gerrit 2.2.1
 
 Gerrit 2.2.1 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.2.1.war[https://www.gerritcodereview.com/download/gerrit-2.2.1.war]
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -15,8 +13,7 @@
 *WARNING:* Upgrading to 2.2.x requires the server be first upgraded
 to 2.1.7, and then to 2.2.x.
 
-New Features
-------------
+== New Features
 * Add 'Expand All Comments' checkbox in PatchScreen
 +
 Allows users to save a user preference that automatically expands
@@ -28,8 +25,7 @@
 usage adds a new column of output per project line listing the
 current value of that branch.
 
-Bug Fixes
----------
+== Bug Fixes
 * issue 994 Rename "-- All Projects --" to "All-Projects"
 +
 The name "-- All Projects --.git" is difficult to work with on
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.1.txt b/ReleaseNotes/ReleaseNotes-2.2.2.1.txt
index aabe03a..37f5a76 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.2.2.1
-================================
+= Release notes for Gerrit 2.2.2.1
 
 Gerrit 2.2.2.1 is now available:
 
@@ -11,8 +10,7 @@
 link:ReleaseNotes-2.2.2.html[ReleaseNotes].
 
 
-Bug Fixes
----------
+== Bug Fixes
 * issue 1139 Fix change state in patch set approval if reviewer is added to
 closed change
 +
@@ -35,8 +33,7 @@
 commit 14246de3c0f81c06bba8d4530e6bf00e918c11b0
 
 
-Documentation
--------------
+== Documentation
 * Update top level SUBMITTING_PATCHES
 +
 This document is out of date, the URLs are from last August.
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
index 2747ab0..f50c4e7 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.2.2.2
-================================
+= Release notes for Gerrit 2.2.2.2
 
 Gerrit 2.2.2.2 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from anything earlier, follow the upgrade
 procedure in the 2.2.2 link:ReleaseNotes-2.2.2.html[ReleaseNotes].
 
-Security Fixes
---------------
+== Security Fixes
 * Some access control sections may be ignored
 +
 Gerrit sometimes ignored an access control section in a project
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.txt
index 3889dcc..276714c 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.2.2
-==============================
+= Release notes for Gerrit 2.2.2
 
 Gerrit 2.2.2 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.2.2.war[https://www.gerritcodereview.com/download/gerrit-2.2.2.war]
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -15,11 +13,9 @@
 *WARNING:* Upgrading to 2.2.x requires the server be first upgraded
 to 2.1.7 (or a later 2.1.x version), and then to 2.2.x.
 
-New Features
-------------
+== New Features
 
-Prolog
-~~~~~~
+=== Prolog
 * issue 971 Use Prolog Cafe for ChangeControl.canSubmit()
 
 *  Add per-project prolog submit rule files
@@ -59,8 +55,7 @@
 administrators can play around with by downloading the Gerrit WAR
 file and executing: java -jar gerrit.war prolog-shell
 
-Prolog Predicates
-^^^^^^^^^^^^^^^^^
+==== Prolog Predicates
 *  Add Prolog Predicates to check commit messages and edits
 +
 commit_message returns the commit message as a symbol.
@@ -104,8 +99,7 @@
 it exportable for now until we can come back and clean up the legacy
 approval data code.
 
-Web
-~~~
+=== Web
 
 * Support in Firefox delete key in NpIntTextBox
 +
@@ -118,8 +112,7 @@
 There is a bug in gwt 2.1.0 that prevents pressing special keys like
 Enter, Backspace etc. from being properly recognized and so they have no effect.
 
-ChangeScreen
-^^^^^^^^^^^^
+==== ChangeScreen
 * issue 855 Indicate outdated dependencies on the ChangeScreen
 +
 If a change dependency is no longer the latest patchSet for that
@@ -142,8 +135,7 @@
 permits the submit_rule to make an ApprovalCategory optional, or to
 make a new label required.
 
-Diff Screen
-^^^^^^^^^^^
+==== Diff Screen
 * Add top level menus for a new PatchScreen header
 +
 Modify the PatchScreen so that the header contents is selectable
@@ -183,8 +175,7 @@
 automatic result that Git would create, and the actual result that
 was uploaded by the author/committer of the merge.
 
-Groups
-^^^^^^
+==== Groups
 * Add menu to AccountGroupScreen
 +
 This change introduces a menu in the AccountGroupScreen and
@@ -199,8 +190,7 @@
 'groups' file in the 'refs/meta/config' branch which requires
 the UUID of the group to be known.
 
-Project Access
-^^^^^^^^^^^^^^
+==== Project Access
 * Automatically add new rule when adding new permission
 +
 If a new permission was added to a block, immediately create the new
@@ -218,8 +208,7 @@
 switch back to the "read-only" view where the widgets are all
 disabled and the Edit button is enabled.
 
-Project Branches
-^^^^^^^^^^^^^^^^
+==== Project Branches
 * Display refs/meta/config branch on ProjectBranchesScreen
 +
 The new refs/meta/config branch was not shown in the ProjectBranchesScreen.
@@ -231,8 +220,7 @@
 Since HEAD and refs/meta/config do not represent ordinary branches,
 highlight their rows with a special style in the ProjectBranchesScreen.
 
-URLs
-^^^^
+==== URLs
 * Modernize URLs to be shorter and consistent
 +
 Instead of http://site/#change,1234 we now use a slightly more
@@ -250,8 +238,7 @@
 
 * issue 1018 Accept ~ in linkify() URLs
 
-SSH
-~~~
+=== SSH
 * Added a set-reviewers ssh command
 
 * Support removing more than one reviewer at once
@@ -290,8 +277,7 @@
 will report additional data about the JVM, and tell the caller
 where it is running.
 
-Queries
-^^^^^^^
+==== Queries
 * Output patchset creation date for 'query' command.
 
 * issue 1053 Support comments option in query command
@@ -300,8 +286,7 @@
 used. If --comments is used together with --patch-sets all inline
 comments are included in the output.
 
-Config
-~~~~~~
+=== Config
 * Move batch user priority to a capability
 +
 Instead of using a magical group, use a special capability to
@@ -351,8 +336,7 @@
 This allows aliases which redirect to gerrit's ssh port (say
 from port 22) to be setup and advertised to users.
 
-Dev
-~~~
+=== Dev
 * Updated eclipse settings for 3.7 and m2e 1.0
 
 * Fix build in m2eclipse 1.0
@@ -375,8 +359,7 @@
 switching between different users when using the
 DEVELOPMENT_BECOME_ANY_ACCOUNT authentication type.
 
-Miscellaneous
-~~~~~~~~~~~~~
+=== Miscellaneous
 * Permit adding reviewers to closed changes
 +
 Permit adding a reviewer to closed changes to support post-submit
@@ -412,8 +395,7 @@
 gerrit page load.
 
 
-Performance
------------
+== Performance
 * Bumped Brics version to 1.11.8
 +
 This Brics version fixes a performance issue in some larger Gerrit systems.
@@ -453,8 +435,7 @@
 during the construction of ProjectState is a waste of resources.
 
 
-Upgrades
---------
+== Upgrades
 * Upgrade to GWT 2.3.0
 * Upgrade to Gson to 1.7.1
 * Upgrade to gwtjsonrpc 1.2.4
@@ -463,8 +444,7 @@
 * Upgrade to Brics 1.11.8
 
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix: Issue where Gerrit could not linkify certain URLs
 
 * issue 1015 Fix handling of regex ref patterns in Access panel
@@ -563,11 +543,9 @@
 duplicate account_ids later.
 
 
-Documentation
--------------
+== Documentation
 
-New Documents
-~~~~~~~~~~~~~
+=== New Documents
 * First Cut of Gerrit Walkthrough Introduction documentation.
 +
 Add a new document intended to be a complement for the existing
@@ -580,8 +558,7 @@
 The new document covers quick installation, new project and first
 upload.  It contains lots of quoted output, with a demo style to it.
 
-Access Control
-~~~~~~~~~~~~~~
+=== Access Control
 * Code review
 
 * Conversion table between 2.1 and 2.2
@@ -614,8 +591,7 @@
 +
 Access categories are now sorted to match drop down box in UI
 
-Other Documentation
-~~~~~~~~~~~~~~~~~~~
+=== Other Documentation
 * Added additional information on the install instructions.
 +
 The installation instructions presumes much prior knowledge,
diff --git a/ReleaseNotes/ReleaseNotes-2.3.1.txt b/ReleaseNotes/ReleaseNotes-2.3.1.txt
index 8914c69..627fba5 100644
--- a/ReleaseNotes/ReleaseNotes-2.3.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.3.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.3.1
-==============================
+= Release notes for Gerrit 2.3.1
 
 Gerrit 2.3.1 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from anything earlier, follow the upgrade
 procedure in the 2.3 link:ReleaseNotes-2.3.html[ReleaseNotes].
 
-Security Fixes
---------------
+== Security Fixes
 * Some access control sections may be ignored
 +
 Gerrit sometimes ignored an access control section in a project
diff --git a/ReleaseNotes/ReleaseNotes-2.3.txt b/ReleaseNotes/ReleaseNotes-2.3.txt
index 9cdc886..7a29d0e 100644
--- a/ReleaseNotes/ReleaseNotes-2.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.3.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.3
-============================
+= Release notes for Gerrit 2.3
 
 Gerrit 2.3 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.3.war[https://www.gerritcodereview.com/download/gerrit-2.3.war]
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -19,10 +17,8 @@
 upgrade directly to 2.3.x.
 
 
-New Features
-------------
-Drafts
-~~~~~~
+== New Features
+=== Drafts
 * New draft statuses and magic branches
 +
 Adds draft status to Change. DRAFT status in change occurs before NEW
@@ -65,8 +61,7 @@
 * When pushing changes as drafts, output [DRAFT] next to the change link
 
 
-Web
-~~~
+=== Web
 * issue 203 Create project through web interface
 +
 Add a new panel in the Admin->Projects Screen.  It
@@ -106,8 +101,7 @@
 * Disable SSH Keys in the web UI if SSHD is disabled
 
 
-SSH
-~~~
+=== SSH
 * Adds --description (-d) option to ls-projects
 +
 Allows listing of projects together with their respective
@@ -167,8 +161,7 @@
 labels could not be applied due to change being closed.
 
 
-Config
-~~~~~~
+=== Config
 * issue 349 Apply states for projects (active, readonly and hidden)
 +
 Active state indicates the project is regular and is the default value.
@@ -250,8 +243,7 @@
 logic associated with the site header, footer and CSS.
 
 
-Dev
-~~~
+=== Dev
 * Fix 'No source code is available for type org.eclipse.jgit.lib.Constants'
 
 * Fix miscellaneous compiler warnings
@@ -270,8 +262,7 @@
 Fixes java.lang.NoClassDefFoundError: com/google/gwt/dev/DevMode
 
 
-Miscellaneous
-~~~~~~~~~~~~~
+=== Miscellaneous
 * Allow superprojects to subscribe to submodules updates
 +
 The feature introduced in this release allows superprojects to
@@ -370,8 +361,7 @@
 * Improve validation of email registration tokens
 
 
-Upgrades
---------
+== Upgrades
 * Upgrade to gwtorm 1.2
 
 * Upgrade to JGit 1.1.0.201109151100-r.119-gb4495d1
@@ -382,8 +372,7 @@
 * Support Velocity 1.5 (as well as previous 1.6.4)
 
 
-Bug Fixes
----------
+== Bug Fixes
 * Avoid NPE when group is missing
 
 * Do not fail with NPE if context path of request is null
@@ -437,8 +426,7 @@
 * Update top level SUBMITTING_PATCHES URLs
 
 
-Documentation
--------------
+== Documentation
 * Some updates to the design docs
 
 * cmd-index: Fix link to documentation of rename-group command
diff --git a/ReleaseNotes/ReleaseNotes-2.4.1.txt b/ReleaseNotes/ReleaseNotes-2.4.1.txt
index dbe6c4b..f3c4765 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.4.1
-==============================
+= Release notes for Gerrit 2.4.1
 
 Gerrit 2.4.1 is now available:
 
@@ -11,8 +10,7 @@
 link:ReleaseNotes-2.4.html[ReleaseNotes].
 
 
-Bug Fixes
----------
+== Bug Fixes
 * Catch all exceptions when async emailing
 +
 This fixes email notification issues reported
diff --git a/ReleaseNotes/ReleaseNotes-2.4.2.txt b/ReleaseNotes/ReleaseNotes-2.4.2.txt
index 5652d15..d5c2a11 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.2.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.4.2
-==============================
+= Release notes for Gerrit 2.4.2
 
 Gerrit 2.4.2 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from anything earlier, follow the upgrade
 procedure in the 2.4 link:ReleaseNotes-2.4.html[ReleaseNotes].
 
-Security Fixes
---------------
+== Security Fixes
 * Some access control sections may be ignored
 +
 Gerrit sometimes ignored an access control section in a project
diff --git a/ReleaseNotes/ReleaseNotes-2.4.3.txt b/ReleaseNotes/ReleaseNotes-2.4.3.txt
index 6745564..ece0bda 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.3.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.4.3
-==============================
+= Release notes for Gerrit 2.4.3
 
 There are no schema changes from link:ReleaseNotes-2.4.2.html[2.4.2].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.4.3.war[https://www.gerritcodereview.com/download/gerrit-2.4.3.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Patch JGit security hole
 +
 The security hole may permit a modified Git client to gain access
diff --git a/ReleaseNotes/ReleaseNotes-2.4.4.txt b/ReleaseNotes/ReleaseNotes-2.4.4.txt
index 5570271..f9ea6b5 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.4.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.4.4
-==============================
+= Release notes for Gerrit 2.4.4
 
 There are no schema changes from link:ReleaseNotes-2.4.4.html[2.4.4].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.4.4.war[https://www.gerritcodereview.com/download/gerrit-2.4.4.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix clone for modern Git clients
 +
 The security fix in 2.4.3 broke clone for recent Git clients,
diff --git a/ReleaseNotes/ReleaseNotes-2.4.txt b/ReleaseNotes/ReleaseNotes-2.4.txt
index 0e11550..1db4ba3 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.4
-============================
+= Release notes for Gerrit 2.4
 
 Gerrit 2.4 is now available:
 
 link:https://www.gerritcodereview.com/download/gerrit-2.4.war[https://www.gerritcodereview.com/download/gerrit-2.4.war]
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -16,11 +14,9 @@
 a later 2.1.x version), and then to 2.4.x.  If you are upgrading from 2.2.x.x or
 newer, you may ignore this warning and upgrade directly to 2.4.x.
 
-New Features
-------------
+== New Features
 
-Security
-~~~~~~~~
+=== Security
 
 * Restrict visibility to arbitrary user dashboards
 +
@@ -42,8 +38,7 @@
 
 * Indicate that 'not found' may actually be a permission issue
 
-Web
-~~~
+=== Web
 
 * Add user preference to mark files reviewed automatically or manually
 +
@@ -82,14 +77,12 @@
 * Change 'Loading ...' to say 'Working ...' as, often, there is more going on
 than just loading a response.
 
-Performance
-~~~~~~~~~~~
+=== Performance
 
 * Asynchronously send email so it does not block the UI
 * Optimize queries for open/merged changes by project + branch
 
-Git
-~~~
+=== Git
 
 * Implement a multi-sub-task progress monitor for ReceiveCommits
 
@@ -116,8 +109,7 @@
 can be monitored for timeouts and cancelled, and have stalls reported
 to the user from the main thread.
 
-Search
-~~~~~~
+=== Search
 
 * Add the '--dependencies' option to the 'query' command.
 +
@@ -141,15 +133,13 @@
 With this change, we can fetch the comments on a patchset by sending a
 request to 'https://site/query?comments=true'
 
-Access Rights
-~~~~~~~~~~~~~
+=== Access Rights
 
 * Added the 'emailReviewers' as a global capability.
 +
 This replaces the 'emailOnlyAuthors' flag of account groups.
 
-Dev
-~~~
+=== Dev
 
 * issue 1272 Add scripts to create release notes from git log
 +
@@ -164,8 +154,7 @@
 
 * Add '--issues' and '--issue_numbers' options to the 'gitlog2asciidoc.py'
 
-Miscellaneous
-~~~~~~~~~~~~~
+=== Miscellaneous
 
 * Remove perl from 'commit-msg' hook
 +
@@ -174,8 +163,7 @@
 
 * updating contrib 'trivial_rebase.py' for 2.2.2.1
 
-Upgrades
---------
+== Upgrades
 
 * Updated to Guice 3.0.
 * Updated to gwtorm 1.4.
@@ -185,8 +173,7 @@
 The change also shrinks the built WAR from 38M to 23M
 by excluding the now unnecessary GWT server code.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * issue 904 Users who starred a change should receive all the emails about a change.
 
@@ -226,11 +213,9 @@
 * Fix inconsistent behavior when replicating refs/meta/config
 * Fix duplicated results on status:open project:P branch:B
 
-Documentation
--------------
+== Documentation
 
-Access Rights
-~~~~~~~~~~~~~
+=== Access Rights
 * Capabilities introduced
 * Kill and priority capabilities
 * Administrate server capability
@@ -246,8 +231,7 @@
 * Project owner example role
 * Administrator example role
 
-Miscellaneous
-~~~~~~~~~~~~~
+=== Miscellaneous
 * User upload documentation: Replace changes
 * Add visible-to-all flag in the documentation for cmd-create-group
 * Add a contributing guideline for annotations
diff --git a/ReleaseNotes/ReleaseNotes-2.5.1.txt b/ReleaseNotes/ReleaseNotes-2.5.1.txt
index 6e6a481..c2982df 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.5.1
-==============================
+= Release notes for Gerrit 2.5.1
 
 Gerrit 2.5.1 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from a version older than 2.5, follow the upgrade
 procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
 
-Security Fixes
---------------
+== Security Fixes
 * Correctly identify Git-over-HTTP operations
 +
 Git operations over HTTP should be classified as using AccessPath.GIT
@@ -45,8 +43,7 @@
   project by definition has no parent)
 by pushing changes of the `project.config` file to `refs/meta/config`.
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix RequestCleanup bug with Git over HTTP
 +
 Decide if a continuation is going to be used early, before the filter
diff --git a/ReleaseNotes/ReleaseNotes-2.5.2.txt b/ReleaseNotes/ReleaseNotes-2.5.2.txt
index cc436ac..9bedeac 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.2.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.5.2
-==============================
+= Release notes for Gerrit 2.5.2
 
 Gerrit 2.5.2 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from any earlier version, follow the upgrade
 procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
 
-Bug Fixes
----------
+== Bug Fixes
 * Improve performance of ReceiveCommits for repos with many refs
 +
 When validating the received commits all existing refs were added as
diff --git a/ReleaseNotes/ReleaseNotes-2.5.3.txt b/ReleaseNotes/ReleaseNotes-2.5.3.txt
index 8e9db0c..6448f1c 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.3.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.5.3
-==============================
+= Release notes for Gerrit 2.5.3
 
 Gerrit 2.5.3 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from a version older than 2.5, follow the upgrade
 procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
 
-Security Fixes
---------------
+== Security Fixes
 * Patch vulnerabilities in OpenID client library
 +
 Installations using OpenID for authentication were vulnerable to a
diff --git a/ReleaseNotes/ReleaseNotes-2.5.4.txt b/ReleaseNotes/ReleaseNotes-2.5.4.txt
index 4d51528..6ea93bb 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.4.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.5.4
-==============================
+= Release notes for Gerrit 2.5.4
 
 Gerrit 2.5.4 is now available:
 
@@ -10,8 +9,7 @@
 However, if upgrading from a version older than 2.5, follow the upgrade
 procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
 
-Bug Fixes
----------
+== Bug Fixes
 * Require preferred email to be verified
 +
 Some users were able to select a preferred email address that was
diff --git a/ReleaseNotes/ReleaseNotes-2.5.5.txt b/ReleaseNotes/ReleaseNotes-2.5.5.txt
index 146fd40..27dd2b6 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.5.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.5.5
-==============================
+= Release notes for Gerrit 2.5.5
 
 There are no schema changes from link:ReleaseNotes-2.5.4.html[2.5.4].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.5.5.war[https://www.gerritcodereview.com/download/gerrit-2.5.5.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Patch JGit security hole
 +
 The security hole may permit a modified Git client to gain access
diff --git a/ReleaseNotes/ReleaseNotes-2.5.6.txt b/ReleaseNotes/ReleaseNotes-2.5.6.txt
index b1e88f9..393eb93 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.6.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.5.6
-==============================
+= Release notes for Gerrit 2.5.6
 
 There are no schema changes from link:ReleaseNotes-2.5.6.html[2.5.6].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.5.6.war[https://www.gerritcodereview.com/download/gerrit-2.5.6.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Fix clone for modern Git clients
 +
 The security fix in 2.5.4 broke clone for recent Git clients,
diff --git a/ReleaseNotes/ReleaseNotes-2.5.txt b/ReleaseNotes/ReleaseNotes-2.5.txt
index 8519ee9..6bcb87a 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.5
-============================
+= Release notes for Gerrit 2.5
 
 Gerrit 2.5 is now available:
 
@@ -10,8 +9,7 @@
 link:ReleaseNotes-2.4.2.html[Gerrit 2.4.2]. These bug fixes are *not*
 listed in these release notes.
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -21,8 +19,7 @@
 a later 2.1.x version), and then to 2.5.x.  If you are upgrading from 2.2.x.x or
 newer, you may ignore this warning and upgrade directly to 2.5.x.
 
-Warning on upgrade to schema version 68
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Warning on upgrade to schema version 68
 
 The migration to schema version 68, may result in a warning, which can
 be ignored when running init in the interactive mode.
@@ -48,19 +45,16 @@
 message you can see that the migration failed because the index exists
 already (as in the example above), you can safely ignore this warning.
 
-Upgrade Warnings
-----------------
+== Upgrade Warnings
 
 [[replication]]
-Replication
-~~~~~~~~~~~
+=== Replication
 
 Gerrit 2.5 no longer includes replication support out of the box.
 Servers that reply upon `replication.config` to copy Git repository
 data to other locations must also install the replication plugin.
 
-Cache Configuration
-~~~~~~~~~~~~~~~~~~~
+=== Cache Configuration
 
 Disk caches are now backed by individual H2 databases, rather than
 Ehcache's own private format. Administrators are encouraged to clear
@@ -98,11 +92,9 @@
 updates are often made to the source without telling Gerrit to reload
 the cache.
 
-New Features
-------------
+== New Features
 
-Plugins
-~~~~~~~
+=== Plugins
 
 The Gerrit server functionality can be extended by installing plugins.
 Depending on how tightly the extension code is coupled with the Gerrit
@@ -261,8 +253,7 @@
 +
 This enables plugins to make use of servlet sessions.
 
-REST API
-~~~~~~~~
+=== REST API
 Gerrit now supports a REST like API available over HTTP. The API is
 suitable for automated tools to build upon, as well as supporting some
 ad-hoc scripting use cases.
@@ -302,11 +293,9 @@
 `site.enableDeprecatedQuery`] parameter in the Gerrit config file. This
 allows to enforce tools to move to the new API.
 
-Web
-~~~
+=== Web
 
-Change Screen
-^^^^^^^^^^^^^
+==== Change Screen
 
 * Display commit message in a box
 +
@@ -384,8 +373,7 @@
 
 * Use more gentle shade of red to highlight outdated dependencies
 
-Patch Screens
-^^^^^^^^^^^^^
+==== Patch Screens
 
 * New patch screen header
 +
@@ -447,8 +435,7 @@
 
 * Use download icons instead of the `Download` text links
 
-User Dashboard
-^^^^^^^^^^^^^^
+==== User Dashboard
 * Support for link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/user-custom-dashboards.html[custom
   dashboards]
 
@@ -472,8 +459,7 @@
 and the oldest stale at the bottom. This may help users to identify
 items to take immediate action on, as they appear closer to the top.
 
-Access Rights Screen
-^^^^^^^^^^^^^^^^^^^^
+==== Access Rights Screen
 
 * Display error if modifying access rights for a ref is forbidden
 +
@@ -512,8 +498,7 @@
 For project owners now also groups to which they are not a member are
 suggested when editing the access rights of the project.
 
-Other
-^^^^^
+==== Other
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=1592[issue 1592]:
   Ask user to login if change is not found
@@ -626,8 +611,7 @@
 The URL for the external system can be configured for the
 link:#custom-extension[`CUSTOM_EXTENSION`] auth type.
 
-Access Rights
-~~~~~~~~~~~~~
+=== Access Rights
 
 * Restrict rebasing of a change in the web UI to the change owner and
   the submitter
@@ -657,8 +641,7 @@
 `Anonymous users` were able to access the `refs/meta/config` branch
 which by default should only be accessible by the project owners.
 
-Search
-~~~~~~
+=== Search
 * Offer suggestions for the search operators in the search panel
 +
 There are many search operators and it's difficult to remember all of
@@ -678,8 +661,7 @@
 
 * `/query` API has been link:#query-deprecation[deprecated]
 
-SSH
-~~~
+=== SSH
 * link:http://code.google.com/p/gerrit/issues/detail?id=1095[issue 1095]
   link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-set-account.html[SSH command to manage
   accounts]
@@ -738,11 +720,9 @@
 command output a tab-separated table containing all available
 information about each group (though not its members).
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
-Commands
-^^^^^^^^
+==== Commands
 
 * document for the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-create-group.html[`create-group`]
   command that for unknown users an account is automatically created if
@@ -770,8 +750,7 @@
 
 * Fix and complete synopsis of commands
 
-Access Control
-^^^^^^^^^^^^^^
+==== Access Control
 
 * Clarify the ref format for
   link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/access-control.html#category_push_merge[`Push
@@ -785,8 +764,7 @@
   link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/access-control.html#capability_emailReviewers[
   `emailReviewers`] capability
 
-Error
-^^^^^
+==== Error
 * Improve documentation of link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/error-change-closed.html[
   `change closed` error]
 +
@@ -806,8 +784,7 @@
   a tag fails because the tagger is somebody else and the `Forge
   Committer` access right is not assigned.
 
-Dev
-^^^
+==== Dev
 
 * Update push URL in link:../SUBMITTING_PATCHES[SUBMITTING_PATCHES]
 +
@@ -840,8 +817,7 @@
 Document what it takes to make a Gerrit stable or stable-fix release,
 and how to release Gerrit subprojects.
 
-Other
-^^^^^
+==== Other
 * Add link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/prolog-cookbook.html[Cookbook for Prolog
   submit rules]
 +
@@ -884,8 +860,7 @@
 +
 Correct typos, spelling mistakes, and grammatical errors.
 
-Dev
-~~~
+=== Dev
 * Add link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-release.html#plugin-api[script for
   releasing plugin API jars]
 
@@ -948,8 +923,7 @@
 `all` profile so that the plugin modules are always built for release
 builds.
 
-Mail
-~~~~
+=== Mail
 
 * Add unified diff to newchange mail template
 +
@@ -997,8 +971,7 @@
 +
 Show the URL right away in the body.
 
-Miscellaneous
-~~~~~~~~~~~~~
+=== Miscellaneous
 * Back in-memory caches with Guava, disk caches with H2
 +
 Instead of using Ehcache for in-memory caches, use Guava. The Guava
@@ -1264,8 +1237,7 @@
 eventually go away when there is proper support for authentication
 plugins.
 
-Performance
-~~~~~~~~~~~
+=== Performance
 [[performance-issue-on-showing-group-list]]
 * Fix performance issues on showing the list of groups in the Gerrit
   WebUI
@@ -1399,8 +1371,7 @@
 front allows the database to send all requests to the backend as early
 as possible, allowing the network latency to overlap.
 
-Upgrades
---------
+== Upgrades
 * Update Gson to 2.1
 * Update GWT to 2.4.0
 * Update JGit to 2.0.0.201206130900-r.23-gb3dbf19
@@ -1416,11 +1387,9 @@
 inner table did not dynamically resize to handle a larger number
 of cached items, causing O(N) lookup performance for most objects.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Security
-~~~~~~~~
+=== Security
 * Ensure that only administrators can change the global capabilities
 +
 Only Gerrit server administrators (members of the groups that have
@@ -1468,8 +1437,7 @@
 Hence this check is currently not done and these access rights in this
 case have simply no effect.
 
-Web
-~~~
+=== Web
 
 * Do not show "Session cookie not available" on sign in
 +
@@ -1625,8 +1593,7 @@
 and redirect to 'Admin' > 'Projects' to show the projects the caller
 has access to.
 
-Mail
-~~~~
+=== Mail
 
 * Fix: Rebase did not mail all reviewers
 
@@ -1653,8 +1620,7 @@
 `Reverted.vm` were not extracted during the initialization of a new
 site.
 
-SSH
-~~~
+=== SSH
 * Fix reject message if bypassing code review is not allowed
 +
 If a user is not allowed to bypass code review, but tries to push a
@@ -1726,8 +1692,7 @@
 non existing project name this was logged in the `error.log` but
 printing the error out to the user is sufficient.
 
-Authentication
-~~~~~~~~~~~~~~
+=== Authentication
 
 * Fix NPE in LdapRealm caused by non-LDAP users
 +
@@ -1748,8 +1713,7 @@
 is valid. When the URL is missing (e.g. because the provider is
 still broken) rely on the context path of the application instead.
 
-Replication
-~~~~~~~~~~~
+=== Replication
 
 * Fix inconsistent behavior when replicating `refs/meta/config`
 +
@@ -1766,8 +1730,7 @@
 The groupCache was being used before it was set in the class. Fix the
 ordering of the assignment.
 
-Approval Categories
-~~~~~~~~~~~~~~~~~~~
+=== Approval Categories
 
 * Make `NoBlock` and `NoOp` approval category functions work
 +
@@ -1811,8 +1774,7 @@
 collection came out null, which cannot be iterated. Make it always be
 an empty list.
 
-Other
-~~~~~
+=== Other
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=1554[issue 1554]:
   Fix cloning of new projects from slave servers
diff --git a/ReleaseNotes/ReleaseNotes-2.6.1.txt b/ReleaseNotes/ReleaseNotes-2.6.1.txt
index e43b077..94de483 100644
--- a/ReleaseNotes/ReleaseNotes-2.6.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.6.1.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.6.1
-==============================
+= Release notes for Gerrit 2.6.1
 
 There are no schema changes from link:ReleaseNotes-2.6.html[2.6].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.6.1.war[https://www.gerritcodereview.com/download/gerrit-2.6.1.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * Patch JGit security hole
 +
 The security hole may permit a modified Git client to gain access
diff --git a/ReleaseNotes/ReleaseNotes-2.6.txt b/ReleaseNotes/ReleaseNotes-2.6.txt
index dfd5d80..26b0b0e 100644
--- a/ReleaseNotes/ReleaseNotes-2.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.6.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.6
-============================
+= Release notes for Gerrit 2.6
 
 Gerrit 2.6 is now available:
 
@@ -12,8 +11,7 @@
 link:ReleaseNotes-2.5.4.html[Gerrit 2.5.4]. These bug fixes are *not*
 listed in these release notes.
 
-Schema Change
--------------
+== Schema Change
 *WARNING:* This release contains schema changes.  To upgrade:
 ----
   java -jar gerrit.war init -d site_path
@@ -23,8 +21,7 @@
 a later 2.1.x version), and then to 2.6.x.  If you are upgrading from 2.2.x.x or
 newer, you may ignore this warning and upgrade directly to 2.6.x.
 
-Reverse Proxy Configuration Changes
------------------------------------
+== Reverse Proxy Configuration Changes
 
 If you are running a reverse proxy in front of Gerrit (e.g. Apache or Nginx),
 make sure to check your configuration, especially if you are encountering
@@ -34,8 +31,7 @@
 
 Gerrit now requires passed URLs to be unchanged by the proxy.
 
-Release Highlights
-------------------
+== Release Highlights
 * 42x improvement on `git clone` and `git fetch`
 +
 Running link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-gc.html[
@@ -51,14 +47,11 @@
 labels] and link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/prolog-cookbook.html[
 Prolog rules].
 
-New Features
-------------
+== New Features
 
-Web UI
-~~~~~~
+=== Web UI
 
-Global
-^^^^^^
+==== Global
 
 * New Login Screens
 +
@@ -93,8 +86,7 @@
 
 * Add a link to the REST API documentation in the top menu.
 
-Search
-^^^^^^
+==== Search
 * Suggest projects, groups and users in search panel
 +
 Suggest projects, groups and users in the search panel as parameter for
@@ -107,8 +99,7 @@
 The values that are suggested for the search operators in the search
 panel are now only quoted if they contain a whitespace.
 
-Change Screens
-^^^^^^^^^^^^^^
+==== Change Screens
 
 * A change's commit message can be edited from the change screen.
 
@@ -182,8 +173,7 @@
 
 * Rename "Old Version History" to "Reference Version".
 
-Patch Screens
-^^^^^^^^^^^^^
+==== Patch Screens
 
 * Support for file comments
 +
@@ -205,8 +195,7 @@
 
 * Enable expanding skipped lines even if 'Syntax Coloring' is off.
 
-Project Screens
-^^^^^^^^^^^^^^^
+==== Project Screens
 
 * Support filtering of projects in the project list screen
 +
@@ -260,8 +249,7 @@
 Improve the error messages that are displayed in the WebUI if the
 creation of a branch fails due to invalid user input.
 
-Group Screens
-^^^^^^^^^^^^^
+==== Group Screens
 
 * Support filtering of groups in the group list screen
 +
@@ -276,8 +264,7 @@
 well-known system groups which are of type 'SYSTEM'. The system groups
 are so well-known that there is no need to display the type for them.
 
-Dashboard Screens
-^^^^^^^^^^^^^^^^^
+==== Dashboard Screens
 
 * Link dashboard title to a URL version of itself
 +
@@ -290,8 +277,7 @@
 
 * Increase time span for "Recently Closed" section in user dashboard to 4 weeks.
 
-Account Screens
-^^^^^^^^^^^^^^^
+==== Account Screens
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1740[Issue 1740]:
   Display description how to generate SSH Key in SshPanel
@@ -302,16 +288,14 @@
 
 * Make the text for "Register" customizable
 
-Plugin Screens
-^^^^^^^^^^^^^^
+==== Plugin Screens
 
 * Show status for enabled plugins in the WebUI as 'Enabled'
 +
 Earlier no status was shown for enabled plugins, which was confusing to
 some users.
 
-REST API
-~~~~~~~~
+=== REST API
 
 * A big chunk of the Gerrit functionality is now available via the
   link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api.html[REST API].
@@ -443,8 +427,7 @@
 HTML thanks to Gson encoding HTML control characters using Unicode
 character escapes within JSON strings.
 
-Project Dashboards
-~~~~~~~~~~~~~~~~~~
+=== Project Dashboards
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/user-dashboards.html#project-dashboards[
   Support for storing custom dashboards for projects]
 +
@@ -472,8 +455,7 @@
 The `foreach` parameter which will get appended to all the queries in
 the dashboard.
 
-Access Controls
-~~~~~~~~~~~~~~~
+=== Access Controls
 * Allow to overrule `BLOCK` permissions on the same project
 +
 It was impossible to block a permission for a group and allow the same
@@ -567,8 +549,7 @@
 Having the `Push` access right for `refs/meta/config` on the
 `All-Projects` project without being administrator has no effect.
 
-Hooks
-~~~~~
+=== Hooks
 * Change topic is passed to hooks as `--topic NAME`.
 * link:https://code.google.com/p/gerrit/issues/detail?id=1200[Issue 1200]:
 New `reviewer-added` hook and stream event when a reviewer is added.
@@ -581,8 +562,7 @@
 
 * Add `--is-draft` parameter to `comment-added` hook
 
-Git
-~~~
+=== Git
 * Add options to `refs/for/` magic branch syntax
 +
 Git doesn't want to modify the network protocol to support passing
@@ -629,8 +609,7 @@
 
 * Add `oldObjectId` and `newObjectId` to the `GitReferenceUpdatedListener.Update`
 
-SSH
-~~~
+=== SSH
 * New SSH command to http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-gc.html[
   run Git garbage collection]
 +
@@ -659,8 +638,7 @@
 * http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-test-submit-type.html[
   test-submit type] tests the Prolog submit type with a chosen change.
 
-Query
-~~~~~
+=== Query
 * Allow `{}` to be used for quoting in query expressions
 +
 This makes it a little easier to query for group names that contain
@@ -676,8 +654,7 @@
 * When a file is renamed the old file name is included in the Patch
   attribute
 
-Plugins
-~~~~~~~
+=== Plugins
 * Plugins can contribute Prolog facts/predicates from Java.
 * Plugins can prompt for parameters during `init` with `InitStep`.
 * Plugins can now contribute JavaScript to the web UI. UI plugins can
@@ -697,8 +674,7 @@
 delegate to the plugin servlet's magic handling for static files and
 documentation. Add JAR attributes to configure these prefixes.
 
-Prolog
-~~~~~~
+=== Prolog
 [[submit-type-from-prolog]]
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/prolog-cookbook.html#HowToWriteSubmitType[
   Support controlling the submit type for changes from Prolog]
@@ -723,8 +699,7 @@
 
 * A new `max_with_block` predicate was added for more convenient usage
 
-Email
-~~~~~
+=== Email
 * Notify project watchers if draft change is published
 * Notify users mentioned in commit footer on draft publish
 * Add new notify type that allows watching of new patch sets
@@ -747,8 +722,7 @@
 which review updates should send email, and which categories of users
 on a change should get that email.
 
-Labels
-~~~~~~
+=== Labels
 * Approval categories stored in the database have been replaced with labels
   configured in `project.config`. Existing categories are migrated to
   `project.config` in `All-Projects` as part of the schema upgrade; no user
@@ -765,8 +739,7 @@
 or their own build system they can now trivially add the `Verified`
 category by pasting 5 lines into `project.config`.
 
-Dev
-~~~
+=== Dev
 
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/dev-readme.html#debug-javascript[
   Support loading debug JavaScript]
@@ -815,8 +788,7 @@
 * "Become Any Account" can be used for accounts whose full name is an empty string.
 
 
-Performance
-~~~~~~~~~~~
+=== Performance
 * Bitmap Optimizations
 +
 On running the http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-gc.html[
@@ -904,8 +876,7 @@
 database traffic a cache for changes was introduced. This cache is
 disabled by default since it can mess up multi-server setups.
 
-Misc
-~~~~
+=== Misc
 * Add config parameter to make new groups by default visible to all
 +
 Add a new http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#groups.newGroupsVisibleToAll[
@@ -1018,8 +989,7 @@
 
 * Show path to gerrit.war in command for upgrade schema
 
-Upgrades
-~~~~~~~~
+=== Upgrades
 * link:https://code.google.com/p/gerrit/issues/detail?id=1619[Issue 1619]:
 Embedded Jetty is now 8.1.7.v20120910.
 
@@ -1033,11 +1003,9 @@
 +
 Fixes some issues with IE9 and IE10.
 
-Bug Fixes
----------
+== Bug Fixes
 
-Web UI
-~~~~~~
+=== Web UI
 * link:https://code.google.com/p/gerrit/issues/detail?id=1662[Issue 1662]:
   Don't show error on ACL modification if empty permissions are added
 +
@@ -1234,8 +1202,7 @@
 Correctly keep patch set ordering after a new patch set is added via
 the Web UI.
 
-REST API
-~~~~~~~~
+=== REST API
 * Fix returning of 'Email Reviewers' capability via REST
 +
 The `/accounts/self/capabilities/` didn't return the 'Email Reviewers'
@@ -1249,8 +1216,7 @@
 * Provide a more descriptive error message for unauthenticated REST
   API access
 
-Git
-~~~
+=== Git
 * The wildcard `.` is now permitted in reference regex rules.
 
 * Checking if a change is mergeable no longer writes to the repository.
@@ -1356,8 +1322,7 @@
 "Only 0 of 0 new change refs created in xxx; aborting"
 could appear in the error log.
 
-SSH
-~~~
+=== SSH
 * `review --restore` allows a review score to be added on the restored change.
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1721[Issue 1721]:
@@ -1379,8 +1344,7 @@
 
 * Fix setting account's full name via ssh.
 
-Query
-~~~~~
+=== Query
 * link:https://code.google.com/p/gerrit/issues/detail?id=1729[Issue 1729]:
   Fix query by 'label:Verified=0'
 
@@ -1389,8 +1353,7 @@
 
 * Fix query cost for "status:merged commit:c0ffee"
 
-Plugins
-~~~~~~~
+=== Plugins
 * Skip disabled plugins on rescan
 +
 In a background thread Gerrit periodically scans for new or changed
@@ -1450,8 +1413,7 @@
   Allow InternalUser (aka plugins) to see any internal group, and run
   plugin startup and shutdown as PluginUser.
 
-Email
-~~~~~
+=== Email
 * Merge failure emails are only sent once per day.
 * Unused macros are removed from the mail templates.
 * Unnecessary ellipses are no longer applied to email subjects.
@@ -1467,8 +1429,7 @@
 If a user is watching 'All Comments' on `All-Projects` this should
 apply to all projects.
 
-Misc
-~~~~
+=== Misc
 * Provide more descriptive message for NoSuchProjectException
 
 * On internal error due to receive timeout include the value of
@@ -1630,15 +1591,13 @@
 standard `Authorization` header unspecified and available for use in
 HTTP reverse proxies.
 
-Documentation
--------------
+== Documentation
 
 The link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/index.html[
 documentation index] is restructured to make it easier to use for different kinds of
 users.
 
-User Documentation
-~~~~~~~~~~~~~~~~~~
+=== User Documentation
 * Split link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api.html[
   REST API documentation] and have one page per top level resource
 
@@ -1750,8 +1709,7 @@
 * Fix external links in 2.0.21 and 2.0.24 release notes
 * Manual pages can be optionally created/installed for core gerrit ssh commands.
 
-Developer And Maintainer Documentation
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Developer And Maintainer Documentation
 * Updated the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/dev-eclipse.html#maven[
   Maven plugin installation instructions] for Eclipse 3.7 (Indigo).
 
diff --git a/ReleaseNotes/ReleaseNotes-2.7.txt b/ReleaseNotes/ReleaseNotes-2.7.txt
index 9782b08..0870cbf 100644
--- a/ReleaseNotes/ReleaseNotes-2.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.7.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.7
-============================
+= Release notes for Gerrit 2.7
 
 
 Gerrit 2.7 is now available:
@@ -10,8 +9,7 @@
 Gerrit 2.7 includes the bug fixes done with link:ReleaseNotes-2.6.1.html[Gerrit 2.6.1].
 These bug fixes are *not* listed in these release notes.
 
-Schema Change
--------------
+== Schema Change
 
 
 *WARNING:* This release contains schema changes.  To upgrade:
@@ -25,8 +23,7 @@
 
 
 
-Gerrit Trigger Plugin in Jenkins
---------------------------------
+== Gerrit Trigger Plugin in Jenkins
 
 
 *WARNING:* Upgrading to 2.7 may cause the Gerrit Trigger Plugin in Jenkins to
@@ -34,8 +31,7 @@
 below.
 
 
-Release Highlights
-------------------
+== Release Highlights
 
 
 * New `copyMaxScore` setting for labels.
@@ -46,12 +42,10 @@
 * Several new REST APIs.
 
 
-New Features
-------------
+== New Features
 
 
-General
-~~~~~~~
+=== General
 
 * New `copyMaxScore` setting for labels.
 +
@@ -105,12 +99,10 @@
 * Allow administrators to see all groups.
 
 
-Web UI
-~~~~~~
+=== Web UI
 
 
-Global
-^^^^^^
+==== Global
 
 * User avatars are displayed in more places in the Web UI.
 
@@ -120,8 +112,7 @@
 mouse over avatar images.
 
 
-Change Screens
-^^^^^^^^^^^^^^
+==== Change Screens
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=667[Issue 667]:
@@ -140,8 +131,7 @@
 change comments.
 
 
-Diff Screens
-^^^^^^^^^^^^
+==== Diff Screens
 
 * Show images in side-by-side and unified diffs.
 
@@ -150,15 +140,13 @@
 * Harmonize unified diff's styling of images with that of text.
 
 
-REST API
-~~~~~~~~
+=== REST API
 
 
 Several new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api.html[
 REST API endpoints] are added.
 
-Accounts
-^^^^^^^^
+==== Accounts
 
 
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-accounts.html#get-diff-preferences[
@@ -168,8 +156,7 @@
 Set account diff preferences]
 
 
-Changes
-^^^^^^^
+==== Changes
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1820[Issue 1820]:
@@ -181,16 +168,14 @@
 
 
 
-Projects
-^^^^^^^^
+==== Projects
 
 
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-projects.html#get-config[
 Get project configuration]
 
 
-ssh
-~~~
+=== ssh
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1088[Issue 1088]:
@@ -198,11 +183,9 @@
 Kerberos authentication for ssh interaction].
 
 
-Bug Fixes
----------
+== Bug Fixes
 
-General
-~~~~~~~
+=== General
 
 * Postpone check for first account until adding an account.
 
@@ -240,8 +223,7 @@
 draft was already deleted.
 
 
-Web UI
-~~~~~~
+=== Web UI
 
 
 * Properly handle double-click on external group in GroupTable.
@@ -282,8 +264,7 @@
 Fix browser null-pointer exception when ChangeCache is incomplete.
 
 
-REST API
-~~~~~~~~
+=== REST API
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1819[Issue 1819]:
@@ -294,37 +275,32 @@
 * Correct URL encoding in 'GroupInfo'.
 
 
-Email
-~~~~~
+=== Email
 
 * Log failure to access reviewer list for notification emails.
 
 * Log when appropriate if email delivery is skipped.
 
 
-ssh
-~~~
+=== ssh
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2016[Issue 2016]:
 Flush caches after adding or deleting ssh keys via the `set-account` ssh command.
 
-Tools
-~~~~~
+=== Tools
 
 
 * The release build now builds for all browser configurations.
 
 
-Upgrades
---------
+== Upgrades
 
 * `gwtexpui` is now built in the gerrit tree rather than linking a separate module.
 
 
 
-Documentation
--------------
+== Documentation
 
 
 * Update the access control documentation to clarify how to set
diff --git a/ReleaseNotes/ReleaseNotes-2.8.1.txt b/ReleaseNotes/ReleaseNotes-2.8.1.txt
index 26414a1..5e32cf5 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.1.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.8.1
-==============================
+= Release notes for Gerrit 2.8.1
 
 There are no schema changes from link:ReleaseNotes-2.8.html[2.8].
 
 link:https://www.gerritcodereview.com/download/gerrit-2.8.1.war[https://www.gerritcodereview.com/download/gerrit-2.8.1.war]
 
-Bug Fixes
----------
+== Bug Fixes
 * link:https://code.google.com/p/gerrit/issues/detail?id=2073[Issue 2073]:
 Changes that depend on outdated patch sets were missing in the related changes list.
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.8.2.txt b/ReleaseNotes/ReleaseNotes-2.8.2.txt
index 926db02..99cb437 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.2.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.8.2
-==============================
+= Release notes for Gerrit 2.8.2
 
 There are no schema changes from link:ReleaseNotes-2.8.1.html[2.8.1].
 
@@ -8,8 +7,7 @@
 https://www.gerritcodereview.com/download/gerrit-2.8.2.war]
 
 
-Lucene Index
-------------
+== Lucene Index
 
 * Support committing Lucene writes within a fixed interval.
 +
@@ -24,8 +22,7 @@
 indexes are committed.
 
 
-General
--------
+== General
 
 * Only add "cherry picked from" when cherry picking a merged change.
 +
@@ -142,8 +139,7 @@
 The joda time library was being unnecessarily packaged in the root of
 the gerrit.war file.
 
-Change Screen / Diff Screen
----------------------------
+== Change Screen / Diff Screen
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2398[Issue 2398]:
@@ -231,8 +227,7 @@
 +
 Now, an error message will be displayed in the UI.
 
-ssh
----
+== ssh
 
 
 * Support for nio2 backend is removed.
@@ -263,8 +258,7 @@
 * link:https://code.google.com/p/gerrit/issues/detail?id=2515[Issue 2515]:
 Fix internal server error when updating an existing label with `gerrit review`.
 
-Replication Plugin
-------------------
+== Replication Plugin
 
 
 * Never replicate automerge-cache commits.
@@ -287,14 +281,12 @@
 * Update documentation to clarify replication of refs/meta/config when
 refspec is 'all refs'.
 
-Upgrades
---------
+== Upgrades
 
 
 * JGit is upgraded to 3.2.0.201312181205-r
 
-Documentation
--------------
+== Documentation
 
 
 * Add missing documentation of the secondary index configuration.
diff --git a/ReleaseNotes/ReleaseNotes-2.8.3.txt b/ReleaseNotes/ReleaseNotes-2.8.3.txt
index 2bd4aa7..f94dce0 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.3.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.8.3
-==============================
+= Release notes for Gerrit 2.8.3
 
 There are no schema changes from link:ReleaseNotes-2.8.2.html[2.8.2].
 
@@ -8,8 +7,7 @@
 https://www.gerritcodereview.com/download/gerrit-2.8.3.war]
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Fix for merging multiple changes with "Cherry Pick", "Merge Always" and
 "Merge If Necessary" strategies.
@@ -19,8 +17,7 @@
 them to actually land into the branch.
 
 
-Documentation
--------------
+== Documentation
 
 * Minor fixes in the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8.3/dev-buck.html[
diff --git a/ReleaseNotes/ReleaseNotes-2.8.4.txt b/ReleaseNotes/ReleaseNotes-2.8.4.txt
index b80ac17..8aac71c 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.4.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.8.4
-==============================
+= Release notes for Gerrit 2.8.4
 
 There are no schema changes from link:ReleaseNotes-2.8.3.html[2.8.3].
 
@@ -8,12 +7,10 @@
 https://www.gerritcodereview.com/download/gerrit-2.8.4.war]
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 
-Secondary Index
-~~~~~~~~~~~~~~~
+=== Secondary Index
 
 
 * Disable `commitWithin` when running Reindex.
@@ -31,8 +28,7 @@
 `SubIndex.NrtFuture` objects were being added as listeners of `searchManager`
 and never released.
 
-Change Screen
-~~~~~~~~~~~~~
+=== Change Screen
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2456[Issue 2456]:
@@ -84,8 +80,7 @@
 tooltip on the up arrow but did not show the tooltip on the left
 or right arrows.
 
-Plugins
-~~~~~~~
+=== Plugins
 
 
 * Fix ChangeListener auto-registered implementations.
@@ -98,8 +93,7 @@
 Plugins could be built, but not loaded, if they had any manifest entries
 that contained a dollar sign.
 
-Misc
-~~~~
+=== Misc
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2564[Issue 2564],
@@ -156,8 +150,7 @@
 so stream-events consumers can properly detect who uploaded the
 rebased patch set.
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1273[Issue 1273]:
diff --git a/ReleaseNotes/ReleaseNotes-2.8.5.txt b/ReleaseNotes/ReleaseNotes-2.8.5.txt
index db18083..ae30530 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.5.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.8.5
-==============================
+= Release notes for Gerrit 2.8.5
 
 Download:
 link:https://www.gerritcodereview.com/download/gerrit-2.8.5.war[
 https://www.gerritcodereview.com/download/gerrit-2.8.5.war]
 
-Schema Changes and Upgrades
----------------------------
+== Schema Changes and Upgrades
 
 
 * There are no schema changes from link:ReleaseNotes-2.8.4.html[2.8.4].
@@ -24,19 +22,16 @@
   java -jar gerrit.war init -d site_path
 ----
 
-Bug Fixes
----------
+== Bug Fixes
 
 
-Secondary Index
-~~~~~~~~~~~~~~~
+=== Secondary Index
 
 
 * Fix deadlocks on index shutdown.
 
 
-Change Screen
-~~~~~~~~~~~~~
+=== Change Screen
 
 
 * Only permit current patch set to edit the commit message.
@@ -68,8 +63,7 @@
 * Fix failure to load side-by-side diff due to "ISE EditIterator out of bounds"
 error.
 
-ssh
-~~~
+=== ssh
 
 * Upgrade SSHD to version 0.11.0.
 +
@@ -90,8 +84,7 @@
 link:https://issues.apache.org/jira/browse/SSHD-252[bug in SSHD].  That bug
 was fixed in SSHD version 0.10.0, so now we can re-enable nio2.
 
-Misc
-~~~~
+=== Misc
 
 
 * Keep old timestamps during data migration.
diff --git a/ReleaseNotes/ReleaseNotes-2.8.6.1.txt b/ReleaseNotes/ReleaseNotes-2.8.6.1.txt
index d1ed9e9..81e7297 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.6.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.6.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.8.6.1
-================================
+= Release notes for Gerrit 2.8.6.1
 
 There are no schema changes from link:ReleaseNotes-2.8.6.html[2.8.6].
 
@@ -7,8 +6,7 @@
 link:https://www.gerritcodereview.com/download/gerrit-2.8.6.1.war[
 https://www.gerritcodereview.com/download/gerrit-2.8.6.1.war]
 
-Bug Fixes
----------
+== Bug Fixes
 
 * The fix in 2.8.6 for the merge queue race condition caused a regression
 in database transaction handling.
@@ -17,7 +15,6 @@
 database support.
 
 
-Updates
--------
+== Updates
 
 * gwtorm is updated to 1.7.3
diff --git a/ReleaseNotes/ReleaseNotes-2.8.6.txt b/ReleaseNotes/ReleaseNotes-2.8.6.txt
index ab79a20..a810ad0 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.6.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.8.6
-==============================
+= Release notes for Gerrit 2.8.6
 
 There are no schema changes from link:ReleaseNotes-2.8.5.html[2.8.5].
 
@@ -10,8 +9,7 @@
 *Warning*: Support for MySQL's MyISAM storage engine is discontinued.
 Only transactional storage engines are supported.
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2034[Issue 2034],
 link:https://code.google.com/p/gerrit/issues/detail?id=2383[Issue 2383],
@@ -40,8 +38,7 @@
 * Fix sporadic SSHD handshake failures
 (link:https://issues.apache.org/jira/browse/SSHD-330[SSHD-330]).
 
-Updates
--------
+== Updates
 
 * gwtorm is updated to 1.7.1
 * sshd is updated to 0.11.1-atlassian-1
diff --git a/ReleaseNotes/ReleaseNotes-2.8.txt b/ReleaseNotes/ReleaseNotes-2.8.txt
index 2d1dc7a..472f0dc 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.8
-============================
+= Release notes for Gerrit 2.8
 
 
 Gerrit 2.8 is now available:
@@ -8,8 +7,7 @@
 https://www.gerritcodereview.com/download/gerrit-2.8.war]
 
 
-Schema Change
--------------
+== Schema Change
 
 
 *WARNING:* This release contains schema changes.  To upgrade:
@@ -46,8 +44,7 @@
 site initialization].
 
 
-Release Highlights
-------------------
+== Release Highlights
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/intro-change-screen.html[
@@ -70,11 +67,9 @@
 * New core plugin: Download Commands.
 
 
-New Features
-------------
+== New Features
 
-Build
-~~~~~
+=== Build
 
 * Gerrit is now built with
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/dev-buck.html[
@@ -83,8 +78,7 @@
 * Documentation is now built with Buck and link:http://asciidoctor.org[Asciidoctor].
 
 
-Indexing and Search
-~~~~~~~~~~~~~~~~~~~
+=== Indexing and Search
 
 Gerrit can be configured to use a
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#__a_id_index_a_section_index[
@@ -111,8 +105,7 @@
 reindex program] before restarting the Gerrit server.
 
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 * Project owners can define `receive.maxObjectSizeLimit` in the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#receive.maxObjectSizeLimit[
@@ -168,17 +161,14 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-labels.html#httpd.label_copyAllScoresOnTrivialRebase[
 configured to copy scores forward to new patch sets for trivial rebases].
 
-Web UI
-~~~~~~
+=== Web UI
 
 
-Global
-^^^^^^
+==== Global
 
 * The change status is shown in a separate column on dashboards and search results.
 
-Change Screens
-^^^^^^^^^^^^^^
+==== Change Screens
 
 
 * New change screen with completely redesigned UI, using the REST API.
@@ -220,8 +210,7 @@
 are uploaded.
 
 
-REST API
-~~~~~~~~
+=== REST API
 
 * Several new link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api.html[
 REST API endpoints] are added.
@@ -230,15 +219,13 @@
 
 * REST views can handle 'HTTP 422 Unprocessable Entity' responses.
 
-Access Rights
-^^^^^^^^^^^^^
+==== Access Rights
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-access.html#list-access[
 List access rights for project(s)]
 
-Accounts
-^^^^^^^^
+==== Accounts
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#create-account[
@@ -310,8 +297,7 @@
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#unstar-change[
 Unstar change]
 
-Changes
-^^^^^^^
+==== Changes
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-changes.html#rebase-change[
@@ -345,8 +331,7 @@
 Get included in]
 
 
-Config
-^^^^^^
+==== Config
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-config.html#get-capabilities[
 Get capabilities]
@@ -355,8 +340,7 @@
 Get version] (of the Gerrit server)
 
 
-Projects
-^^^^^^^^
+==== Projects
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-projects.html#list-branches[
@@ -381,8 +365,7 @@
 Set configuration]
 
 
-Capabilities
-~~~~~~~~~~~~
+=== Capabilities
 
 
 New global capabilities are added.
@@ -403,8 +386,7 @@
 explicitly.
 
 
-Emails
-~~~~~~
+=== Emails
 
 * The `RebasedPatchSet` template is removed.  Email notifications for rebased
 changes are now sent with the `ReplacePatchSet` template.
@@ -413,12 +395,10 @@
 to, and links to the file(s) in which comments are made.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
 
-Global
-^^^^^^
+==== Global
 
 
 * Plugins may now contribute buttons to various parts of the UI using the
@@ -475,14 +455,12 @@
 
 
 
-Commit Message Length Checker
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+==== Commit Message Length Checker
 
 
 * Commits whose subject or body length exceeds the limit can be rejected.
 
-Replication
-^^^^^^^^^^^
+==== Replication
 
 * Automatically create missing repositories on the destination.
 +
@@ -528,8 +506,7 @@
 delay.
 
 
-ssh
-~~~
+=== ssh
 
 
 * The `commit-msg` hook installation command is now
@@ -557,8 +534,7 @@
 * The 'CHANGEID,PATCHSET' format for specifying a patch set in the `review` command
 is no longer considered to be a 'legacy' feature that will be removed in future.
 
-Daemon
-~~~~~~
+=== Daemon
 
 
 * Add `--init` option to Daemon to initialize site on daemon start.
@@ -567,12 +543,10 @@
 non-interactive (batch) mode.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 
-General
-~~~~~~~
+=== General
 
 
 * Use the parent change on the same branch for rebases.
@@ -619,8 +593,7 @@
 lots of time can be saved when pushing to complex Gits.
 
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 
 * Do not persist default project state in `project.config`.
@@ -644,12 +617,10 @@
 
 * Fix JdbcSQLException when numbers are read from cache.
 
-Web UI
-~~~~~~
+=== Web UI
 
 
-Global
-^^^^^^
+==== Global
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1574[Issue 1574]:
@@ -669,8 +640,7 @@
 If a user voted '-1', and then another user voted '+1' for a label, the
 label was shown as a red '1' in the change list instead of red '-1'.
 
-Change Screens
-^^^^^^^^^^^^^^
+==== Change Screens
 
 
 * Default review comment visibility is changed to expand all recent.
@@ -694,8 +664,7 @@
 
 * Prevent duplicate permitted_labels from being shown in labels list.
 
-Diff Screens
-^^^^^^^^^^^^
+==== Diff Screens
 
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1233[Issue 1233]:
@@ -708,8 +677,7 @@
 that comment was not shown if there was no code changed between
 the two patch sets
 
-Project Screens
-^^^^^^^^^^^^^^^
+==== Project Screens
 
 
 * Only enable the delete branch button when branches are selected.
@@ -717,16 +685,14 @@
 * Disable the delete branch button while branch deletion requests are
 still being processed.
 
-User Profile Screens
-^^^^^^^^^^^^^^^^^^^^
+==== User Profile Screens
 
 
 * The preferred email address field is shown as empty if the user has no
 preferred email address.
 
 
-REST API
-~~~~~~~~
+=== REST API
 
 
 * Support raw input also in POST requests.
@@ -735,8 +701,7 @@
 
 * Return all revisions when `o=ALL_REVISIONS` is set on `/changes/`.
 
-ssh
-~~~
+=== ssh
 
 
 * The `--force-message` option is removed from the
@@ -762,11 +727,9 @@
 * Improve the error message when rejecting upload for review to a read-only project.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
-Global
-^^^^^^
+==== Global
 
 * Better error message when a Javascript plugin cannot be loaded.
 
@@ -784,8 +747,7 @@
 * Make plugin servlet's context path authorization aware.
 
 
-Review Notes
-^^^^^^^^^^^^
+==== Review Notes
 
 * Do not try to create review notes for ref deletion events.
 
@@ -800,22 +762,19 @@
 
 * Correct documentation of the export command.
 
-Emails
-~~~~~~
+=== Emails
 
 * Email notifications are sent for new changes created via actions in the
 Web UI such as cherry-picking or reverting a change.
 
 
-Tools
-~~~~~
+=== Tools
 
 
 * git-exproll.sh: return non-zero on errors
 
 
-Documentation
--------------
+== Documentation
 
 
 * The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/index.html[
@@ -832,8 +791,7 @@
 Documentation of the query operator is fixed.
 
 
-Upgrades
---------
+== Upgrades
 
 * Update JGit to 3.1.0.201310021548-r
 * Update gwtorm to 1.7
diff --git a/ReleaseNotes/ReleaseNotes-2.9.1.txt b/ReleaseNotes/ReleaseNotes-2.9.1.txt
index 3377df4..b584193 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.1.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.9.1
-==============================
+= Release notes for Gerrit 2.9.1
 
 There are no schema changes from link:ReleaseNotes-2.9.html[2.9].
 
@@ -13,8 +12,7 @@
 startup failure described in
 link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
 
-Bug Fixes
----------
+== Bug Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2801[Issue 2801]:
 Set default for review SSH command to `notify=ALL`.
diff --git a/ReleaseNotes/ReleaseNotes-2.9.2.txt b/ReleaseNotes/ReleaseNotes-2.9.2.txt
index 4e5de01..ec5b77e 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.2.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.9.2
-==============================
+= Release notes for Gerrit 2.9.2
 
 Download:
 link:https://www.gerritcodereview.com/download/gerrit-2.9.2.war[
 https://www.gerritcodereview.com/download/gerrit-2.9.2.war]
 
-Important Notes
----------------
+== Important Notes
 
 *WARNING:* There are no schema changes from
 link:ReleaseNotes-2.9.1.html[2.9.1], but when upgrading from an existing site
@@ -28,11 +26,9 @@
 startup failure described in
 link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
 
-Bug Fixes
----------
+== Bug Fixes
 
-ssh
-~~~
+=== ssh
 
 * Update SSHD to 0.13.0.
 +
@@ -41,8 +37,7 @@
 +
 Also update SSHD Mina to 2.0.8 and Bouncycastle to 1.51.
 
-Database
-~~~~~~~~
+=== Database
 
 * Update gwtorm to 1.14.
 +
@@ -54,8 +49,7 @@
 were initialized with Gerrit version 2.6 or later, the primary key column
 order will be fixed during initialization when upgrading to 2.9.2.
 
-Secondary Index
-~~~~~~~~~~~~~~~
+=== Secondary Index
 
 * Fix "400 cannot create query for index" error in "Conflicts With" list.
 +
@@ -79,8 +73,7 @@
 the database. If a user clicks on search result from a stale change, they will
 get a 404 page and the change will be removed from the index.
 
-Change Screen
-~~~~~~~~~~~~~
+=== Change Screen
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2964[Issue 2964]:
 Fix comment box font colors of dark CodeMirror themes.
@@ -107,8 +100,7 @@
 
 * Remove 'send email' checkbox from reply box on change screen.
 
-Plugins
-~~~~~~~
+=== Plugins
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=543[Issue 543]
 Replication plugin: Prevent creating repos on extra servers.
@@ -120,8 +112,7 @@
 By ensuring the authGroup can see the project first, the repository is
 not created if it's not needed.
 
-Security
-~~~~~~~~
+=== Security
 
 * Do not throw away bytes from the CNSPRG when generating HTTP passwords.
 +
@@ -141,8 +132,7 @@
 with BLOCK or DENY action were considered as project owners.
 
 
-Miscellaneous Fixes
-~~~~~~~~~~~~~~~~~~~
+=== Miscellaneous Fixes
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2911[Issue 2911]:
 Fix Null Pointer Exception after a MergeValidationListener throws
diff --git a/ReleaseNotes/ReleaseNotes-2.9.3.txt b/ReleaseNotes/ReleaseNotes-2.9.3.txt
index e6c8573..1b732cb 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.3.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.9.3
-==============================
+= Release notes for Gerrit 2.9.3
 
 Download:
 link:https://www.gerritcodereview.com/download/gerrit-2.9.3.war[
 https://www.gerritcodereview.com/download/gerrit-2.9.3.war]
 
-Important Notes
----------------
+== Important Notes
 
 *WARNING:* There are no schema changes from
 link:ReleaseNotes-2.9.2.html[2.9.2], but when upgrading from an existing site
@@ -28,8 +26,7 @@
 startup failure described in
 link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
 
-Bug Fixes
----------
+== Bug Fixes
 
 *Downgrade SSHD to 0.9.0-4-g5967cfd*
 
diff --git a/ReleaseNotes/ReleaseNotes-2.9.4.txt b/ReleaseNotes/ReleaseNotes-2.9.4.txt
index 5063489..e2ad6ac 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.4.txt
@@ -1,12 +1,10 @@
-Release notes for Gerrit 2.9.4
-==============================
+= Release notes for Gerrit 2.9.4
 
 Download:
 link:https://www.gerritcodereview.com/download/gerrit-2.9.4.war[
 https://www.gerritcodereview.com/download/gerrit-2.9.4.war]
 
-Important Notes
----------------
+== Important Notes
 
 *WARNING:* There are no schema changes from
 link:ReleaseNotes-2.9.3.html[2.9.3], but when upgrading from an existing site
@@ -28,8 +26,7 @@
 startup failure described in
 link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
 
-Bug Fixes
----------
+== Bug Fixes
 
 * Update JGit to 3.4.2.201412180340-r
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.9.txt b/ReleaseNotes/ReleaseNotes-2.9.txt
index 3387f98..c026914 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.9
-============================
+= Release notes for Gerrit 2.9
 
 
 Gerrit 2.9 is now available:
@@ -20,8 +19,7 @@
 link:ReleaseNotes-2.8.6.1.html[Gerrit 2.8.6.1].
 These bug fixes are *not* listed in these release notes.
 
-Important Notes
----------------
+== Important Notes
 
 
 *WARNING:* This release contains schema changes.  To upgrade:
@@ -98,8 +96,7 @@
 the site's `plugins` folder.
 
 
-Release Highlights
-------------------
+== Release Highlights
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=2065[Issue 2065]:
@@ -116,22 +113,18 @@
 to the old change screen.
 
 
-New Features
-------------
+== New Features
 
 
-Web UI
-~~~~~~
+=== Web UI
 
 
-Global
-^^^^^^
+==== Global
 
 * Project links by default link to the project dashboard.
 
 
-New Change Screen
-^^^^^^^^^^^^^^^^^
+==== New Change Screen
 
 
 * The new change screen is now the default change screen.
@@ -197,8 +190,7 @@
 New copy-to-clipboard button for commit ID.
 
 
-New Side-by-Side Diff Screen
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+==== New Side-by-Side Diff Screen
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=348[Issue 348]:
 The lines of a patch file are linkable.
@@ -220,8 +212,7 @@
 The full file path is shown.
 
 
-Change List / Dashboards
-^^^^^^^^^^^^^^^^^^^^^^^^
+==== Change List / Dashboards
 
 * The `Status` column shows `Merge Conflict` for changes that are not
 mergeable.
@@ -240,8 +231,7 @@
 without the `limit` operator.
 
 
-Project Screens
-^^^^^^^^^^^^^^^
+==== Project Screens
 
 * The general project screen provides a copyable clone command that
 automatically installs the `commit-msg` hook.
@@ -263,15 +253,13 @@
 Run Garbage Collection] global capability.
 
 
-User Preferences
-^^^^^^^^^^^^^^^^
+==== User Preferences
 
 * Users can choose the UK date format to render dates and timestamps in
 the UI.
 
 
-Secondary Index
-~~~~~~~~~~~~~~~
+=== Secondary Index
 
 * Support for query via the SQL index is removed. The usage of
 a secondary index is now mandatory.
@@ -280,8 +268,7 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/pgm-reindex.html[
 reindex] program.
 
-ssh
-~~~
+=== ssh
 
 * New `--notify` option on the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/cmd-review.html[
@@ -305,12 +292,10 @@
 New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/cmd-create-branch.html[
 create-branch] command.
 
-REST API
-~~~~~~~~
+=== REST API
 
 
-Changes
-^^^^^^^
+==== Changes
 
 
 [[sortkey-deprecation]]
@@ -325,22 +310,19 @@
 Queries with sortkeys are still supported against old index versions, to enable
 online reindexing while clients have an older JS version.
 
-Projects
-^^^^^^^^
+==== Projects
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/rest-api-projects.html#get-content[
 Get content of a file from HEAD of a branch].
 
-Documentation
-^^^^^^^^^^^^^
+==== Documentation
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/rest-api-documentation.html#search-documentation.html[
 Search documentation].
 
-Access Rights
-~~~~~~~~~~~~~
+=== Access Rights
 
 
 * New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/access-control.html#capability_viewAllAccounts[
@@ -374,8 +356,7 @@
 Gerrit but not in LDAP are authenticated with their HTTP password from
 the Gerrit database.
 
-Search
-~~~~~~
+=== Search
 
 * New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/user-search.html#mergeable[
 is:mergeable] search operator.
@@ -411,8 +392,7 @@
 ** `p` = `project`
 ** `f` = `file`
 
-Daemon
-~~~~~~
+=== Daemon
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/dev-inspector.html[
@@ -421,8 +401,7 @@
 New `-s` option is added to the Daemon to start an interactive Jython shell for inspection and
 troubleshooting of live data of the Gerrit instance.
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
 
 * The documentation is now
@@ -442,8 +421,7 @@
 Newly structured documentation index].
 
 
-Configuration
-~~~~~~~~~~~~~
+=== Configuration
 
 * New init step for installing the `Verified` label.
 
@@ -463,8 +441,7 @@
 Allow the text of the "Report Bug" link to be configured.
 
 
-Misc
-~~~~
+=== Misc
 
 * The removal of reviewers and their votes is recorded as a change
 message.
@@ -478,8 +455,7 @@
 * Stable CSS class names.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
 
 * Plugin API to invoke the REST API.
@@ -499,8 +475,7 @@
 Remote plugin administration is by default disabled].
 
 
-Extension Points
-^^^^^^^^^^^^^^^^
+==== Extension Points
 
 
 * Extension point to provide a "Message Of The Day".
@@ -531,8 +506,7 @@
 DataSource Interception].
 
 
-JavaScript Plugins
-^^^^^^^^^^^^^^^^^^
+==== JavaScript Plugins
 
 
 * link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.9/js-api.html#self_on[
@@ -545,19 +519,16 @@
 insert arbitrary HTML fragments from plugins.
 
 
-Bug Fixes
----------
+== Bug Fixes
 
 
-Access Rights
-~~~~~~~~~~~~~
+=== Access Rights
 
 
 * Fix possibility to overcome BLOCK permissions.
 
 
-Web UI
-~~~~~~
+=== Web UI
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=2652[Issue 2652]:
@@ -622,8 +593,7 @@
 Fix copying from copyable label in Safari.
 
 
-Secondary Index
-~~~~~~~~~~~~~~~
+=== Secondary Index
 
 * Fix Online Reindexing.
 
@@ -640,16 +610,14 @@
 Reindex change after updating commit message.
 
 
-REST
-~~~~
+=== REST
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=2568[Issue 2568]:
 Update description file during `PUT /projects/{name}/config`.
 
 
-SSH
-~~~
+=== SSH
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=2516[Issue 2516]:
@@ -659,8 +627,7 @@
 Clarify for review command when `--verified` can be used.
 
 
-Plugins
-~~~~~~~
+=== Plugins
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=2551[Issue 2551]:
@@ -670,16 +637,14 @@
 Respect servlet context path in URL for top menu items.
 
 
-Other
-~~~~~
+=== Other
 
 
 * link:http://code.google.com/p/gerrit/issues/detail?id=2382[Issue 2382]:
 Clean left over data migration after removal of TrackingIds table.
 
 
-Upgrades
---------
+== Upgrades
 
 * Update JGit to 3.4.0.201405051725-m7
 +
@@ -704,11 +669,9 @@
 * Update GWT to 2.6.0
 
 
-Plugins
--------
+== Plugins
 
-Replication
-~~~~~~~~~~~
+=== Replication
 
 * Default push refSpec is changed to `refs/*:refs/*` (non-forced push).
 +
@@ -728,8 +691,7 @@
 * Configuration changes can be detected and replication is
 automatically restarted.
 
-Issue Tracker System plugins
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Issue Tracker System plugins
 
 *WARNING:* The `hooks-*` plugins (`plugins/hooks-bugzilla`,
 `plugins/hooks-jira` and `plugins/hooks-rtc`) are deprecated with
diff --git a/ReleaseNotes/asciidoc.conf b/ReleaseNotes/asciidoc.conf
deleted file mode 100644
index 527f58f..0000000
--- a/ReleaseNotes/asciidoc.conf
+++ /dev/null
@@ -1,19 +0,0 @@
-[attributes]
-asterisk=&#42;
-plus=&#43;
-caret=&#94;
-startsb=&#91;
-endsb=&#93;
-tilde=&#126;
-max-width=55em
-
-[specialsections]
-GERRIT=gerrituplink
-
-[gerrituplink]
-<hr style="
-  height: 2px;
-  color: silver;
-  margin-top: 1.2em;
-  margin-bottom: 0.5em;
-">
diff --git a/ReleaseNotes/config.defs b/ReleaseNotes/config.defs
new file mode 100644
index 0000000..86b7603
--- /dev/null
+++ b/ReleaseNotes/config.defs
@@ -0,0 +1,14 @@
+def release_notes_attributes():
+  return [
+    'toc',
+    'newline="\\n"',
+    'asterisk="&#42;"',
+    'plus="&#43;"',
+    'caret="&#94;"',
+    'startsb="&#91;"',
+    'endsb="&#93;"',
+    'tilde="&#126;"',
+    'last-update-label!',
+    'stylesheet=DEFAULT',
+    'linkcss=true',
+  ]
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 4cfb15a..bba07dc 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,9 +1,13 @@
-Gerrit Code Review - Release Notes
-==================================
+= Gerrit Code Review - Release Notes
 
-[[2_12]]
-Version 2.12.x
---------------
+[[s2_13]]
+== Version 2.13.x
+* link:ReleaseNotes-2.13.2.html[2.13.2]
+* link:ReleaseNotes-2.13.1.html[2.13.1]
+* link:ReleaseNotes-2.13.html[2.13]
+
+[[s2_12]]
+== Version 2.12.x
 * link:ReleaseNotes-2.12.5.html[2.12.5]
 * link:ReleaseNotes-2.12.4.html[2.12.4]
 * link:ReleaseNotes-2.12.3.html[2.12.3]
@@ -11,9 +15,8 @@
 * link:ReleaseNotes-2.12.1.html[2.12.1]
 * link:ReleaseNotes-2.12.html[2.12]
 
-[[2_11]]
-Version 2.11.x
---------------
+[[s2_11]]
+== Version 2.11.x
 * link:ReleaseNotes-2.11.10.html[2.11.10]
 * link:ReleaseNotes-2.11.9.html[2.11.9]
 * link:ReleaseNotes-2.11.8.html[2.11.8]
@@ -26,9 +29,8 @@
 * link:ReleaseNotes-2.11.1.html[2.11.1]
 * link:ReleaseNotes-2.11.html[2.11]
 
-[[2_10]]
-Version 2.10.x
---------------
+[[s2_10]]
+== Version 2.10.x
 * link:ReleaseNotes-2.10.8.html[2.10.8]
 * link:ReleaseNotes-2.10.7.html[2.10.7]
 * link:ReleaseNotes-2.10.6.html[2.10.6]
@@ -40,9 +42,8 @@
 * link:ReleaseNotes-2.10.1.html[2.10.1]
 * link:ReleaseNotes-2.10.html[2.10]
 
-[[2_9]]
-Version 2.9.x
--------------
+[[s2_9]]
+== Version 2.9.x
 * link:ReleaseNotes-2.9.5.html[2.9.5]
 * link:ReleaseNotes-2.9.4.html[2.9.4]
 * link:ReleaseNotes-2.9.3.html[2.9.3]
@@ -50,9 +51,8 @@
 * link:ReleaseNotes-2.9.1.html[2.9.1]
 * link:ReleaseNotes-2.9.html[2.9]
 
-[[2_8]]
-Version 2.8.x
--------------
+[[s2_8]]
+== Version 2.8.x
 * link:ReleaseNotes-2.8.6.1.html[2.8.6.1]
 * link:ReleaseNotes-2.8.6.html[2.8.6]
 * link:ReleaseNotes-2.8.5.html[2.8.5]
@@ -62,20 +62,17 @@
 * link:ReleaseNotes-2.8.1.html[2.8.1]
 * link:ReleaseNotes-2.8.html[2.8]
 
-[[2_7]]
-Version 2.7.x
--------------
+[[s2_7]]
+== Version 2.7.x
 * link:ReleaseNotes-2.7.html[2.7]
 
-[[2_6]]
-Version 2.6.x
--------------
+[[s2_6]]
+== Version 2.6.x
 * link:ReleaseNotes-2.6.1.html[2.6.1]
 * link:ReleaseNotes-2.6.html[2.6]
 
-[[2_5]]
-Version 2.5.x
--------------
+[[s2_5]]
+== Version 2.5.x
 * link:ReleaseNotes-2.5.6.html[2.5.6]
 * link:ReleaseNotes-2.5.5.html[2.5.5]
 * link:ReleaseNotes-2.5.4.html[2.5.4]
@@ -84,33 +81,29 @@
 * link:ReleaseNotes-2.5.1.html[2.5.1]
 * link:ReleaseNotes-2.5.html[2.5]
 
-[[2_4]]
-Version 2.4.x
--------------
+[[s2_4]]
+== Version 2.4.x
 * link:ReleaseNotes-2.4.4.html[2.4.4]
 * link:ReleaseNotes-2.4.3.html[2.4.3]
 * link:ReleaseNotes-2.4.2.html[2.4.2]
 * link:ReleaseNotes-2.4.1.html[2.4.1]
 * link:ReleaseNotes-2.4.html[2.4]
 
-[[2_3]]
-Version 2.3.x
--------------
+[[s2_3]]
+== Version 2.3.x
 * link:ReleaseNotes-2.3.1.html[2.3.1]
 * link:ReleaseNotes-2.3.html[2.3]
 
-[[2_2]]
-Version 2.2.x
--------------
+[[s2_2]]
+== Version 2.2.x
 * link:ReleaseNotes-2.2.2.2.html[2.2.2.2]
 * link:ReleaseNotes-2.2.2.1.html[2.2.2.1]
 * link:ReleaseNotes-2.2.2.html[2.2.2]
 * link:ReleaseNotes-2.2.1.html[2.2.1]
 * link:ReleaseNotes-2.2.0.html[2.2.0]
 
-[[2_1]]
-Version 2.1.x
--------------
+[[s2_1]]
+== Version 2.1.x
 * link:ReleaseNotes-2.1.10.html[2.1.10]
 * link:ReleaseNotes-2.1.9.html[2.1.9]
 * link:ReleaseNotes-2.1.8.html[2.1.8]
@@ -131,9 +124,8 @@
 * link:ReleaseNotes-2.1.1.html[2.1.1]
 * link:ReleaseNotes-2.1.html[2.1]
 
-[[2_0]]
-Version 2.0.x
--------------
+[[s2_0]]
+== Version 2.0.x
 * link:ReleaseNotes-2.0.24.html[2.0.24.2]
 * link:ReleaseNotes-2.0.24.html[2.0.24.1]
 * link:ReleaseNotes-2.0.24.html[2.0.24]
diff --git a/VERSION b/VERSION
index 1bcaa4b..8bcf86b 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.12.9'
+GERRIT_VERSION = '2.13.14'
diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..e9ad5e1
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,729 @@
+ANTLR_VERS = '3.5.2'
+
+maven_jar(
+  name = 'java_runtime',
+  artifact = 'org.antlr:antlr-runtime:' + ANTLR_VERS,
+  sha1 = 'cd9cd41361c155f3af0f653009dcecb08d8b4afd',
+)
+
+maven_jar(
+  name = 'stringtemplate',
+  artifact = 'org.antlr:stringtemplate:4.0.2',
+  sha1 = 'e28e09e2d44d60506a7bcb004d6c23ff35c6ac08',
+)
+
+maven_jar(
+  name = 'org_antlr',
+  artifact = 'org.antlr:antlr:' + ANTLR_VERS,
+  sha1 = 'c4a65c950bfc3e7d04309c515b2177c00baf7764',
+)
+
+maven_jar(
+  name = 'antlr27',
+  artifact = 'antlr:antlr:2.7.7',
+  sha1 = '83cd2cd674a217ade95a4bb83a8a14f351f48bd0',
+)
+
+GUICE_VERS = '4.0'
+
+maven_jar(
+  name = 'guice_library',
+  artifact = 'com.google.inject:guice:' + GUICE_VERS,
+  sha1 = '0f990a43d3725781b6db7cd0acf0a8b62dfd1649',
+)
+
+maven_jar(
+  name = 'guice_assistedinject',
+  artifact = 'com.google.inject.extensions:guice-assistedinject:' + GUICE_VERS,
+  sha1 = '8fa6431da1a2187817e3e52e967535899e2e46ca',
+)
+
+maven_jar(
+  name = 'guice_servlet',
+  artifact = 'com.google.inject.extensions:guice-servlet:' + GUICE_VERS,
+  sha1 = '4503da866f4c402b5090579b40c1c4aaefabb164',
+)
+
+maven_jar(
+  name = 'aopalliance',
+  artifact = 'aopalliance:aopalliance:1.0',
+  sha1 = '0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8',
+)
+
+maven_jar(
+  name = 'javax_inject',
+  artifact = 'javax.inject:javax.inject:1',
+  sha1 = '6975da39a7040257bd51d21a231b76c915872d38',
+)
+
+maven_jar(
+  name = 'servlet_api_3_1',
+  artifact = 'org.apache.tomcat:tomcat-servlet-api:8.0.24',
+  sha1 = '5d9e2e895e3111622720157d0aa540066d5fce3a',
+)
+
+GWT_VERS = '2.8.0'
+
+maven_jar(
+  name = 'user',
+  artifact = 'com.google.gwt:gwt-user:' + GWT_VERS,
+  sha1 = '518579870499e15531f454f35dca0772d7fa31f7',
+)
+
+maven_jar(
+  name = 'dev',
+  artifact = 'com.google.gwt:gwt-dev:' + GWT_VERS,
+  sha1 = 'f160a61272c5ebe805cd2d3d3256ed3ecf14893f',
+)
+
+maven_jar(
+  name = 'javax_validation',
+  artifact = 'javax.validation:validation-api:1.0.0.GA',
+  sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e',
+)
+
+maven_jar(
+  name = 'jsinterop_annotations',
+  artifact = 'com.google.jsinterop:jsinterop-annotations:1.0.0',
+  sha1 = '23c3a3c060ffe4817e67673cc8294e154b0a4a95',
+)
+
+maven_jar(
+  name = 'ant',
+  artifact = 'ant:ant:1.6.5',
+  sha1 = '7d18faf23df1a5c3a43613952e0e8a182664564b',
+)
+
+maven_jar(
+  name = 'colt',
+  artifact = 'colt:colt:1.2.0',
+  sha1 = '0abc984f3adc760684d49e0f11ddf167ba516d4f',
+)
+
+maven_jar(
+  name = 'tapestry',
+  artifact = 'tapestry:tapestry:4.0.2',
+  sha1 = 'e855a807425d522e958cbce8697f21e9d679b1f7',
+)
+
+maven_jar(
+  name = 'w3c_css_sac',
+  artifact = 'org.w3c.css:sac:1.3',
+  sha1 = 'cdb2dcb4e22b83d6b32b93095f644c3462739e82',
+)
+
+JGIT_VERS = '4.4.1.201607150455-r.105-g81ba2be'
+
+maven_jar(
+  name = 'jgit',
+  repository = 'http://gerrit-maven.storage.googleapis.com/',
+  artifact = 'org.eclipse.jgit:org.eclipse.jgit:' + JGIT_VERS,
+  sha1 = 'c07c9c66da7983095a40945c0bfab211a473c4c5',
+)
+
+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',
+)
+
+# 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',
+  url = 'http://gerrit-maven.storage.googleapis.com/org/eclipse/jgit/org.eclipse.jgit/' +
+      '%s/org.eclipse.jgit-%s-sources.jar' % (JGIT_VERS, JGIT_VERS),
+)
+
+maven_jar(
+  name = 'ewah',
+  artifact = 'com.googlecode.javaewah:JavaEWAH:0.7.9',
+  sha1 = 'eceaf316a8faf0e794296ebe158ae110c7d72a5a',
+)
+
+maven_jar(
+  name = 'jgit_archive',
+  repository = 'http://gerrit-maven.storage.googleapis.com/',
+  artifact = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + JGIT_VERS,
+  sha1 = 'fc3bc40e070c54198a046fcd3a1f7cac47163961',
+)
+
+maven_jar(
+  name = 'jgit_junit',
+  repository = 'http://gerrit-maven.storage.googleapis.com/',
+  artifact = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + JGIT_VERS,
+  sha1 = 'b4565ee84a6e1d0952010282b9fcf705ac6171a7',
+)
+
+maven_jar(
+  name = 'gwtjsonrpc',
+  artifact = 'com.google.gerrit:gwtjsonrpc:1.10',
+  sha1 = '25adea6ef102b761993688e80dfc7203e0f5edf0',
+)
+
+http_jar(
+  name = 'gwtjsonrpc_src',
+  sha256 = '009c4c7574eaddf49d2c72dd015cfbd5b495fbeea4c3958c2ec548af2c186733',
+  url = 'http://repo.maven.apache.org/maven2/com/google/gerrit/gwtjsonrpc/1.10/gwtjsonrpc-1.10-sources.jar',
+)
+
+maven_jar(
+  name = 'gson',
+  artifact = 'com.google.code.gson:gson:2.6.2',
+  sha1 = 'f1bc476cc167b18e66c297df599b2377131a8947',
+)
+
+maven_jar(
+  name = 'gwtorm_client',
+  artifact = 'com.google.gerrit:gwtorm:1.15',
+  sha1 = '26a2459f543ed78977535f92e379dc0d6cdde8bb',
+)
+
+http_jar(
+  name = 'gwtorm_client_src',
+  sha256 = 'e0cf9382ed8c3cd1f0884ab77dabe634a04546676c4960d8b4c4b64a20132ef6',
+  url = 'http://repo.maven.apache.org/maven2/com/google/gerrit/gwtorm/1.15/gwtorm-1.15-sources.jar',
+)
+
+maven_jar(
+  name = 'protobuf',
+  artifact = 'com.google.protobuf:protobuf-java:2.5.0',
+  sha1 = 'a10732c76bfacdbd633a7eb0f7968b1059a65dfa',
+)
+
+maven_jar(
+  name = 'joda_time',
+  artifact = 'joda-time:joda-time:2.8',
+  sha1 = '9f2785d7184b97d005a44241ccaf980f43b9ccdb',
+)
+
+maven_jar(
+  name = 'joda_convert',
+  artifact = 'org.joda:joda-convert:1.2',
+  sha1 = '35ec554f0cd00c956cc69051514d9488b1374dec',
+)
+
+maven_jar(
+  name = 'guava',
+  artifact = 'com.google.guava:guava:19.0',
+  sha1 = '6ce200f6b23222af3d8abb6b6459e6c44f4bb0e9',
+)
+
+maven_jar(
+  name = 'velocity',
+  artifact = 'org.apache.velocity:velocity:1.7',
+  sha1 = '2ceb567b8f3f21118ecdec129fe1271dbc09aa7a',
+)
+
+maven_jar(
+  name = 'jsch',
+  artifact = 'com.jcraft:jsch:0.1.53',
+  sha1 = '658b682d5c817b27ae795637dfec047c63d29935',
+)
+
+maven_jar(
+  name = 'juniversalchardet',
+  artifact = 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3',
+  sha1 = 'cd49678784c46aa8789c060538e0154013bb421b',
+)
+
+SLF4J_VERS = '1.7.7'
+
+maven_jar(
+  name = 'log_api',
+  artifact = 'org.slf4j:slf4j-api:' + SLF4J_VERS,
+  sha1 = '2b8019b6249bb05d81d3a3094e468753e2b21311',
+)
+
+maven_jar(
+  name = 'log_nop',
+  artifact = 'org.slf4j:slf4j-nop:' + SLF4J_VERS,
+  sha1 = '6cca9a3b999ff28b7a35ca762b3197cd7e4c2ad1',
+)
+
+maven_jar(
+  name = 'impl_log4j',
+  artifact = 'org.slf4j:slf4j-log4j12:' + SLF4J_VERS,
+  sha1 = '58f588119ffd1702c77ccab6acb54bfb41bed8bd',
+)
+
+maven_jar(
+  name = 'jcl_over_slf4j',
+  artifact = 'org.slf4j:jcl-over-slf4j:' + SLF4J_VERS,
+  sha1 = '56003dcd0a31deea6391b9e2ef2f2dc90b205a92',
+)
+
+maven_jar(
+  name = 'log4j',
+  artifact = 'log4j:log4j:1.2.17',
+  sha1 = '5af35056b4d257e4b64b9e8069c0746e8b08629f',
+)
+
+maven_jar(
+  name = 'jsonevent_layout',
+  artifact = 'net.logstash.log4j:jsonevent-layout:1.7',
+  sha1 = '507713504f0ddb75ba512f62763519c43cf46fde',
+)
+
+maven_jar(
+  name = 'json_smart',
+  artifact = 'net.minidev:json-smart:1.1.1',
+  sha1 = '24a2f903d25e004de30ac602c5b47f2d4e420a59',
+)
+
+maven_jar(
+  name = 'args4j',
+  artifact = 'args4j:args4j:2.0.26',
+  sha1 = '01ebb18ebb3b379a74207d5af4ea7c8338ebd78b',
+)
+
+maven_jar(
+  name = 'commons_codec',
+  artifact = 'commons-codec:commons-codec:1.4',
+  sha1 = '4216af16d38465bbab0f3dff8efa14204f7a399a',
+)
+
+maven_jar(
+  name = 'commons_collections',
+  artifact = 'commons-collections:commons-collections:3.2.2',
+  sha1 = '8ad72fe39fa8c91eaaf12aadb21e0c3661fe26d5',
+)
+
+maven_jar(
+  name = 'commons_compress',
+  artifact = 'org.apache.commons:commons-compress:1.7',
+  sha1 = 'ab365c96ee9bc88adcc6fa40d185c8e15a31410d',
+)
+
+maven_jar(
+  name = 'commons_lang',
+  artifact = 'commons-lang:commons-lang:2.6',
+  sha1 = '0ce1edb914c94ebc388f086c6827e8bdeec71ac2',
+)
+
+maven_jar(
+  name = 'commons_dbcp',
+  artifact = 'commons-dbcp:commons-dbcp:1.4',
+  sha1 = '30be73c965cc990b153a100aaaaafcf239f82d39',
+)
+
+maven_jar(
+  name = 'commons_pool',
+  artifact = 'commons-pool:commons-pool:1.5.5',
+  sha1 = '7d8ffbdc47aa0c5a8afe5dc2aaf512f369f1d19b',
+)
+
+maven_jar(
+  name = 'commons_net',
+  artifact = 'commons-net:commons-net:2.2',
+  sha1 = '07993c12f63c78378f8c90de4bc2ee62daa7ca3a',
+)
+
+maven_jar(
+  name = 'commons_oro',
+  artifact = 'oro:oro:2.0.8',
+  sha1 = '5592374f834645c4ae250f4c9fbb314c9369d698',
+)
+
+maven_jar(
+  name = 'commons_validator',
+  artifact = 'commons-validator:commons-validator:1.5.1',
+  sha1 = '86d05a46e8f064b300657f751b5a98c62807e2a0',
+)
+
+maven_jar(
+  name = 'automaton',
+  artifact = 'dk.brics.automaton:automaton:1.11-8',
+  sha1 = '6ebfa65eb431ff4b715a23be7a750cbc4cc96d0f',
+)
+
+maven_jar(
+  name = 'pegdown',
+  artifact = 'org.pegdown:pegdown:1.4.2',
+  sha1 = 'd96db502ed832df867ff5d918f05b51ba3879ea7',
+)
+
+maven_jar(
+  name = 'grappa',
+  artifact = 'com.github.parboiled1:grappa:1.0.4',
+  sha1 = 'ad4b44b9c305dad7aa1e680d4b5c8eec9c4fd6f5',
+)
+
+maven_jar(
+  name = 'jitescript',
+  artifact = 'me.qmx.jitescript:jitescript:0.4.0',
+  sha1 = '2e35862b0435c1b027a21f3d6eecbe50e6e08d54',
+)
+
+OW2_VERS = '5.0.3'
+
+maven_jar(
+  name = 'ow2_asm',
+  artifact = 'org.ow2.asm:asm:' + OW2_VERS,
+  sha1 = 'dcc2193db20e19e1feca8b1240dbbc4e190824fa',
+)
+
+maven_jar(
+  name = 'ow2_asm_analysis',
+  artifact = 'org.ow2.asm:asm-analysis:' + OW2_VERS,
+  sha1 = 'c7126aded0e8e13fed5f913559a0dd7b770a10f3',
+)
+
+maven_jar(
+  name = 'ow2_asm_commons',
+  artifact = 'org.ow2.asm:asm-commons:' + OW2_VERS,
+  sha1 = 'a7111830132c7f87d08fe48cb0ca07630f8cb91c',
+)
+
+maven_jar(
+  name = 'ow2_asm_tree',
+  artifact = 'org.ow2.asm:asm-tree:' + OW2_VERS,
+  sha1 = '287749b48ba7162fb67c93a026d690b29f410bed',
+)
+
+maven_jar(
+  name = 'ow2_asm_util',
+  artifact = 'org.ow2.asm:asm-util:' + OW2_VERS,
+  sha1 = '1512e5571325854b05fb1efce1db75fcced54389',
+)
+
+maven_jar(
+  name = 'auto_value',
+  artifact = 'com.google.auto.value:auto-value:1.2',
+  sha1 = '6873fed014fe1de1051aae2af68ba266d2934471',
+)
+
+maven_jar(
+  name = 'tukaani_xz',
+  artifact = 'org.tukaani:xz:1.4',
+  sha1 = '18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3',
+)
+
+LUCENE_VERS = '5.4.1'
+
+maven_jar(
+  name = 'lucene_core',
+  artifact = 'org.apache.lucene:lucene-core:' + LUCENE_VERS,
+  sha1 = 'c52b2088e2c30dfd95fd296ab6fb9cf8de9855ab',
+)
+
+maven_jar(
+  name = 'lucene_analyzers_common',
+  artifact = 'org.apache.lucene:lucene-analyzers-common:' + LUCENE_VERS,
+  sha1 = 'c2aa2c4e00eb9cdeb5ac00dc0495e70c441f681e',
+)
+
+maven_jar(
+  name = 'backward_codecs',
+  artifact = 'org.apache.lucene:lucene-backward-codecs:' + LUCENE_VERS,
+  sha1 = '5273da96380dfab302ad06c27fe58100db4c4e2f',
+)
+
+maven_jar(
+  name = 'lucene_misc',
+  artifact = 'org.apache.lucene:lucene-misc:' + LUCENE_VERS,
+  sha1 = '95f433b9d7dd470cc0aa5076e0f233907745674b',
+)
+
+maven_jar(
+  name = 'lucene_queryparser',
+  artifact = 'org.apache.lucene:lucene-queryparser:' + LUCENE_VERS,
+  sha1 = 'dccd5279bfa656dec21af444a7a66820eb1cd618',
+)
+
+maven_jar(
+  name = 'mime_util',
+  artifact = 'eu.medsea.mimeutil:mime-util:2.1.3',
+  sha1 = '0c9cfae15c74f62491d4f28def0dff1dabe52a47',
+)
+
+PROLOG_VERS = '1.4.1'
+
+maven_jar(
+  name = 'prolog_runtime',
+  repository = 'http://gerrit-maven.storage.googleapis.com/',
+  artifact = 'com.googlecode.prolog-cafe:prolog-runtime:' + PROLOG_VERS,
+  sha1 = 'c5d9f92e49c485969dcd424dfc0c08125b5f8246',
+)
+
+maven_jar(
+  name = 'prolog_compiler',
+  repository = 'http://gerrit-maven.storage.googleapis.com/',
+  artifact = 'com.googlecode.prolog-cafe:prolog-compiler:' + PROLOG_VERS,
+  sha1 = 'ac24044c6ec166fdcb352b78b80d187ead3eff41',
+)
+
+maven_jar(
+  name = 'prolog_io',
+  repository = 'http://gerrit-maven.storage.googleapis.com/',
+  artifact = 'com.googlecode.prolog-cafe:prolog-io:' + PROLOG_VERS,
+  sha1 = 'b072426a4b1b8af5e914026d298ee0358a8bb5aa',
+)
+
+maven_jar(
+  name = 'cafeteria',
+  repository = 'http://gerrit-maven.storage.googleapis.com/',
+  artifact = 'com.googlecode.prolog-cafe:prolog-cafeteria:' + PROLOG_VERS,
+  sha1 = '8cbc3b0c19e7167c42d3f11667b21cb21ddec641',
+)
+
+maven_jar(
+  name = 'guava_retrying',
+  artifact = 'com.github.rholder:guava-retrying:2.0.0',
+  sha1 = '974bc0a04a11cc4806f7c20a34703bd23c34e7f4',
+)
+
+maven_jar(
+  name = 'jsr305',
+  artifact = 'com.google.code.findbugs:jsr305:2.0.2',
+  sha1 = '516c03b21d50a644d538de0f0369c620989cd8f0',
+)
+
+maven_jar(
+  name = 'blame_cache',
+  repository = 'http://gerrit-maven.storage.googleapis.com/',
+  artifact = 'com/google/gitiles:blame-cache:0.1-9',
+  sha1 = '51d35e6f8bbc2412265066cea9653dd758c95826',
+)
+
+maven_jar(
+  name = 'dropwizard_core',
+  artifact = 'io.dropwizard.metrics:metrics-core:3.1.2',
+  sha1 = '224f03afd2521c6c94632f566beb1bb5ee32cf07',
+)
+
+# This version must match the version that also appears in
+# gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
+BC_VERS = '1.52'
+
+maven_jar(
+  name = 'bcprov',
+  artifact = 'org.bouncycastle:bcprov-jdk15on:' + BC_VERS,
+  sha1 = '88a941faf9819d371e3174b5ed56a3f3f7d73269',
+)
+
+maven_jar(
+  name = 'bcpg',
+  artifact = 'org.bouncycastle:bcpg-jdk15on:' + BC_VERS,
+  sha1 = 'ff4665a4b5633ff6894209d5dd10b7e612291858',
+)
+
+maven_jar(
+  name = 'bcpkix',
+  artifact = 'org.bouncycastle:bcpkix-jdk15on:' + BC_VERS,
+  sha1 = 'b8ffac2bbc6626f86909589c8cc63637cc936504',
+)
+
+maven_jar(
+  name = 'sshd',
+  artifact = 'org.apache.sshd:sshd-core:1.4.0',
+  sha1 = 'c8f3d7457fc9979d1b9ec319f0229b89793c8e56',
+)
+
+maven_jar(
+  name = 'mina_core',
+  artifact = 'org.apache.mina:mina-core:2.0.16',
+  sha1 = 'f720f17643eaa7b0fec07c1d7f6272972c02bba4',
+)
+
+maven_jar(
+  name = 'h2',
+  artifact = 'com.h2database:h2:1.3.176',
+  sha1 = 'fd369423346b2f1525c413e33f8cf95b09c92cbd',
+)
+
+HTTPCOMP_VERS = '4.4.1'
+
+maven_jar(
+  name = 'fluent_hc',
+  artifact = 'org.apache.httpcomponents:fluent-hc:' + HTTPCOMP_VERS,
+  sha1 = '96fb842b68a44cc640c661186828b60590c71261',
+)
+
+maven_jar(
+  name = 'httpclient',
+  artifact = 'org.apache.httpcomponents:httpclient:' + HTTPCOMP_VERS,
+  sha1 = '016d0bc512222f1253ee6b64d389c84e22f697f0',
+)
+
+maven_jar(
+  name = 'httpcore',
+  artifact = 'org.apache.httpcomponents:httpcore:' + HTTPCOMP_VERS,
+  sha1 = 'f5aa318bda4c6c8d688c9d00b90681dcd82ce636',
+)
+
+maven_jar(
+  name = 'httpmime',
+  artifact = 'org.apache.httpcomponents:httpmime:' + HTTPCOMP_VERS,
+  sha1 = '2f8757f5ac5e38f46c794e5229d1f3c522e9b1df',
+)
+
+# Test-only dependencies below.
+
+maven_jar(
+  name = 'jimfs',
+  artifact = 'com.google.jimfs:jimfs:1.0',
+  sha1 = 'edd65a2b792755f58f11134e76485a928aab4c97',
+)
+
+maven_jar(
+  name = 'junit',
+  artifact = 'junit:junit:4.11',
+  sha1 = '4e031bb61df09069aeb2bffb4019e7a5034a4ee0',
+)
+
+maven_jar(
+  name = 'hamcrest_core',
+  artifact = 'org.hamcrest:hamcrest-core:1.3',
+  sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0',
+)
+
+maven_jar(
+  name = 'truth',
+  artifact = 'com.google.truth:truth:0.28',
+  sha1 = '0a388c7877c845ff4b8e19689dda5ac9d34622c4',
+)
+
+maven_jar(
+  name = 'easymock',
+  artifact = 'org.easymock:easymock:3.4', # When bumping the version
+  sha1 = '9fdeea183a399f25c2469497612cad131e920fa3',
+)
+
+maven_jar(
+  name = 'cglib_2_2',
+  artifact = 'cglib:cglib-nodep:2.2.2',
+  sha1 = '00d456bb230c70c0b95c76fb28e429d42f275941',
+)
+
+maven_jar(
+  name = 'objenesis',
+  artifact = 'org.objenesis:objenesis:2.2',
+  sha1 = '3fb533efdaa50a768c394aa4624144cf8df17845',
+)
+
+POWERM_VERS = '1.6.4'
+
+maven_jar(
+  name = 'powermock_module_junit4',
+  artifact = 'org.powermock:powermock-module-junit4:' + POWERM_VERS,
+  sha1 = '8692eb1d9bb8eb1310ffe8a20c2da7ee6d1b5994',
+)
+
+maven_jar(
+  name = 'powermock_module_junit4_common',
+  artifact = 'org.powermock:powermock-module-junit4-common:' + POWERM_VERS,
+  sha1 = 'b0b578da443794ceb8224bd5f5f852aaf40f1b81',
+)
+
+maven_jar(
+  name = 'powermock_reflect',
+  artifact = 'org.powermock:powermock-reflect:' + POWERM_VERS,
+  sha1 = '5532f4e7c42db4bca4778bc9f1afcd4b0ee0b893',
+)
+
+maven_jar(
+  name = 'powermock_api_easymock',
+  artifact = 'org.powermock:powermock-api-easymock:' + POWERM_VERS,
+  sha1 = '5c385a0d8c13f84b731b75c6e90319c532f80b45',
+)
+
+maven_jar(
+  name = 'powermock_api_support',
+  artifact = 'org.powermock:powermock-api-support:' + POWERM_VERS,
+  sha1 = '314daafb761541293595630e10a3699ebc07881d',
+)
+
+maven_jar(
+  name = 'powermock_core',
+  artifact = 'org.powermock:powermock-core:' + POWERM_VERS,
+  sha1 = '85fb32e9ccba748d569fc36aef92e0b9e7f40b87',
+)
+
+maven_jar(
+  name = 'javassist',
+  artifact = 'org.javassist:javassist:3.20.0-GA',
+  sha1 = 'a9cbcdfb7e9f86fbc74d3afae65f2248bfbf82a0',
+)
+
+maven_jar(
+  name = 'derby',
+  artifact = 'org.apache.derby:derby:10.11.1.1',
+  sha1 = 'df4b50061e8e4c348ce243b921f53ee63ba9bbe1',
+)
+
+JETTY_VERS = '9.2.14.v20151106'
+
+maven_jar(
+  name = 'jetty_servlet',
+  artifact = 'org.eclipse.jetty:jetty-servlet:' + JETTY_VERS,
+  sha1 = '3a2cd4d8351a38c5d60e0eee010fee11d87483ef',
+)
+
+maven_jar(
+  name = 'jetty_security',
+  artifact = 'org.eclipse.jetty:jetty-security:' + JETTY_VERS,
+  sha1 = '2d36974323fcb31e54745c1527b996990835db67',
+)
+
+maven_jar(
+  name = 'jetty_servlets',
+  artifact = 'org.eclipse.jetty:jetty-servlets:' + JETTY_VERS,
+  sha1 = 'a75c78a0ee544073457ca5ee9db20fdc6ed55225',
+)
+
+maven_jar(
+  name = 'jetty_server',
+  artifact = 'org.eclipse.jetty:jetty-server:' + JETTY_VERS,
+  sha1 = '70b22c1353e884accf6300093362b25993dac0f5',
+)
+
+maven_jar(
+  name = 'jetty_jmx',
+  artifact = 'org.eclipse.jetty:jetty-jmx:' + JETTY_VERS,
+  sha1 = '617edc5e966b4149737811ef8b289cd94b831bab',
+)
+
+maven_jar(
+  name = 'jetty_continuation',
+  artifact = 'org.eclipse.jetty:jetty-continuation:' + JETTY_VERS,
+  sha1 = '8909d62fd7e28351e2da30de6fb4105539b949c0',
+)
+
+maven_jar(
+  name = 'jetty_http',
+  artifact = 'org.eclipse.jetty:jetty-http:' + JETTY_VERS,
+  sha1 = '699ad1f2fa6fb0717e1b308a8c9e1b8c69d81ef6',
+)
+
+maven_jar(
+  name = 'jetty_io',
+  artifact = 'org.eclipse.jetty:jetty-io:' + JETTY_VERS,
+  sha1 = 'dfa4137371a3f08769820138ca1a2184dacda267',
+)
+
+maven_jar(
+  name = 'jetty_util',
+  artifact = 'org.eclipse.jetty:jetty-util:' + JETTY_VERS,
+  sha1 = '0057e00b912ae0c35859ac81594a996007706a0b',
+)
+
+maven_jar(
+  name = 'openid_consumer',
+  artifact = 'org.openid4java:openid4java:0.9.8',
+  sha1 = 'de4f1b33d3b0f0b2ab1d32834ec1190b39db4160',
+)
+
+maven_jar(
+  name = 'nekohtml',
+  artifact = 'net.sourceforge.nekohtml:nekohtml:1.9.10',
+  sha1 = '14052461031a7054aa094f5573792feb6686d3de',
+)
+
+maven_jar(
+  name = 'xerces',
+  artifact = 'xerces:xercesImpl:2.8.1',
+  sha1 = '25101e37ec0c907db6f0612cbf106ee519c1aef1',
+)
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py
index 32edf84..5f5b9ef 100755
--- a/contrib/abandon_stale.py
+++ b/contrib/abandon_stale.py
@@ -72,11 +72,17 @@
     parser.add_option('-m', '--message', dest='message',
                       metavar='STRING', default=None,
                       help='Custom message to append to abandon message')
+    parser.add_option('--branch', dest='branches', metavar='BRANCH_NAME',
+                      default=[], action='append',
+                      help='Abandon changes only on the given branch')
     parser.add_option('--exclude-branch', dest='exclude_branches',
                       metavar='BRANCH_NAME',
                       default=[],
                       action='append',
                       help='Do not abandon changes on given branch')
+    parser.add_option('--project', dest='projects', metavar='PROJECT_NAME',
+                      default=[], action='append',
+                      help='Abandon changes only on the given project')
     parser.add_option('--exclude-project', dest='exclude_projects',
                       metavar='PROJECT_NAME',
                       default=[],
@@ -126,9 +132,15 @@
         stale_changes = []
         offset = 0
         step = 500
-        query_terms = ["status:new", "age:%s" % options.age] + \
-                      ["-branch:%s" % b for b in options.exclude_branches] + \
-                      ["-project:%s" % p for p in options.exclude_projects]
+        query_terms = ["status:new", "age:%s" % options.age]
+        if options.branches:
+            query_terms += ["branch:%s" % b for b in options.branches]
+        elif options.exclude_branches:
+            query_terms += ["-branch:%s" % b for b in options.exclude_branches]
+        if options.projects:
+            query_terms += ["project:%s" % p for p in options.projects]
+        elif options.exclude_projects:
+            query_terms = ["-project:%s" % p for p in options.exclude_projects]
         if options.owner:
             query_terms += ["owner:%s" % options.owner]
         query = "%20".join(query_terms)
diff --git a/contrib/bash_completion b/contrib/bash_completion
index 6772235..19060a5c 100644
--- a/contrib/bash_completion
+++ b/contrib/bash_completion
@@ -65,7 +65,7 @@
     COMPREPLY=()
     cur="${COMP_WORDS[COMP_CWORD]}"
     prev="${COMP_WORDS[COMP_CWORD-1]}"
-    opts="check restart run start status stop supervise"
+    opts="check restart run start status stop supervise threads"
 
     COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
 }
diff --git a/contrib/git-exproll.sh b/contrib/git-exproll.sh
index 066c57c..9ad7a85 100644
--- a/contrib/git-exproll.sh
+++ b/contrib/git-exproll.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 # Copyright (c) 2012, Code Aurora Forum. All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/contrib/populate-fixture-data.py b/contrib/populate-fixture-data.py
new file mode 100644
index 0000000..c35f82c
--- /dev/null
+++ b/contrib/populate-fixture-data.py
@@ -0,0 +1,298 @@
+#!/usr/bin/env python
+# 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.
+
+"""
+This script will populate an empty standard Gerrit instance with some
+data for local testing.
+
+This script requires 'requests'. If you do not have this module, run
+'pip3 install requests' to install it.
+
+TODO(hiesel): Make real git commits instead of empty changes
+TODO(hiesel): Add comments
+"""
+
+import atexit
+import json
+import os
+import random
+import shutil
+import subprocess
+import tempfile
+
+import requests
+import requests.auth
+
+DEFAULT_TMP_PATH = "/tmp"
+TMP_PATH = ""
+BASE_URL = "http://localhost:8080/a/"
+ACCESS_URL = BASE_URL + "access/"
+ACCOUNTS_URL = BASE_URL + "accounts/"
+CHANGES_URL = BASE_URL + "changes/"
+CONFIG_URL = BASE_URL + "config/"
+GROUPS_URL = BASE_URL + "groups/"
+PLUGINS_URL = BASE_URL + "plugins/"
+PROJECTS_URL = BASE_URL + "projects/"
+
+ADMIN_DIGEST = requests.auth.HTTPDigestAuth("admin", "secret")
+
+# GROUP_ADMIN stores a GroupInfo for the admin group (see Gerrit rest docs)
+# In addition, GROUP_ADMIN["name"] stores the admin group"s name.
+GROUP_ADMIN = {}
+
+HEADERS = {"Content-Type": "application/json", "charset": "UTF-8"}
+
+# Random names from US Census Data
+FIRST_NAMES = [
+  "Casey", "Yesenia", "Shirley", "Tara", "Wanda", "Sheryl", "Jaime", "Elaine",
+  "Charlotte", "Carly", "Bonnie", "Kirsten", "Kathryn", "Carla", "Katrina",
+  "Melody", "Suzanne", "Sandy", "Joann", "Kristie", "Sally", "Emma", "Susan",
+  "Amanda", "Alyssa", "Patty", "Angie", "Dominique", "Cynthia", "Jennifer",
+  "Theresa", "Desiree", "Kaylee", "Maureen", "Jeanne", "Kellie", "Valerie",
+  "Nina", "Judy", "Diamond", "Anita", "Rebekah", "Stefanie", "Kendra", "Erin",
+  "Tammie", "Tracey", "Bridget", "Krystal", "Jasmin", "Sonia", "Meghan",
+  "Rebecca", "Jeanette", "Meredith", "Beverly", "Natasha", "Chloe", "Selena",
+  "Teresa", "Sheena", "Cassandra", "Rhonda", "Tami", "Jodi", "Shelly", "Angela",
+  "Kimberly", "Terry", "Joanna", "Isabella", "Lindsey", "Loretta", "Dana",
+  "Veronica", "Carolyn", "Laura", "Karen", "Dawn", "Alejandra", "Cassie",
+  "Lorraine", "Yolanda", "Kerry", "Stephanie", "Caitlin", "Melanie", "Kerri",
+  "Doris", "Sandra", "Beth", "Carol", "Vicki", "Shelia", "Bethany", "Rachael",
+  "Donna", "Alexandra", "Barbara", "Ana", "Jillian", "Ann", "Rachel", "Lauren",
+  "Hayley", "Misty", "Brianna", "Tanya", "Danielle", "Courtney", "Jacqueline",
+  "Becky", "Christy", "Alisha", "Phyllis", "Faith", "Jocelyn", "Nancy",
+  "Gloria", "Kristen", "Evelyn", "Julie", "Julia", "Kara", "Chelsey", "Cassidy",
+  "Jean", "Chelsea", "Jenny", "Diana", "Haley", "Kristine", "Kristina", "Erika",
+  "Jenna", "Alison", "Deanna", "Abigail", "Melissa", "Sierra", "Linda",
+  "Monica", "Tasha", "Traci", "Yvonne", "Tracy", "Marie", "Maria", "Michaela",
+  "Stacie", "April", "Morgan", "Cathy", "Darlene", "Cristina", "Emily"
+  "Ian", "Russell", "Phillip", "Jay", "Barry", "Brad", "Frederick", "Fernando",
+  "Timothy", "Ricardo", "Bernard", "Daniel", "Ruben", "Alexis", "Kyle", "Malik",
+  "Norman", "Kent", "Melvin", "Stephen", "Daryl", "Kurt", "Greg", "Alex",
+  "Mario", "Riley", "Marvin", "Dan", "Steven", "Roberto", "Lucas", "Leroy",
+  "Preston", "Drew", "Fred", "Casey", "Wesley", "Elijah", "Reginald", "Joel",
+  "Christopher", "Jacob", "Luis", "Philip", "Mark", "Rickey", "Todd", "Scott",
+  "Terrence", "Jim", "Stanley", "Bobby", "Thomas", "Gabriel", "Tracy", "Marcus",
+  "Peter", "Michael", "Calvin", "Herbert", "Darryl", "Billy", "Ross", "Dustin",
+  "Jaime", "Adam", "Henry", "Xavier", "Dominic", "Lonnie", "Danny", "Victor",
+  "Glen", "Perry", "Jackson", "Grant", "Gerald", "Garrett", "Alejandro",
+  "Eddie", "Alan", "Ronnie", "Mathew", "Dave", "Wayne", "Joe", "Craig",
+  "Terry", "Chris", "Randall", "Parker", "Francis", "Keith", "Neil", "Caleb",
+  "Jon", "Earl", "Taylor", "Bryce", "Brady", "Max", "Sergio", "Leon", "Gene",
+  "Darin", "Bill", "Edgar", "Antonio", "Dalton", "Arthur", "Austin", "Cristian",
+  "Kevin", "Omar", "Kelly", "Aaron", "Ethan", "Tom", "Isaac", "Maurice",
+  "Gilbert", "Hunter", "Willie", "Harry", "Dale", "Darius", "Jerome", "Jason",
+  "Harold", "Kerry", "Clarence", "Gregg", "Shane", "Eduardo", "Micheal",
+  "Howard", "Vernon", "Rodney", "Anthony", "Levi", "Larry", "Franklin", "Jimmy",
+  "Jonathon", "Carl",
+]
+
+LAST_NAMES = [
+  "Savage", "Hendrix", "Moon", "Larsen", "Rocha", "Burgess", "Bailey", "Farley",
+  "Moses", "Schmidt", "Brown", "Hoover", "Klein", "Jennings", "Braun", "Rangel",
+  "Casey", "Dougherty", "Hancock", "Wolf", "Henry", "Thomas", "Bentley",
+  "Barnett", "Kline", "Pitts", "Rojas", "Sosa", "Paul", "Hess", "Chase",
+  "Mckay", "Bender", "Colins", "Montoya", "Townsend", "Potts", "Ayala", "Avery",
+  "Sherman", "Tapia", "Hamilton", "Ferguson", "Huang", "Hooper", "Zamora",
+  "Logan", "Lloyd", "Quinn", "Monroe", "Brock", "Ibarra", "Fowler", "Weiss",
+  "Montgomery", "Diaz", "Dixon", "Olson", "Robertson", "Arias", "Benjamin",
+  "Abbott", "Stein", "Schroeder", "Beck", "Velasquez", "Barber", "Nichols",
+  "Ortiz", "Burns", "Moody", "Stokes", "Wilcox", "Rush", "Michael", "Kidd",
+  "Rowland", "Mclean", "Saunders", "Chung", "Newton", "Potter", "Hickman",
+  "Ray", "Larson", "Figueroa", "Duncan", "Sparks", "Rose", "Hodge", "Huynh",
+  "Joseph", "Morales", "Beasley", "Mora", "Fry", "Ross", "Novak", "Hahn",
+  "Wise", "Knight", "Frederick", "Heath", "Pollard", "Vega", "Mcclain",
+  "Buckley", "Conrad", "Cantrell", "Bond", "Mejia", "Wang", "Lewis", "Johns",
+  "Mcknight", "Callahan", "Reynolds", "Norris", "Burnett", "Carey", "Jacobson",
+  "Oneill", "Oconnor", "Leonard", "Mckenzie", "Hale", "Delgado", "Spence",
+  "Brandt", "Obrien", "Bowman", "James", "Avila", "Roberts", "Barker", "Cohen",
+  "Bradley", "Prince", "Warren", "Summers", "Little", "Caldwell", "Garrett",
+  "Hughes", "Norton", "Burke", "Holden", "Merritt", "Lee", "Frank", "Wiley",
+  "Ho", "Weber", "Keith", "Winters", "Gray", "Watts", "Brady", "Aguilar",
+  "Nicholson", "David", "Pace", "Cervantes", "Davis", "Baxter", "Sanchez",
+  "Singleton", "Taylor", "Strickland", "Glenn", "Valentine", "Roy", "Cameron",
+  "Beard", "Norman", "Fritz", "Anthony", "Koch", "Parrish", "Herman", "Hines",
+  "Sutton", "Gallegos", "Stephenson", "Lozano", "Franklin", "Howe", "Bauer",
+  "Love", "Ali", "Ellison", "Lester", "Guzman", "Jarvis", "Espinoza",
+  "Fletcher", "Burton", "Woodard", "Peterson", "Barajas", "Richard", "Bryan",
+  "Goodman", "Cline", "Rowe", "Faulkner", "Crawford", "Mueller", "Patterson",
+  "Hull", "Walton", "Wu", "Flores", "York", "Dickson", "Barnes", "Fisher",
+  "Strong", "Juarez", "Fitzgerald", "Schmitt", "Blevins", "Villa", "Sullivan",
+  "Velazquez", "Horton", "Meadows", "Riley", "Barrera", "Neal", "Mendez",
+  "Mcdonald", "Floyd", "Lynch", "Mcdowell", "Benson", "Hebert", "Livingston",
+  "Davies", "Richardson", "Vincent", "Davenport", "Osborn", "Mckee", "Marshall",
+  "Ferrell", "Martinez", "Melton", "Mercer", "Yoder", "Jacobs", "Mcdaniel",
+  "Mcmillan", "Peters", "Atkinson", "Wood", "Briggs", "Valencia", "Chandler",
+  "Rios", "Hunter", "Bean", "Hicks", "Hays", "Lucero", "Malone", "Waller",
+  "Banks", "Myers", "Mitchell", "Grimes", "Houston", "Hampton", "Trujillo",
+  "Perkins", "Moran", "Welch", "Contreras", "Montes", "Ayers", "Hayden",
+  "Daniel", "Weeks", "Porter", "Gill", "Mullen", "Nolan", "Dorsey", "Crane",
+  "Estes", "Lam", "Wells", "Cisneros", "Giles", "Watson", "Vang", "Scott",
+  "Knox", "Hanna", "Fields",
+]
+
+
+def clean(json_string):
+  # Strip JSON XSS Tag
+  json_string = json_string.strip()
+  if json_string.startswith(")]}'"):
+    return json_string[5:]
+  return json_string
+
+
+def digest_auth(user):
+  return requests.auth.HTTPDigestAuth(user["username"], user["http_password"])
+
+
+def fetch_admin_group():
+  global GROUP_ADMIN
+  # Get admin group
+  r = json.loads(clean(requests.get(GROUPS_URL + "?suggest=ad&p=All-Projects",
+                                    headers=HEADERS,
+                                    auth=ADMIN_DIGEST).text))
+  admin_group_name = r.keys()[0]
+  GROUP_ADMIN = r[admin_group_name]
+  GROUP_ADMIN["name"] = admin_group_name
+
+
+def generate_random_text():
+  return " ".join([random.choice("lorem ipsum "
+                                 "doleret delendam "
+                                 "\n esse".split(" ")) for _ in xrange(1, 100)])
+
+
+def set_up():
+  global TMP_PATH
+  TMP_PATH = tempfile.mkdtemp()
+  atexit.register(clean_up)
+  os.makedirs(TMP_PATH + "/ssh")
+  os.makedirs(TMP_PATH + "/repos")
+  fetch_admin_group()
+
+
+def get_random_users(num_users):
+  users = [(f, l) for f in FIRST_NAMES for l in LAST_NAMES][:num_users]
+  names = []
+  for u in users:
+    names.append({"firstname": u[0],
+                  "lastname": u[1],
+                  "name": u[0] + " " + u[1],
+                  "username": u[0] + u[1],
+                  "email": u[0] + "." + u[1] + "@gmail.com",
+                  "http_password": "secret",
+                  "groups": []})
+  return names
+
+
+def generate_ssh_keys(gerrit_users):
+  for user in gerrit_users:
+    key_file = TMP_PATH + "/ssh/" + user["username"] + ".key"
+    subprocess.check_output(["ssh-keygen", "-f", key_file, "-N", ""])
+    with open(key_file + ".pub", "r") as f:
+      user["ssh_key"] = f.read()
+
+
+def create_gerrit_groups():
+  groups = [
+    {"name": "iOS-Maintainers", "description": "iOS Maintainers",
+     "visible_to_all": True, "owner": GROUP_ADMIN["name"],
+     "owner_id": GROUP_ADMIN["id"]},
+    {"name": "Android-Maintainers", "description": "Android Maintainers",
+     "visible_to_all": True, "owner": GROUP_ADMIN["name"],
+     "owner_id": GROUP_ADMIN["id"]},
+    {"name": "Backend-Maintainers", "description": "Backend Maintainers",
+     "visible_to_all": True, "owner": GROUP_ADMIN["name"],
+     "owner_id": GROUP_ADMIN["id"]},
+    {"name": "Script-Maintainers", "description": "Script Maintainers",
+     "visible_to_all": True, "owner": GROUP_ADMIN["name"],
+     "owner_id": GROUP_ADMIN["id"]},
+    {"name": "Security-Team", "description": "Sec Team",
+     "visible_to_all": False, "owner": GROUP_ADMIN["name"],
+     "owner_id": GROUP_ADMIN["id"]}]
+  for g in groups:
+    requests.put(GROUPS_URL + g["name"],
+                 json.dumps(g),
+                 headers=HEADERS,
+                 auth=ADMIN_DIGEST)
+  return [g["name"] for g in groups]
+
+
+def create_gerrit_projects(owner_groups):
+  projects = [
+    {"id": "android", "name": "Android", "parent": "All-Projects",
+     "branches": ["master"], "description": "Our android app.",
+     "owners": [owner_groups[0]], "create_empty_commit": True},
+    {"id": "ios", "name": "iOS", "parent": "All-Projects",
+     "branches": ["master"], "description": "Our ios app.",
+     "owners": [owner_groups[1]], "create_empty_commit": True},
+    {"id": "backend", "name": "Backend", "parent": "All-Projects",
+     "branches": ["master"], "description": "Our awesome backend.",
+     "owners": [owner_groups[2]], "create_empty_commit": True},
+    {"id": "scripts", "name": "Scripts", "parent": "All-Projects",
+     "branches": ["master"], "description": "some small scripts.",
+     "owners": [owner_groups[3]], "create_empty_commit": True}]
+  for p in projects:
+    requests.put(PROJECTS_URL + p["name"],
+                 json.dumps(p),
+                 headers=HEADERS,
+                 auth=ADMIN_DIGEST)
+  return [p["name"] for p in projects]
+
+
+def create_gerrit_users(gerrit_users):
+  for user in gerrit_users:
+    requests.put(ACCOUNTS_URL + user["username"],
+                 json.dumps(user),
+                 headers=HEADERS,
+                 auth=ADMIN_DIGEST)
+
+
+def create_change(user, project_name):
+  random_commit_message = generate_random_text()
+  change = {
+    "project": project_name,
+    "subject": random_commit_message.split("\n")[0],
+    "branch": "master",
+    "status": "NEW",
+  }
+  requests.post(CHANGES_URL,
+                json.dumps(change),
+                headers=HEADERS,
+                auth=digest_auth(user))
+
+
+def clean_up():
+  shutil.rmtree(TMP_PATH)
+
+
+def main():
+  set_up()
+  gerrit_users = get_random_users(100)
+
+  group_names = create_gerrit_groups()
+  for idx, u in enumerate(gerrit_users):
+    u["groups"].append(group_names[idx % len(group_names)])
+    if idx % 5 == 0:
+      # Also add to security group
+      u["groups"].append(group_names[4])
+
+  generate_ssh_keys(gerrit_users)
+  create_gerrit_users(gerrit_users)
+
+  project_names = create_gerrit_projects(group_names)
+
+  for idx, u in enumerate(gerrit_users):
+    create_change(u, project_names[4 * idx / len(gerrit_users)])
+
+main()
diff --git a/contrib/themes/diffy/static/diffy.svg b/contrib/themes/diffy/static/diffy.svg
new file mode 100644
index 0000000..3e6e6c2
--- /dev/null
+++ b/contrib/themes/diffy/static/diffy.svg
@@ -0,0 +1,326 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="600.06732"
+   height="558.20709"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.3.1 r9886"
+   sodipodi:docname="diffy.svg"
+   inkscape:export-filename="/home/sarah/art/diffy.png"
+   inkscape:export-xdpi="500"
+   inkscape:export-ydpi="500">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.66618777"
+     inkscape:cx="300.03365"
+     inkscape:cy="258.35521"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1366"
+     inkscape:window-height="744"
+     inkscape:window-x="0"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-3069.8852,2251.5084)">
+    <text
+       xml:space="preserve"
+       style="font-size:51.65934372px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:end;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeSans;-inkscape-font-specification:FreeSans Bold"
+       x="475.83173"
+       y="900.47076"
+       id="text4257"
+       sodipodi:linespacing="125%"
+       transform="scale(1.1411753,0.87628955)"><tspan
+         sodipodi:role="line"
+         x="475.83173"
+         y="900.47076"
+         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:end;text-anchor:end;font-family:Acme;-inkscape-font-specification:Acme Bold"
+         id="tspan3008" /></text>
+    <flowRoot
+       xml:space="preserve"
+       id="flowRoot4263"
+       style="font-size:12px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
+         id="flowRegion4265"><rect
+           id="rect4267"
+           width="313.14728"
+           height="343.45187"
+           x="-335.37064"
+           y="205.85435" /></flowRegion><flowPara
+         id="flowPara4269" /></flowRoot>    <g
+       id="g10119"
+       transform="matrix(0.99201906,-0.12608805,0.12608805,0.99201906,273.85438,408.58042)">
+      <path
+         sodipodi:nodetypes="csccscac"
+         inkscape:connector-curvature="0"
+         id="path4130"
+         d="m 3378.9965,-2206.2972 c 0,0 13.4039,-8.8398 27.0022,-17.1718 13.5984,-8.3319 22.2322,-21.276 22.2322,-21.276 l 34.2799,28.1467 c 0,0 -16.0641,15.553 -24.7742,21.6893 -3.709,2.6129 -10.9463,9.1577 -10.9463,9.1577 0,0 -14.3617,-10.0769 -22.3623,-13.5225 -8.0773,-3.4786 -25.4315,-7.0234 -25.4315,-7.0234 z"
+         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         sodipodi:nodetypes="cac"
+         inkscape:connector-curvature="0"
+         d="m 3414.7387,-2204.9828 c 0,0 8.2233,-5.8571 11.9326,-9.2622 3.8224,-3.5089 10.5596,-11.437 10.5596,-11.437"
+         style="fill:#ffffff;stroke:#ff0000;stroke-width:8.97858429;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+         id="path4185" />
+      <path
+         sodipodi:nodetypes="cascacsc"
+         inkscape:connector-curvature="0"
+         id="path3307"
+         d="m 3247.6516,-2171.4037 c 0,0 4.3348,-19.2835 10.7071,-26.2701 7.4284,-8.1446 19.072,-14.1542 29.3495,-15.24 27.8177,-2.9386 63.2371,6.1908 63.2371,6.1908 0,0 -30.8042,0.7262 -45.6022,4.5134 -5.1922,1.3289 -14.7941,6.2977 -14.7941,6.2977 0,0 -16.1166,0.8374 -29.8971,12.0927 -13.7804,11.2552 -3.0344,9.7831 -3.0344,9.7831"
+         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         sodipodi:nodetypes="ccsccscscccccscc"
+         inkscape:connector-curvature="0"
+         id="path3011"
+         d="m 3332.9985,-2012.4352 -20.0742,24.9886 c 0,0 -16.3479,-5.082 -40.3255,4.9725 -23.9776,10.0546 -39.2193,26.4042 -39.2193,26.4042 l 7.8897,-0.7975 c 0,0 20.4818,-14.4371 31.9614,-17.1294 11.4796,-2.6923 23.6197,-2.8388 23.6197,-2.8388 0,0 -25.492,10.0264 -30.7497,12.3759 -6.2584,2.7968 -15.0874,9.199 -15.0874,9.199 l 10.4761,-0.2965 c 17.6867,-7.2528 41.3221,-16.0187 59.9685,-15.047 15.5919,1.1274 33.9405,16.7543 33.9405,16.7543 l -3.8165,-9.9895 c 0,0 -10.8307,-8.1932 -16.3866,-13.7491 -5.5558,-5.5558 17.5848,-25.8193 17.5848,-25.8193 -14.3098,-2.8351 -2.1172,-7.9424 -19.7815,-9.0274 z"
+         style="fill:#ffff00;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         style="fill:#ffff00;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+         d="m 3397.9929,-1984.0461 -15.5774,33.0846 c 0,0 -14.4499,-2.2165 -39.5097,4.7139 -25.0597,6.9303 -44.9575,20.0476 -44.9575,20.0476 l 10.5706,1.822 c 0,0 19.7276,-11.1182 30.516,-14.1955 7.747,-2.2097 23.9934,-2.9008 23.9934,-2.9008 0,0 -20.2687,6.6869 -30.057,10.9033 -5.4067,2.329 -15.7817,7.9273 -15.7817,7.9273 l 10.8391,1.5269 c 0,0 31.056,-11.8625 47.4047,-13.3504 11.1103,-1.0112 11.8331,-2.962 33.324,3.1079 10.0509,2.8388 16.3928,6.252 16.3928,6.252 l -3.3122,-5.8696 c 0,0 -5.3909,-4.5711 -8.2712,-6.6111 -3.5982,-2.5484 -11.2202,-7.0059 -11.2202,-7.0059 l 15.4484,-30.858 c -13.3255,-5.9362 -2.8177,-3.6211 -19.8021,-8.5942 z"
+         id="path3013"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="ccsccacaccasccaccc" />
+      <path
+         sodipodi:nodetypes="aaczsccsssca"
+         inkscape:connector-curvature="0"
+         id="path3014"
+         d="m 3138.8926,-2124.2277 c 9.8375,-20.5358 27.6337,-38.9533 48.4705,-48.136 22.7321,-10.018 51.1582,-10.0879 74.4382,-3.5941 7.6944,6.5 10.3115,-17.1804 46.9059,-27.4208 36.5944,-10.2403 91.6429,-6.509 142.1505,22.7854 54.0494,31.3487 -23.7386,41.9213 -8.0812,94.4492 32.3844,113.578 -63.1345,101.5204 -63.1345,101.5204 0,0 -25.2539,-16.1625 -35.3554,-17.1726 -10.1015,-1.0102 -46.6429,-25.1251 -68.8662,-28.6607 -22.2234,-3.5355 -36.9003,18.6222 -107.991,-9.9282 -24.3178,-9.7661 -33.8401,-33.335 -33.8401,-33.335 0,0 -2.0102,-35.2405 5.3033,-50.5076 z"
+         style="fill:#0000ff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         sodipodi:nodetypes="cssscscsac"
+         inkscape:connector-curvature="0"
+         id="path3028"
+         d="m 3179.7636,-2035.4822 c 0,0 33.988,-7.9036 57.2215,-14.9746 23.2335,-7.0711 36.1637,-43.7474 71.519,-48.7981 35.3554,-5.0508 68.528,22.1624 80.6498,47.4162 12.1218,25.2539 50.6701,55.6193 50.6701,55.6193 0,0 1.0101,11.1117 -14.1422,18.1828 -15.1522,7.0711 -10.4989,7.6628 -10.4989,7.6628 0,0 -43.1103,-11.4139 -62.396,-21.4139 -19.2857,-10 -61.236,-23.044 -92.9377,-30.7963 -26.2653,-6.4228 -35.0334,2.5255 -80.0856,-12.8982 z"
+         style="fill:#ff6600;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         sodipodi:nodetypes="cscccccscc"
+         inkscape:connector-curvature="0"
+         id="path3009"
+         d="m 3470.1209,-2002.8128 c 0,0 13.8937,99.0002 33.6829,157.8517 19.7893,58.8514 78.4464,180.4742 78.4464,180.4742 l 28.1031,-31.8521 -52.5625,-109.7203 60.5152,95.9016 26.0124,-35.7271 c 0,0 -70.663,-88.4878 -93.8954,-140.4597 -23.2324,-51.9718 -37.4945,-152.9082 -37.4945,-152.9082 z"
+         style="fill:#0000ff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         sodipodi:nodetypes="csscsccccccccccsc"
+         inkscape:connector-curvature="0"
+         id="path3016"
+         d="m 3359.2154,-2191.502 c 0,0 -40.7142,5 -57.1428,42.1428 -16.4286,37.1429 50.7143,115.7143 60.7143,132.1429 10,16.4286 24.2857,38.5714 24.2857,38.5714 0,0 47.6766,32.0335 82.9062,53.8444 32.6567,20.2179 65.2266,33.1109 65.2266,33.1109 0,0 1.1687,-0.4937 7.0265,-19.1963 -12.1218,-21.7183 -38.3216,-53.2543 -54.5036,-69.192 24.2792,14.1167 43.346,28.1254 60.1041,56.5482 0,0 4.9588,-21.3916 6.5659,-30.7894 -3.0357,-9.5763 -47.4771,-56.5685 -47.4771,-56.5685 20.5517,8.7613 33.7508,23.0255 50.0892,44.7732 0,0 3.3263,-21.5598 3.8366,-32.9472 -2.5253,-6.566 -33.7228,-35.0595 -33.7228,-35.0595 13.1654,6.6374 19.714,11.0661 34.4127,24.0476 0,0 -3.0357,-60.7142 -66.6072,-130.7142 -63.5714,-70 -135.7143,-50.7143 -135.7143,-50.7143 z"
+         style="fill:#000080;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <g
+         transform="translate(2935.644,-2513.1499)"
+         id="g3138">
+        <path
+           style="fill:#ff0000;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+           d="m 337.66259,394.0585 c 0,0 -28.86328,-30.61257 -60.78781,-25.80202 -31.92454,4.81054 -41.10831,33.67382 -39.79634,39.79633 1.31197,6.12252 13.557,11.80771 22.30345,15.74361 8.74644,3.9359 32.79917,-0.43732 45.48152,-6.55984 12.68235,-6.12251 32.79918,-23.17808 32.79918,-23.17808 z"
+           id="path3022"
+           inkscape:connector-curvature="0" />
+        <path
+           style="fill:#ffff00;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-miterlimit:4"
+           d="m 286.65383,372.49887 c 12.50235,2.18615 20.86528,14.09354 18.67912,26.59589 -2.18615,12.50234 -14.09354,20.86527 -26.59589,18.67911 -12.50235,-2.18616 -20.86528,-14.09354 -18.67912,-26.59588 z"
+           id="path3024"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="csscc" />
+        <path
+           sodipodi:type="arc"
+           style="fill:#000000;fill-opacity:1;stroke:none"
+           id="path3026"
+           sodipodi:cx="229.64285"
+           sodipodi:cy="403.79074"
+           sodipodi:rx="8.2142859"
+           sodipodi:ry="8.2142859"
+           d="m 237.85714,403.79074 c 0,4.53663 -3.67766,8.21429 -8.21429,8.21429 -4.53662,0 -8.21428,-3.67766 -8.21428,-8.21429 0,-4.53662 3.67766,-8.21428 8.21428,-8.21428 4.53663,0 8.21429,3.67766 8.21429,8.21428 z"
+           transform="matrix(1.5694327,0,0,1.5694327,-85.566867,-233.30737)" />
+      </g>
+      <path
+         sodipodi:nodetypes="cssssssc"
+         style="fill:#ffff00;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+         d="m 3110.2692,-2103.8575 c -32.3249,20.7081 -31.4522,56.0845 -27.4116,60.6301 4.0406,4.5457 8.0812,1.0102 15.6574,-1.5152 7.5761,-2.5254 26.2639,-10.1015 42.4264,-11.1117 16.1624,-1.0101 21.2132,3.0305 23.7386,-0.505 2.5254,-3.5356 4.0406,-15.1523 3.0304,-25.7589 -1.0101,-10.6066 -1.0101,-23.2335 -7.071,-25.2538 -6.061,-2.0203 -38.2484,-5.0718 -50.3702,3.5145 z"
+         id="path3296"
+         inkscape:connector-curvature="0" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path3305"
+         d="m 3251.1404,-2178.2729 c 0,0 12.1428,21.0714 67.1428,32.5 55,11.4286 125.7143,-38.2143 125.7143,-38.2143 l 38.7857,38.6429 c 0,0 -113.0714,25.6428 -150.2143,21 -37.1428,-4.6429 -81.4285,-50.3572 -81.4285,-50.3572"
+         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         sodipodi:nodetypes="ccccc"
+         inkscape:connector-curvature="0"
+         id="path4099"
+         d="m 3453.1169,-2166.1191 -30.2186,14.3667 6.8378,5.5184 29.9689,-14.0733 z"
+         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
+      <path
+         sodipodi:nodetypes="cacscacsc"
+         inkscape:connector-curvature="0"
+         id="path3303"
+         d="m 3154.8351,-2149.0637 c 0,0 -6.1891,7.2925 -8.7928,11.2888 -2.8625,4.3936 -7.4472,13.8569 -7.4472,13.8569 0,0 49.8667,-24.0831 67.6065,-30.2972 18.2787,-6.4028 54.803,-21.7873 54.803,-21.7873 0,0 -3.8451,-1.8876 -5.9103,-2.3872 -3.1018,-0.7503 -9.5343,-0.868 -9.5343,-0.868 0,0 -25.7837,-0.1235 -42.8032,6.1343 -17.0196,6.2579 -47.9217,24.0597 -47.9217,24.0597 z"
+         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
+         d="m 3433.0379,-2169.2212 24.4739,22.8165 -8.1844,3.1976 -24.3243,-22.4616 z"
+         id="path27241"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="ccccc" />
+    </g>
+    <g
+       id="g10229"
+       transform="translate(31.403024,6.4439689)">
+      <g
+         id="text4253"
+         style="font-size:140.50161743px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeSans;-inkscape-font-specification:FreeSans Bold"
+         transform="scale(1.066563,0.93759113)">
+        <path
+           id="path10219"
+           style="font-family:Acme;-inkscape-font-specification:Acme Bold"
+           d="m 2917.8887,-1930.8568 c -6.6505,0 -16.2514,-0.5152 -28.8029,-1.5455 l 1.6861,-34.1419 -1.6861,-62.8043 37.514,0 c 12.645,10e-5 22.527,3.7937 29.6458,11.3807 7.1187,7.5871 10.678,18.1247 10.6781,31.6128 -10e-5,17.0476 -4.4024,30.5826 -13.2071,40.605 -8.8049,9.9288 -20.7475,14.8932 -35.8279,14.8932 m 3.653,-83.3175 c -6.1821,10e-5 -10.5376,0.047 -13.0666,0.1405 l -1.4051,45.101 0.9836,22.7613 c 7.868,0.562 13.1602,0.843 15.8766,0.843 7.3997,0 13.2071,-2.9973 17.4222,-8.9921 4.3087,-5.9947 6.463,-14.7058 6.4631,-26.1333 -1e-4,-11.4274 -2.1076,-19.9043 -6.3226,-25.4308 -4.1214,-5.5263 -10.7718,-8.2895 -19.9512,-8.2896"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10221"
+           style="font-family:Acme;-inkscape-font-specification:Acme Bold"
+           d="m 3002.3389,-2030.051 -1.967,61.1182 1.686,36.5305 -19.6702,0 1.686,-34.1419 -1.686,-59.9942 19.9512,-3.5126"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10223"
+           style="font-family:Acme;-inkscape-font-specification:Acme Bold"
+           d="m 3074.2384,-2014.3148 -29.9268,0 -0.843,25.8523 26.8358,0 -1.5455,14.8932 -25.7118,0 -0.1405,4.6365 1.686,36.5305 -19.6703,0 1.6861,-34.1419 -1.6861,-62.8043 50.8616,0 -1.5455,15.0337"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10225"
+           style="font-family:Acme;-inkscape-font-specification:Acme Bold"
+           d="m 3136.2567,-2014.3148 -29.9268,0 -0.843,25.8523 26.8358,0 -1.5456,14.8932 -25.7117,0 -0.1405,4.6365 1.686,36.5305 -19.6703,0 1.6861,-34.1419 -1.6861,-62.8043 50.8616,0 -1.5455,15.0337"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10227"
+           style="font-family:Acme;-inkscape-font-specification:Acme Bold"
+           d="m 3177.4808,-1987.9005 1.686,0 10.3971,-19.1082 10.5376,-23.1828 17.1412,2.5291 -12.9261,23.4637 -18.2652,35.6874 1.5455,36.109 -19.6702,0 1.405,-33.7204 -17.7032,-35.9684 -13.4882,-24.5878 19.8107,-3.5126 9.1326,21.3563 10.3972,20.9347"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="text3010"
+         style="font-size:62.54807663px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Acme;-inkscape-font-specification:Acme"
+         transform="scale(1.1647576,0.85854772)">
+        <path
+           id="path10175"
+           d="m 2645.5222,-2090.4097 0.688,-7.1931 29.7104,0 -0.688,7.1931 -10.8834,-0.9382 -0.2502,20.6408 0.7506,16.2625 -8.7568,0 0.7506,-15.1992 -0.2502,-21.6416 -11.071,0.8757"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10177"
+           d="m 2697.8329,-2070.2692 c 0,-2.8772 -0.2502,-4.7954 -0.7505,-5.7545 -0.5005,-0.959 -1.668,-1.4385 -3.5027,-1.4386 -1.8348,1e-4 -3.9197,0.7298 -6.2548,2.1892 l 0,4.566 0.688,14.6988 -8.444,1.5637 0.7506,-15.1992 -0.7506,-31.6493 8.5691,-1.5637 -0.8131,16.2 0,7.3181 c 0.6254,-0.834 1.3343,-1.6471 2.1266,-2.4394 0.7923,-0.7922 2.0224,-1.6888 3.6903,-2.6896 1.668,-1.0007 3.4401,-1.5011 5.3166,-1.5011 1.9181,0 3.5235,0.7506 4.8162,2.2517 1.2926,1.4595 1.939,3.461 1.939,6.0046 l -0.3753,7.0054 0.688,15.0116 -8.4439,1.5637 0.7505,-16.1374"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10179"
+           d="m 2723.6897,-2060.8245 c 1.2093,0 2.4811,-0.3127 3.8155,-0.9382 1.3343,-0.6255 2.3768,-1.251 3.1274,-1.8764 l 1.1258,-0.9383 2.8772,3.5027 c -0.417,0.7089 -1.0633,1.522 -1.939,2.4394 -0.8757,0.9174 -1.7722,1.7096 -2.6895,2.3768 -0.8757,0.6255 -2.0641,1.2093 -3.5653,1.7514 -1.4594,0.5004 -2.9814,0.7505 -4.566,0.7505 -3.3776,0 -6.1297,-1.2718 -8.2563,-3.8154 -2.1267,-2.5853 -3.19,-5.9003 -3.19,-9.9451 0,-5.0873 1.4803,-9.549 4.4409,-13.3853 2.9606,-3.8363 6.4216,-5.7544 10.383,-5.7544 3.044,0 5.4,0.8548 7.068,2.5644 1.7096,1.7097 2.5644,4.1074 2.5644,7.1931 0,1.8347 -0.3127,3.9197 -0.9382,6.2548 l -1.251,1.3135 -15.637,1.4386 c 0.7089,4.7119 2.9189,7.0679 6.6301,7.0679 m 0,-19.3899 c -1.8347,0 -3.3776,0.7506 -4.6285,2.2517 -1.251,1.4595 -1.9807,3.336 -2.1892,5.6294 l 10.6332,-1.3135 c 0.125,-0.9591 0.1876,-1.7514 0.1876,-2.3769 0,-2.7938 -1.3344,-4.1907 -4.0031,-4.1907"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10181"
+           d="m 2760.1934,-2097.9155 -0.6881,20.3907 15.9498,-20.2656 6.3799,4.2533 -14.3861,16.7003 15.2618,18.0764 -8.0062,5.0038 -15.2617,-21.579 -0.1251,4.6285 0.7506,16.2625 -8.7568,0 0.7506,-15.1992 -0.7506,-26.708 8.8819,-1.5637"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10183"
+           d="m 2793.6018,-2054.4446 c -2.2517,0 -4.0864,-0.7297 -5.5042,-2.1892 -1.376,-1.5011 -2.0641,-3.461 -2.0641,-5.8795 l 0.3753,-7.193 -0.688,-15.0115 8.3814,-1.5638 -0.8131,19.0147 c 0,1.793 0.3127,3.1065 0.9382,3.9405 0.6672,0.834 1.7097,1.251 3.1274,1.251 1.4178,0 3.3776,-0.7089 5.8796,-2.1267 l 0,-3.7529 -0.6881,-16.4501 8.444,-1.5637 -0.7506,15.637 0.063,5.129 c 0,2.6687 0.688,5.3582 2.0641,8.0687 l -6.505,2.8772 c -1.3761,-2.7104 -2.2101,-4.8788 -2.502,-6.505 -3.7529,4.2115 -7.0053,6.3173 -9.7575,6.3173"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10185"
+           d="m 2836.1199,-2070.2692 c 0,-2.8772 -0.2502,-4.7954 -0.7506,-5.7545 -0.5004,-0.959 -1.668,-1.4385 -3.5027,-1.4386 -1.8347,1e-4 -3.9197,0.7298 -6.2548,2.1892 l 0,4.566 0.688,14.6988 -8.3814,1.5637 0.7506,-15.1992 c -0.1668,-5.5876 -0.6255,-10.5914 -1.3761,-15.0115 l 7.5683,-1.6263 c 0.2919,2.4186 0.5004,4.7746 0.6255,7.068 0.6255,-0.834 1.3344,-1.6471 2.1266,-2.4394 0.7923,-0.8339 2.0433,-1.7513 3.7529,-2.7521 1.7097,-1.0424 3.5027,-1.5637 5.3792,-1.5637 1.9181,0 3.5235,0.7506 4.8162,2.2517 1.2926,1.4595 1.9389,3.461 1.939,6.0046 l -0.3753,7.0054 0.688,15.0116 -8.444,1.5637 0.7506,-16.1374"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10187"
+           d="m 2848.5318,-2048.2523 c 0,-3.6695 3.4193,-6.5467 10.2579,-8.6317 -1.6263,-0.8339 -2.4394,-1.9181 -2.4394,-3.2525 0,-0.542 0.2085,-1.0841 0.6255,-1.6262 0.4169,-0.5421 1.0216,-1.105 1.8139,-1.6888 l 0,-0.5004 c -3.0024,0 -5.4,-0.834 -7.1931,-2.5019 -1.7513,-1.6679 -2.627,-3.8988 -2.627,-6.6927 0,-3.6277 1.4803,-6.776 4.4409,-9.4447 2.9606,-2.7104 6.4633,-4.0656 10.5081,-4.0656 1.0425,0 2.5645,0.2085 4.566,0.6254 2.0015,0.4171 3.9614,0.6256 5.8795,0.6255 l 4.8162,0 -0.6254,5.4417 -4.6912,-0.5629 c 0.417,1.2093 0.6255,2.5853 0.6255,4.1281 0,1.5429 -0.3753,3.0858 -1.1258,4.6286 -0.7089,1.5012 -1.5846,2.7104 -2.6271,3.6278 -1.0007,0.9174 -2.0224,1.7305 -3.0648,2.4394 -1.0008,0.6672 -1.8765,1.2718 -2.627,1.8139 -0.7089,0.5004 -1.0634,0.959 -1.0633,1.376 -1e-4,0.7923 0.8339,1.4803 2.5019,2.0641 4.42,1.7931 7.4015,3.3151 8.9443,4.566 1.5846,1.251 2.3768,2.7938 2.3769,4.6286 -10e-5,3.0857 -1.5638,5.6084 -4.6911,7.5683 -3.0858,2.0015 -6.7761,3.0023 -11.071,3.0023 -4.295,0 -7.6309,-0.688 -10.0077,-2.0641 -2.3352,-1.3761 -3.5027,-3.2108 -3.5027,-5.5042 m 18.8895,-26.2702 c 0,-2.0432 -0.4587,-3.5444 -1.3761,-4.5035 -0.9174,-0.959 -2.356,-1.4386 -4.3158,-1.4386 -3.7946,0 -5.6919,1.8765 -5.6919,5.6293 0,3.7112 2.0224,5.5668 6.0672,5.5668 3.5444,0 5.3166,-1.7513 5.3166,-5.254 m 2.2517,24.2061 c 0,-0.7506 -0.3961,-1.4595 -1.1884,-2.1266 -0.7923,-0.6255 -2.21,-1.3761 -4.2533,-2.2518 -3.044,0.834 -5.1498,1.6054 -6.3173,2.3143 -1.1259,0.7089 -1.6888,1.5846 -1.6888,2.627 0,1.0425 0.5629,1.8765 1.6888,2.5019 1.1675,0.6672 2.7729,1.0008 4.8162,1.0008 2.0432,0 3.7112,-0.3753 5.0038,-1.1258 1.2927,-0.7506 1.939,-1.7305 1.939,-2.9398"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10189"
+           d="m 2915.4651,-2090.9101 -13.3228,0 -0.3753,11.5088 11.9467,0 -0.688,6.6301 -11.4463,0 -0.063,2.0641 0.7506,16.2625 -8.7567,0 0.7506,-15.1992 -0.7506,-27.959 22.6424,0 -0.688,6.6927"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10191"
+           d="m 2927.2495,-2054.4446 c -2.2517,0 -4.0865,-0.7297 -5.5042,-2.1892 -1.3761,-1.5011 -2.0641,-3.461 -2.0641,-5.8795 l 0.3753,-7.193 -0.6881,-15.0115 8.3815,-1.5638 -0.8131,19.0147 c -10e-5,1.793 0.3127,3.1065 0.9382,3.9405 0.6671,0.834 1.7096,1.251 3.1274,1.251 1.4177,0 3.3776,-0.7089 5.8795,-2.1267 l 0,-3.7529 -0.688,-16.4501 8.444,-1.5637 -0.7506,15.637 0.063,5.129 c 0,2.6687 0.688,5.3582 2.0641,8.0687 l -6.505,2.8772 c -1.3761,-2.7104 -2.21,-4.8788 -2.5019,-6.505 -3.7529,4.2115 -7.0054,6.3173 -9.7575,6.3173"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="text3016"
+         style="font-size:55.5345993px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Acme;-inkscape-font-specification:Acme"
+         transform="scale(1.0640965,0.93976441)">
+        <path
+           id="path10194"
+           d="m 2910.1662,-1832.5692 -3.1655,0 0,0.3332 0.6664,14.439 -7.4416,0 0.6664,-13.4949 -0.6664,-24.8239 12.051,0 c 3.5542,0 6.3864,0.9626 8.4968,2.8878 2.1103,1.9252 3.1654,4.4798 3.1655,7.6637 -10e-5,2.4066 -0.6295,4.6094 -1.8882,6.6087 -1.2218,1.9622 -2.8878,3.5172 -4.9981,4.6649 l 9.663,13.6615 -6.7197,3.4431 -8.6634,-15.4386 c -0.2592,0.037 -0.6479,0.056 -1.1662,0.056 m 0.111,-17.9376 -2.7212,0 -0.3887,12.5508 3.7764,0 c 1.8511,0 3.2765,-0.6109 4.2761,-1.8327 1.0366,-1.2217 1.555,-2.9618 1.555,-5.2202 0,-3.6653 -2.1659,-5.4979 -6.4976,-5.4979"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10196"
+           d="m 2941.1059,-1823.4615 c 1.0736,0 2.2028,-0.2777 3.3876,-0.833 1.1847,-0.5553 2.1103,-1.1107 2.7767,-1.666 l 0.9997,-0.8331 2.5545,3.11 c -0.3702,0.6294 -0.9441,1.3513 -1.7215,2.1658 -0.7775,0.8145 -1.5735,1.518 -2.388,2.1103 -0.7775,0.5554 -1.8327,1.0737 -3.1655,1.555 -1.2958,0.4443 -2.6471,0.6664 -4.054,0.6664 -2.9989,0 -5.4424,-1.1292 -7.3306,-3.3876 -1.8882,-2.2954 -2.8322,-5.2387 -2.8322,-8.83 0,-4.5168 1.3143,-8.4783 3.9429,-11.8844 2.6286,-3.4061 5.7016,-5.1092 9.2188,-5.1092 2.7026,0 4.7944,0.759 6.2754,2.2769 1.5179,1.518 2.2769,3.6468 2.2769,6.3865 0,1.6291 -0.2777,3.4802 -0.833,5.5535 l -1.1107,1.1662 -13.8837,1.2773 c 0.6294,4.1836 2.5916,6.2754 5.8867,6.2754 m 0,-17.2157 c -1.629,0 -2.9989,0.6664 -4.1096,1.9992 -1.1107,1.2959 -1.7586,2.9619 -1.9437,4.9981 l 9.4409,-1.1662 c 0.1111,-0.8515 0.1666,-1.5549 0.1666,-2.1103 0,-2.4805 -1.1847,-3.7208 -3.5542,-3.7208"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10198"
+           d="m 2965.2808,-1824.7388 0.7219,0 1.8327,-7.6082 2.9989,-13.7171 6.8862,0.833 -4.165,13.7726 -3.4987,12.9951 -9.4964,0.833 -3.2766,-13.4949 -3.9985,-13.4949 7.5527,-1.3883 2.388,12.2176 2.0548,9.0521"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10200"
+           d="m 2988.8795,-1846.0641 -0.6664,13.8281 0.6109,13.0507 -7.4416,1.3883 0.6664,-13.4949 -0.6109,-13.3838 7.4416,-1.3884 m -7.8303,-8.108 c 0,-1.2588 0.4812,-2.3695 1.4439,-3.3321 0.9625,-0.9996 2.0732,-1.4994 3.332,-1.4994 1.2588,0 2.2214,0.3517 2.8878,1.0551 0.6664,0.6665 0.9996,1.6846 0.9997,3.0544 -10e-5,1.3699 -0.4814,2.5547 -1.4439,3.5542 -0.9256,0.9627 -2.0178,1.444 -3.2766,1.4439 -1.2218,10e-5 -2.1844,-0.4072 -2.8878,-1.2217 -0.7034,-0.8145 -1.0551,-1.8326 -1.0551,-3.0544"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10202"
+           d="m 3004.9924,-1823.4615 c 1.0736,0 2.2028,-0.2777 3.3876,-0.833 1.1847,-0.5553 2.1103,-1.1107 2.7767,-1.666 l 0.9996,-0.8331 2.5546,3.11 c -0.3702,0.6294 -0.9441,1.3513 -1.7215,2.1658 -0.7775,0.8145 -1.5735,1.518 -2.388,2.1103 -0.7775,0.5554 -1.8327,1.0737 -3.1655,1.555 -1.2958,0.4443 -2.6472,0.6664 -4.054,0.6664 -2.9989,0 -5.4424,-1.1292 -7.3306,-3.3876 -1.8882,-2.2954 -2.8323,-5.2387 -2.8323,-8.83 0,-4.5168 1.3144,-8.4783 3.943,-11.8844 2.6286,-3.4061 5.7015,-5.1092 9.2187,-5.1092 2.7027,0 4.7945,0.759 6.2754,2.2769 1.518,1.518 2.2769,3.6468 2.277,6.3865 -1e-4,1.6291 -0.2777,3.4802 -0.8331,5.5535 l -1.1106,1.1662 -13.8837,1.2773 c 0.6294,4.1836 2.5916,6.2754 5.8867,6.2754 m 0,-17.2157 c -1.6291,0 -2.9989,0.6664 -4.1096,1.9992 -1.1107,1.2959 -1.7586,2.9619 -1.9437,4.9981 l 9.4409,-1.1662 c 0.111,-0.8515 0.1666,-1.5549 0.1666,-2.1103 0,-2.4805 -1.1848,-3.7208 -3.5542,-3.7208"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10204"
+           d="m 3044.4948,-1824.7388 0.722,0 1.7771,-7.6082 2.8878,-13.7171 6.7752,0.833 -4.054,13.7726 -3.3876,12.9951 -9.1077,0.833 -3.0544,-13.4949 -0.2777,-0.8885 -3.7208,13.5504 -8.83,0.833 -3.221,-13.4949 -3.8319,-13.4949 7.275,-1.3883 2.277,12.2176 1.9992,9.0521 0.7219,0 4.776,-15.8273 -1.1662,-4.0541 7.164,-1.3883 2.3324,12.2176 1.9437,9.0521"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10206"
+           d="m 3081.9121,-1817.1861 c -4.1095,0 -7.5712,-1.7216 -10.3849,-5.1647 -2.8138,-3.4431 -4.2207,-7.6638 -4.2207,-12.6619 0,-6.1088 1.6846,-11.255 5.0537,-15.4386 3.4061,-4.1836 7.5712,-6.2754 12.4953,-6.2754 1.9251,0 3.7022,0.2037 5.3313,0.6109 1.666,0.3702 2.8507,0.759 3.5542,1.1662 l 0.9996,0.6109 -3.7763,7.7193 c -0.8516,-0.7404 -2.0548,-1.4254 -3.6098,-2.0548 -1.555,-0.6664 -2.9618,-0.9996 -4.2206,-0.9996 -2.5546,0 -4.6094,1.0552 -6.1644,3.1655 -1.5179,2.1103 -2.2769,5.1277 -2.2769,9.0521 0,3.8874 0.796,7.0899 2.388,9.6075 1.592,2.4805 3.7578,3.7208 6.4976,3.7208 1.7771,0 3.3876,-0.5553 4.8315,-1.666 1.4438,-1.1107 2.462,-2.5916 3.0544,-4.4428 l 4.6093,2.6656 c -1.2217,3.2581 -3.0914,5.8127 -5.6089,7.6638 -2.5176,1.8142 -5.3684,2.7212 -8.5524,2.7212"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10208"
+           d="m 3106.1131,-1817.797 c -1.9993,0 -3.6283,-0.6479 -4.8871,-1.9437 -1.2217,-1.3328 -1.8326,-3.0729 -1.8326,-5.2202 l 0.3332,-6.3865 -0.6109,-13.3283 7.4416,-1.3884 -0.7219,16.8825 c 0,1.592 0.2777,2.7583 0.833,3.4987 0.5924,0.7405 1.5179,1.1107 2.7767,1.1107 1.2588,0 2.9989,-0.6294 5.2203,-1.8882 l 0,-3.332 -0.6109,-14.6056 7.4972,-1.3884 -0.6664,13.8837 0.055,4.5538 c 0,2.3695 0.6109,4.7575 1.8326,7.164 l -5.7756,2.5545 c -1.2217,-2.4065 -1.9622,-4.3317 -2.2213,-5.7756 -3.3321,3.7394 -6.2199,5.609 -8.6634,5.609"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10210"
+           d="m 3137.3105,-1839.1778 c -1.629,0 -2.8878,0.5924 -3.7764,1.7771 -0.8885,1.1478 -1.3328,2.9249 -1.3328,5.3313 0,2.4066 0.4443,4.3503 1.3328,5.8312 0.9256,1.4439 2.2584,2.1658 3.9985,2.1658 0.8886,0 1.7586,-0.2776 2.6102,-0.833 0.8515,-0.5553 1.4994,-1.1107 1.9437,-1.666 l 0.6109,-0.8886 2.7767,2.8878 c -0.1111,0.1851 -0.2592,0.4443 -0.4443,0.7775 -0.1481,0.3332 -0.5554,0.9256 -1.2218,1.7771 -0.6294,0.8515 -1.3143,1.6105 -2.0547,2.2769 -0.7035,0.6294 -1.6291,1.2218 -2.7768,1.7771 -1.1477,0.5184 -2.3509,0.7775 -3.6097,0.7775 -2.7768,0 -5.0352,-1.1292 -6.7752,-3.3876 -1.7401,-2.2584 -2.6102,-5.2017 -2.6102,-8.83 0,-4.5168 1.3144,-8.4783 3.943,-11.8844 2.6286,-3.4061 5.7015,-5.1092 9.2187,-5.1092 1.0737,0 2.1474,0.1667 3.221,0.4998 1.0737,0.2962 2.1659,0.7775 3.2766,1.4439 l -3.8874,6.8308 c -1.3329,-1.0366 -2.8138,-1.555 -4.4428,-1.555"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10212"
+           d="m 3166.4532,-1817.2416 -10.4406,-15.3831 0,0.3887 0.6109,13.0507 -7.4971,1.3883 0.6664,-13.4949 -0.6664,-28.1005 7.6082,-1.3883 -0.722,26.8232 11.107,-12.1621 5.2202,3.5542 -9.7185,9.1077 10.1628,11.9955 -6.3309,4.2206"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10214"
+           d="m 3174.2514,-1829.4037 c 0,-4.7389 1.2773,-8.7559 3.8319,-12.051 2.5546,-3.295 5.5349,-4.9426 8.9411,-4.9426 3.4431,0 6.1088,1.1107 7.997,3.3321 1.9251,2.2214 2.8877,5.1647 2.8877,8.83 0,4.8871 -1.2402,8.9596 -3.7208,12.2176 -2.4435,3.221 -5.5349,4.8315 -9.2742,4.8315 -3.073,0 -5.6276,-1.1477 -7.6638,-3.4431 -1.9993,-2.3325 -2.9989,-5.2573 -2.9989,-8.7745 m 17.4379,-1.9437 c 0,-5.3683 -1.8327,-8.0525 -5.498,-8.0525 -3.8133,0 -5.72,2.5176 -5.72,7.5527 0,2.6286 0.5368,4.6834 1.6105,6.1643 1.0736,1.481 2.4805,2.2214 4.2206,2.2214 1.7771,0 3.1099,-0.6664 3.9985,-1.9992 0.9256,-1.3699 1.3884,-3.3321 1.3884,-5.8867"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path10216"
+           d="m 3201.0425,-1829.4037 c 0,-4.7389 1.2773,-8.7559 3.8319,-12.051 2.5546,-3.295 5.5349,-4.9426 8.9411,-4.9426 3.4431,0 6.1088,1.1107 7.997,3.3321 1.9251,2.2214 2.8877,5.1647 2.8878,8.83 -10e-5,4.8871 -1.2403,8.9596 -3.7209,12.2176 -2.4435,3.221 -5.5349,4.8315 -9.2742,4.8315 -3.073,0 -5.6276,-1.1477 -7.6638,-3.4431 -1.9993,-2.3325 -2.9989,-5.2573 -2.9989,-8.7745 m 17.4379,-1.9437 c 0,-5.3683 -1.8327,-8.0525 -5.4979,-8.0525 -3.8134,0 -5.7201,2.5176 -5.7201,7.5527 0,2.6286 0.5368,4.6834 1.6105,6.1643 1.0737,1.481 2.4805,2.2214 4.2206,2.2214 1.7771,0 3.1099,-0.6664 3.9985,-1.9992 0.9256,-1.3699 1.3884,-3.3321 1.3884,-5.8867"
+           inkscape:connector-curvature="0" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/contrib/themes/diffy/static/diffymute.svg b/contrib/themes/diffy/static/diffymute.svg
new file mode 100644
index 0000000..6833ba6
--- /dev/null
+++ b/contrib/themes/diffy/static/diffymute.svg
@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="600.06732"
+   height="558.20709"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.3.1 r9886"
+   sodipodi:docname="diffy.svg"
+   inkscape:export-filename="/home/sarah/art/diffy.png"
+   inkscape:export-xdpi="500"
+   inkscape:export-ydpi="500">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.66618777"
+     inkscape:cx="300.03365"
+     inkscape:cy="258.35521"
+     inkscape:document-units="px"
+     inkscape:current-layer="g10119"
+     showgrid="false"
+     inkscape:window-width="1366"
+     inkscape:window-height="744"
+     inkscape:window-x="0"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-3069.8852,2251.5084)">
+    <text
+       xml:space="preserve"
+       style="font-size:51.65934372px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:end;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeSans;-inkscape-font-specification:FreeSans Bold"
+       x="475.83173"
+       y="900.47076"
+       id="text4257"
+       sodipodi:linespacing="125%"
+       transform="scale(1.1411753,0.87628955)"><tspan
+         sodipodi:role="line"
+         x="475.83173"
+         y="900.47076"
+         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:end;text-anchor:end;font-family:Acme;-inkscape-font-specification:Acme Bold"
+         id="tspan3008" /></text>
+    <flowRoot
+       xml:space="preserve"
+       id="flowRoot4263"
+       style="font-size:12px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
+         id="flowRegion4265"><rect
+           id="rect4267"
+           width="313.14728"
+           height="343.45187"
+           x="-335.37064"
+           y="205.85435" /></flowRegion><flowPara
+         id="flowPara4269" /></flowRoot>    <g
+       id="g10119"
+       transform="matrix(0.99201906,-0.12608805,0.12608805,0.99201906,273.85438,408.58042)">
+      <path
+         sodipodi:nodetypes="csccscac"
+         inkscape:connector-curvature="0"
+         id="path4130"
+         d="m 3378.9965,-2206.2972 c 0,0 13.4039,-8.8398 27.0022,-17.1718 13.5984,-8.3319 22.2322,-21.276 22.2322,-21.276 l 34.2799,28.1467 c 0,0 -16.0641,15.553 -24.7742,21.6893 -3.709,2.6129 -10.9463,9.1577 -10.9463,9.1577 0,0 -14.3617,-10.0769 -22.3623,-13.5225 -8.0773,-3.4786 -25.4315,-7.0234 -25.4315,-7.0234 z"
+         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         sodipodi:nodetypes="cac"
+         inkscape:connector-curvature="0"
+         d="m 3414.7387,-2204.9828 c 0,0 8.2233,-5.8571 11.9326,-9.2622 3.8224,-3.5089 10.5596,-11.437 10.5596,-11.437"
+         style="fill:#ffffff;stroke:#ff0000;stroke-width:8.97858429;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+         id="path4185" />
+      <path
+         sodipodi:nodetypes="cascacsc"
+         inkscape:connector-curvature="0"
+         id="path3307"
+         d="m 3247.6516,-2171.4037 c 0,0 4.3348,-19.2835 10.7071,-26.2701 7.4284,-8.1446 19.072,-14.1542 29.3495,-15.24 27.8177,-2.9386 63.2371,6.1908 63.2371,6.1908 0,0 -30.8042,0.7262 -45.6022,4.5134 -5.1922,1.3289 -14.7941,6.2977 -14.7941,6.2977 0,0 -16.1166,0.8374 -29.8971,12.0927 -13.7804,11.2552 -3.0344,9.7831 -3.0344,9.7831"
+         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         sodipodi:nodetypes="ccsccscscccccscc"
+         inkscape:connector-curvature="0"
+         id="path3011"
+         d="m 3332.9985,-2012.4352 -20.0742,24.9886 c 0,0 -16.3479,-5.082 -40.3255,4.9725 -23.9776,10.0546 -39.2193,26.4042 -39.2193,26.4042 l 7.8897,-0.7975 c 0,0 20.4818,-14.4371 31.9614,-17.1294 11.4796,-2.6923 23.6197,-2.8388 23.6197,-2.8388 0,0 -25.492,10.0264 -30.7497,12.3759 -6.2584,2.7968 -15.0874,9.199 -15.0874,9.199 l 10.4761,-0.2965 c 17.6867,-7.2528 41.3221,-16.0187 59.9685,-15.047 15.5919,1.1274 33.9405,16.7543 33.9405,16.7543 l -3.8165,-9.9895 c 0,0 -10.8307,-8.1932 -16.3866,-13.7491 -5.5558,-5.5558 17.5848,-25.8193 17.5848,-25.8193 -14.3098,-2.8351 -2.1172,-7.9424 -19.7815,-9.0274 z"
+         style="fill:#ffff00;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         style="fill:#ffff00;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+         d="m 3397.9929,-1984.0461 -15.5774,33.0846 c 0,0 -14.4499,-2.2165 -39.5097,4.7139 -25.0597,6.9303 -44.9575,20.0476 -44.9575,20.0476 l 10.5706,1.822 c 0,0 19.7276,-11.1182 30.516,-14.1955 7.747,-2.2097 23.9934,-2.9008 23.9934,-2.9008 0,0 -20.2687,6.6869 -30.057,10.9033 -5.4067,2.329 -15.7817,7.9273 -15.7817,7.9273 l 10.8391,1.5269 c 0,0 31.056,-11.8625 47.4047,-13.3504 11.1103,-1.0112 11.8331,-2.962 33.324,3.1079 10.0509,2.8388 16.3928,6.252 16.3928,6.252 l -3.3122,-5.8696 c 0,0 -5.3909,-4.5711 -8.2712,-6.6111 -3.5982,-2.5484 -11.2202,-7.0059 -11.2202,-7.0059 l 15.4484,-30.858 c -13.3255,-5.9362 -2.8177,-3.6211 -19.8021,-8.5942 z"
+         id="path3013"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="ccsccacaccasccaccc" />
+      <path
+         sodipodi:nodetypes="aaczsccsssca"
+         inkscape:connector-curvature="0"
+         id="path3014"
+         d="m 3138.8926,-2124.2277 c 9.8375,-20.5358 27.6337,-38.9533 48.4705,-48.136 22.7321,-10.018 51.1582,-10.0879 74.4382,-3.5941 7.6944,6.5 10.3115,-17.1804 46.9059,-27.4208 36.5944,-10.2403 91.6429,-6.509 142.1505,22.7854 54.0494,31.3487 -23.7386,41.9213 -8.0812,94.4492 32.3844,113.578 -63.1345,101.5204 -63.1345,101.5204 0,0 -25.2539,-16.1625 -35.3554,-17.1726 -10.1015,-1.0102 -46.6429,-25.1251 -68.8662,-28.6607 -22.2234,-3.5355 -36.9003,18.6222 -107.991,-9.9282 -24.3178,-9.7661 -33.8401,-33.335 -33.8401,-33.335 0,0 -2.0102,-35.2405 5.3033,-50.5076 z"
+         style="fill:#0000ff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         sodipodi:nodetypes="cssscscsac"
+         inkscape:connector-curvature="0"
+         id="path3028"
+         d="m 3179.7636,-2035.4822 c 0,0 33.988,-7.9036 57.2215,-14.9746 23.2335,-7.0711 36.1637,-43.7474 71.519,-48.7981 35.3554,-5.0508 68.528,22.1624 80.6498,47.4162 12.1218,25.2539 50.6701,55.6193 50.6701,55.6193 0,0 1.0101,11.1117 -14.1422,18.1828 -15.1522,7.0711 -10.4989,7.6628 -10.4989,7.6628 0,0 -43.1103,-11.4139 -62.396,-21.4139 -19.2857,-10 -61.236,-23.044 -92.9377,-30.7963 -26.2653,-6.4228 -35.0334,2.5255 -80.0856,-12.8982 z"
+         style="fill:#ff6600;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         sodipodi:nodetypes="cscccccscc"
+         inkscape:connector-curvature="0"
+         id="path3009"
+         d="m 3470.1209,-2002.8128 c 0,0 13.8937,99.0002 33.6829,157.8517 19.7893,58.8514 78.4464,180.4742 78.4464,180.4742 l 28.1031,-31.8521 -52.5625,-109.7203 60.5152,95.9016 26.0124,-35.7271 c 0,0 -70.663,-88.4878 -93.8954,-140.4597 -23.2324,-51.9718 -37.4945,-152.9082 -37.4945,-152.9082 z"
+         style="fill:#0000ff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         sodipodi:nodetypes="csscsccccccccccsc"
+         inkscape:connector-curvature="0"
+         id="path3016"
+         d="m 3359.2154,-2191.502 c 0,0 -40.7142,5 -57.1428,42.1428 -16.4286,37.1429 50.7143,115.7143 60.7143,132.1429 10,16.4286 24.2857,38.5714 24.2857,38.5714 0,0 47.6766,32.0335 82.9062,53.8444 32.6567,20.2179 65.2266,33.1109 65.2266,33.1109 0,0 1.1687,-0.4937 7.0265,-19.1963 -12.1218,-21.7183 -38.3216,-53.2543 -54.5036,-69.192 24.2792,14.1167 43.346,28.1254 60.1041,56.5482 0,0 4.9588,-21.3916 6.5659,-30.7894 -3.0357,-9.5763 -47.4771,-56.5685 -47.4771,-56.5685 20.5517,8.7613 33.7508,23.0255 50.0892,44.7732 0,0 3.3263,-21.5598 3.8366,-32.9472 -2.5253,-6.566 -33.7228,-35.0595 -33.7228,-35.0595 13.1654,6.6374 19.714,11.0661 34.4127,24.0476 0,0 -3.0357,-60.7142 -66.6072,-130.7142 -63.5714,-70 -135.7143,-50.7143 -135.7143,-50.7143 z"
+         style="fill:#000080;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <g
+         transform="translate(2935.644,-2513.1499)"
+         id="g3138">
+        <path
+           style="fill:#ff0000;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+           d="m 337.66259,394.0585 c 0,0 -28.86328,-30.61257 -60.78781,-25.80202 -31.92454,4.81054 -41.10831,33.67382 -39.79634,39.79633 1.31197,6.12252 13.557,11.80771 22.30345,15.74361 8.74644,3.9359 32.79917,-0.43732 45.48152,-6.55984 12.68235,-6.12251 32.79918,-23.17808 32.79918,-23.17808 z"
+           id="path3022"
+           inkscape:connector-curvature="0" />
+        <path
+           style="fill:#ffff00;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-miterlimit:4"
+           d="m 286.65383,372.49887 c 12.50235,2.18615 20.86528,14.09354 18.67912,26.59589 -2.18615,12.50234 -14.09354,20.86527 -26.59589,18.67911 -12.50235,-2.18616 -20.86528,-14.09354 -18.67912,-26.59588 z"
+           id="path3024"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="csscc" />
+        <path
+           sodipodi:type="arc"
+           style="fill:#000000;fill-opacity:1;stroke:none"
+           id="path3026"
+           sodipodi:cx="229.64285"
+           sodipodi:cy="403.79074"
+           sodipodi:rx="8.2142859"
+           sodipodi:ry="8.2142859"
+           d="m 237.85714,403.79074 c 0,4.53663 -3.67766,8.21429 -8.21429,8.21429 -4.53662,0 -8.21428,-3.67766 -8.21428,-8.21429 0,-4.53662 3.67766,-8.21428 8.21428,-8.21428 4.53663,0 8.21429,3.67766 8.21429,8.21428 z"
+           transform="matrix(1.5694327,0,0,1.5694327,-85.566867,-233.30737)" />
+      </g>
+      <path
+         sodipodi:nodetypes="cssssssc"
+         style="fill:#ffff00;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+         d="m 3110.2692,-2103.8575 c -32.3249,20.7081 -31.4522,56.0845 -27.4116,60.6301 4.0406,4.5457 8.0812,1.0102 15.6574,-1.5152 7.5761,-2.5254 26.2639,-10.1015 42.4264,-11.1117 16.1624,-1.0101 21.2132,3.0305 23.7386,-0.505 2.5254,-3.5356 4.0406,-15.1523 3.0304,-25.7589 -1.0101,-10.6066 -1.0101,-23.2335 -7.071,-25.2538 -6.061,-2.0203 -38.2484,-5.0718 -50.3702,3.5145 z"
+         id="path3296"
+         inkscape:connector-curvature="0" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path3305"
+         d="m 3251.1404,-2178.2729 c 0,0 12.1428,21.0714 67.1428,32.5 55,11.4286 125.7143,-38.2143 125.7143,-38.2143 l 38.7857,38.6429 c 0,0 -113.0714,25.6428 -150.2143,21 -37.1428,-4.6429 -81.4285,-50.3572 -81.4285,-50.3572"
+         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         sodipodi:nodetypes="ccccc"
+         inkscape:connector-curvature="0"
+         id="path4099"
+         d="m 3453.1169,-2166.1191 -30.2186,14.3667 6.8378,5.5184 29.9689,-14.0733 z"
+         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
+      <path
+         sodipodi:nodetypes="cacscacsc"
+         inkscape:connector-curvature="0"
+         id="path3303"
+         d="m 3154.8351,-2149.0637 c 0,0 -6.1891,7.2925 -8.7928,11.2888 -2.8625,4.3936 -7.4472,13.8569 -7.4472,13.8569 0,0 49.8667,-24.0831 67.6065,-30.2972 18.2787,-6.4028 54.803,-21.7873 54.803,-21.7873 0,0 -3.8451,-1.8876 -5.9103,-2.3872 -3.1018,-0.7503 -9.5343,-0.868 -9.5343,-0.868 0,0 -25.7837,-0.1235 -42.8032,6.1343 -17.0196,6.2579 -47.9217,24.0597 -47.9217,24.0597 z"
+         style="fill:#ffffff;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+      <path
+         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
+         d="m 3433.0379,-2169.2212 24.4739,22.8165 -8.1844,3.1976 -24.3243,-22.4616 z"
+         id="path27241"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="ccccc" />
+    </g>
+  </g>
+</svg>
diff --git a/gerrit-acceptance-framework/BUCK b/gerrit-acceptance-framework/BUCK
index d8f0276..ba68fa3 100644
--- a/gerrit-acceptance-framework/BUCK
+++ b/gerrit-acceptance-framework/BUCK
@@ -2,14 +2,19 @@
 
 DEPS = [
   '//gerrit-gpg:gpg',
+  '//gerrit-launcher:launcher',
+  '//gerrit-openid:openid',
   '//gerrit-pgm:daemon',
+  '//gerrit-pgm:http-jetty',
   '//gerrit-pgm:util-nodep',
+  '//gerrit-server/src/main/prolog:common',
   '//gerrit-server:testutil',
   '//lib/auto:auto-value',
   '//lib/httpcomponents:fluent-hc',
   '//lib/httpcomponents:httpclient',
   '//lib/httpcomponents:httpcore',
-  '//lib/jgit:junit',
+  '//lib/jetty:servlet',
+  '//lib/jgit/org.eclipse.jgit.junit:junit',
   '//lib/log:impl_log4j',
   '//lib/log:log4j',
 ]
@@ -24,8 +29,8 @@
   '//gerrit-reviewdb:server',
   '//gerrit-server:server',
   '//lib:gson',
-  '//lib/jgit:jgit',
   '//lib:jsch',
+  '//lib/jgit/org.eclipse.jgit:jgit',
   '//lib/mina:sshd',
   '//lib:servlet-api-3_1',
 ]
@@ -80,8 +85,8 @@
     '//lib/guice:guice-servlet',
     '//lib/guice:javax-inject',
     '//lib:gwtorm_client',
-    '//lib:junit__jar',
-    '//lib:truth__jar',
+    '//lib:junit',
+    '//lib:truth',
   ],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-acceptance-framework/BUILD b/gerrit-acceptance-framework/BUILD
new file mode 100644
index 0000000..1439ba9
--- /dev/null
+++ b/gerrit-acceptance-framework/BUILD
@@ -0,0 +1,60 @@
+load('//tools/bzl:java.bzl', 'java_library2')
+
+SRCS = glob(['src/test/java/com/google/gerrit/acceptance/*.java'])
+
+DEPS = [
+  '//gerrit-gpg:gpg',
+  '//gerrit-launcher:launcher',
+  '//gerrit-openid:openid',
+  '//gerrit-pgm:daemon',
+  '//gerrit-pgm:http-jetty',
+  '//gerrit-pgm:util-nodep',
+  '//gerrit-server/src/main/prolog:common',
+  '//gerrit-server:testutil',
+  '//lib/auto:auto-value',
+  '//lib/httpcomponents:fluent-hc',
+  '//lib/httpcomponents:httpclient',
+  '//lib/httpcomponents:httpcore',
+  '//lib/jetty:servlet',
+  '//lib/jgit/org.eclipse.jgit.junit:junit',
+  '//lib/log:impl_log4j',
+  '//lib/log:log4j',
+]
+
+PROVIDED = [
+  '//gerrit-common:annotations',
+  '//gerrit-common:server',
+  '//gerrit-extension-api:api',
+  '//gerrit-httpd:httpd',
+  '//gerrit-lucene:lucene',
+  '//gerrit-pgm:init',
+  '//gerrit-reviewdb:server',
+  '//gerrit-server:server',
+  '//lib:gson',
+  '//lib:jsch',
+  '//lib/jgit/org.eclipse.jgit:jgit',
+  '//lib/mina:sshd',
+  '//lib:servlet-api-3_1',
+]
+
+java_binary(
+  name = 'acceptance-framework',
+  main_class = 'Dummy',
+  runtime_deps = [':lib'],
+  visibility = ['//visibility:public'],
+)
+
+java_library2(
+  name = 'lib',
+  srcs = SRCS,
+  exported_deps = DEPS + [
+    '//lib:truth',
+  ],
+  deps = PROVIDED + [ # We want these deps to be exported_deps
+    '//lib:gwtorm',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice-servlet',
+  ],
+  visibility = ['//visibility:public'],
+)
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
index 28f9527..4ba3720 100644
--- a/gerrit-acceptance-framework/pom.xml
+++ b/gerrit-acceptance-framework/pom.xml
@@ -2,10 +2,10 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>2.12.9</version>
+  <version>2.13.14</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
-  <description>API for Gerrit Plugins</description>
+  <description>Framework for Gerrit's acceptance tests</description>
   <url>https://www.gerritcodereview.com/</url>
 
   <licenses>
@@ -23,15 +23,24 @@
 
   <developers>
     <developer>
+      <name>Andrew Bonventre</name>
+    </developer>
+    <developer>
       <name>Dave Borowitz</name>
     </developer>
     <developer>
+      <name>David Ostrovsky</name>
+    </developer>
+    <developer>
       <name>David Pursehouse</name>
     </developer>
     <developer>
       <name>Edwin Kempin</name>
     </developer>
     <developer>
+      <name>Hugo Arès</name>
+    </developer>
+    <developer>
       <name>Martin Fick</name>
     </developer>
     <developer>
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 9194371..a21b7d0 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
@@ -16,12 +16,15 @@
 
 import static com.google.common.truth.Truth.assertThat;
 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 com.google.gerrit.server.project.Util.block;
+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;
@@ -33,6 +36,8 @@
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
+import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -42,35 +47,49 @@
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
+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.AnonymousUser;
+import com.google.gerrit.server.ChangeFinder;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Revisions;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 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.index.ChangeIndexer;
-import com.google.gerrit.server.notedb.NotesMigration;
+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;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.Util;
+import com.google.gerrit.server.query.change.ChangeData;
 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;
+import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -78,11 +97,16 @@
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
 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.Transport;
+import org.junit.After;
 import org.junit.AfterClass;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
@@ -95,6 +119,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Pattern;
@@ -128,7 +153,7 @@
   protected AccountCache accountCache;
 
   @Inject
-  private IdentifiedUser.GenericFactory identifiedUserFactory;
+  protected IdentifiedUser.GenericFactory identifiedUserFactory;
 
   @Inject
   protected PushOneCommit.Factory pushFactory;
@@ -169,18 +194,47 @@
   @GerritPersonIdent
   protected Provider<PersonIdent> serverIdent;
 
+  @Inject
+  protected ChangeData.Factory changeDataFactory;
+
+  @Inject
+  protected PatchSetUtil psUtil;
+
+  @Inject
+  protected ChangeFinder changeFinder;
+
+  @Inject
+  protected Revisions revisions;
+
+  @Inject
+  protected FakeEmailSender sender;
+
+  @Inject
+  protected ChangeNoteUtil changeNoteUtil;
+
+  @Inject
+  protected ChangeResource.Factory changeResourceFactory;
+
+  @Inject
+  private EventRecorder.Factory eventRecorderFactory;
+
   protected TestRepository<InMemoryRepository> testRepo;
   protected GerritServer server;
   protected TestAccount admin;
   protected TestAccount user;
-  protected RestSession adminSession;
-  protected RestSession userSession;
-  protected SshSession sshSession;
+  protected RestSession adminRestSession;
+  protected RestSession userRestSession;
+  protected SshSession adminSshSession;
+  protected SshSession userSshSession;
   protected ReviewDb db;
   protected Project.NameKey project;
+  protected EventRecorder eventRecorder;
 
   @Inject
-  protected NotesMigration notesMigration;
+  protected TestNotesMigration notesMigration;
+
+  @Inject
+  protected ChangeNotes.Factory notesFactory;
 
   @Rule
   public ExpectedException exception = ExpectedException.none();
@@ -209,11 +263,29 @@
   @Rule
   public TemporaryFolder tempSiteDir = new TemporaryFolder();
 
+  @Before
+  public void clearSender() {
+    sender.clear();
+  }
+
+  @Before
+  public void startEventRecorder() {
+    eventRecorder = eventRecorderFactory.create(admin);
+  }
+
+  @After
+  public void closeEventRecorder() {
+    eventRecorder.close();
+  }
+
   @AfterClass
   public static void stopCommonServer() throws Exception {
     if (commonServer != null) {
-      commonServer.stop();
-      commonServer = null;
+      try {
+        commonServer.stop();
+      } finally {
+        commonServer = null;
+      }
     }
     TempFileUtil.cleanup();
   }
@@ -238,11 +310,8 @@
     return cfg.getBoolean("change", null, "submitWholeTopic", false);
   }
 
-  private static boolean isNoteDbTestEnabled() {
-    final String[] RUN_FLAGS = {"yes", "y", "true"};
-    String value = System.getenv("GERRIT_ENABLE_NOTEDB");
-    return value != null &&
-        Arrays.asList(RUN_FLAGS).contains(value.toLowerCase());
+  protected boolean isContributorAgreementsEnabled() {
+    return cfg.getBoolean("auth", null, "contributorAgreements", false);
   }
 
   protected void beforeTest(Description description) throws Exception {
@@ -251,11 +320,9 @@
     GerritServer.Description methodDesc =
       GerritServer.Description.forTestMethod(description, configName);
 
-    if (isNoteDbTestEnabled()) {
-      NotesMigration.setAllEnabledConfig(baseConfig);
-    }
     baseConfig.setString("gerrit", null, "tempSiteDir",
         tempSiteDir.getRoot().getPath());
+    baseConfig.setInt("receive", null, "changeUpdateThreads", 4);
     if (classDesc.equals(methodDesc) && !classDesc.sandboxed() &&
         !methodDesc.sandboxed()) {
       if (commonServer == null) {
@@ -267,6 +334,7 @@
     }
 
     server.getTestInjector().injectMembers(this);
+    notesMigration.setFromEnv();
     Transport.register(inProcessProtocol);
     toClose = Collections.synchronizedList(new ArrayList<Repository>());
     admin = accounts.admin();
@@ -276,14 +344,18 @@
     accountCache.evict(admin.getId());
     accountCache.evict(user.getId());
 
-    adminSession = new RestSession(server, admin);
-    userSession = new RestSession(server, user);
+    adminRestSession = new RestSession(server, admin);
+    userRestSession = new RestSession(server, user);
     initSsh(admin);
     db = reviewDbProvider.open();
-    Context ctx = newRequestContext(admin);
+    Context ctx = newRequestContext(user);
     atrScope.set(ctx);
-    sshSession = ctx.getSession();
-    sshSession.open();
+    userSshSession = ctx.getSession();
+    userSshSession.open();
+    ctx = newRequestContext(admin);
+    atrScope.set(ctx);
+    adminSshSession = ctx.getSession();
+    adminSshSession.open();
     resourcePrefix = UNSAFE_PROJECT_NAME.matcher(
         description.getClassName() + "_"
         + description.getMethodName() + "_").replaceAll("");
@@ -350,14 +422,19 @@
   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)
-      throws RestApiException {
-    return createProject(
-        nameSuffix, parent, createEmptyCommit, SubmitType.MERGE_IF_NECESSARY);
+      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,
@@ -366,8 +443,8 @@
     ProjectInput in = new ProjectInput();
     in.name = name(nameSuffix);
     in.parent = parent != null ? parent.get() : null;
-    in.createEmptyCommit = createEmptyCommit;
     in.submitType = submitType;
+    in.createEmptyCommit = createEmptyCommit;
     return createProject(in);
   }
 
@@ -407,7 +484,8 @@
       repo.close();
     }
     db.close();
-    sshSession.close();
+    adminSshSession.close();
+    userSshSession.close();
     if (server != commonServer) {
       server.stop();
     }
@@ -432,12 +510,73 @@
   }
 
   protected PushOneCommit.Result createChange() throws Exception {
+    return createChange("refs/for/master");
+  }
+
+  protected PushOneCommit.Result createChange(String ref) throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
-    PushOneCommit.Result result = push.to("refs/for/master");
+    PushOneCommit.Result result = push.to(ref);
     result.assertOkStatus();
     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");
+  }
+
+  protected PushOneCommit.Result createChange(String subject,
+      String fileName, String content) throws Exception {
+    PushOneCommit push = pushFactory.create(
+        db, admin.getIdent(), testRepo, subject, fileName, content);
+    return push.to("refs/for/master");
+  }
+
+  protected PushOneCommit.Result createChange(String subject,
+      String fileName, String content, String topic)
+          throws Exception {
+    PushOneCommit push = pushFactory.create(
+        db, admin.getIdent(), testRepo, subject, fileName, content);
+    return push.to("refs/for/master/" + name(topic));
+  }
+
+  protected PushOneCommit.Result createChange(TestRepository<?> repo,
+      String branch, String subject, String fileName, String content,
+      String topic) throws Exception {
+    PushOneCommit push = pushFactory.create(
+        db, admin.getIdent(), repo, subject, fileName, content);
+    return push.to("refs/for/" + branch + "/" + name(topic));
+  }
+
+  protected BranchApi createBranch(Branch.NameKey branch) throws Exception {
+    return gApi.projects()
+        .name(branch.getParentKey().get())
+        .branch(branch.get())
+        .create(new BranchInput());
+  }
+
   protected BranchApi createBranchWithRevision(Branch.NameKey branch,
       String revision) throws Exception {
     BranchInput in = new BranchInput();
@@ -457,13 +596,24 @@
 
   protected PushOneCommit.Result amendChange(String changeId, String ref)
       throws Exception {
+    return amendChange(changeId, ref, admin, testRepo);
+  }
+
+  protected PushOneCommit.Result amendChange(String changeId, String ref,
+      TestAccount testAccount, TestRepository<?> repo) throws Exception {
     Collections.shuffle(RANDOM);
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
-            PushOneCommit.FILE_NAME, new String(Chars.toArray(RANDOM)), changeId);
+        pushFactory.create(db, testAccount.getIdent(), repo,
+            PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME,
+            new String(Chars.toArray(RANDOM)), changeId);
     return push.to(ref);
   }
 
+  protected void merge(PushOneCommit.Result r) throws Exception {
+    revision(r).review(ReviewInput.approve());
+    revision(r).submit();
+  }
+
   protected PushOneCommit.Result amendChangeAsDraft(String changeId)
       throws Exception {
     return amendChange(changeId, "refs/drafts/master");
@@ -495,8 +645,8 @@
   }
 
   private Context newRequestContext(TestAccount account) {
-    return atrScope.newContext(reviewDbProvider, new SshSession(server, admin),
-        identifiedUserFactory.create(Providers.of(db), account.getId()));
+    return atrScope.newContext(reviewDbProvider, new SshSession(server, account),
+        identifiedUserFactory.create(account.getId()));
   }
 
   protected Context setApiUser(TestAccount account) {
@@ -504,7 +654,18 @@
   }
 
   protected Context setApiUserAnonymous() {
-    return atrScope.newContext(reviewDbProvider, null, anonymousUser.get());
+    return atrScope.set(
+        atrScope.newContext(reviewDbProvider, null, anonymousUser.get()));
+  }
+
+  protected Context disableDb() {
+    notesMigration.setFailOnLoad(true);
+    return atrScope.disableDb();
+  }
+
+  protected void enableDb(Context preDisableContext) {
+    notesMigration.setFailOnLoad(false);
+    atrScope.set(preDisableContext);
   }
 
   protected static Gson newGson() {
@@ -554,40 +715,72 @@
 
   protected void setUseContributorAgreements(InheritableBoolean value)
       throws Exception {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    ProjectConfig config = ProjectConfig.read(md);
-    config.getProject().setUseContributorAgreements(value);
-    config.commit(md);
-    projectCache.evict(config.getProject());
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      ProjectConfig config = ProjectConfig.read(md);
+      config.getProject().setUseContributorAgreements(value);
+      config.commit(md);
+      projectCache.evict(config.getProject());
+    }
   }
 
   protected void setUseSignedOffBy(InheritableBoolean value)
       throws Exception {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    ProjectConfig config = ProjectConfig.read(md);
-    config.getProject().setUseSignedOffBy(value);
-    config.commit(md);
-    projectCache.evict(config.getProject());
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      ProjectConfig config = ProjectConfig.read(md);
+      config.getProject().setUseSignedOffBy(value);
+      config.commit(md);
+      projectCache.evict(config.getProject());
+    }
+  }
+
+  protected void setRequireChangeId(InheritableBoolean value) throws Exception {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      ProjectConfig config = ProjectConfig.read(md);
+      config.getProject().setRequireChangeID(value);
+      config.commit(md);
+      projectCache.evict(config.getProject());
+    }
   }
 
   protected void deny(String permission, AccountGroup.UUID id, String ref)
       throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    deny(project, permission, id, ref);
+  }
+
+  protected void deny(Project.NameKey p, String permission,
+      AccountGroup.UUID id, String ref) throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
     Util.deny(cfg, permission, id, ref);
+    saveProjectConfig(p, cfg);
+  }
+
+  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);
+    return rule;
   }
 
   protected void saveProjectConfig(Project.NameKey p, ProjectConfig cfg)
       throws Exception {
-    MetaDataUpdate md = metaDataUpdateFactory.create(p);
-    try {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(p)) {
+      md.setAuthor(identifiedUserFactory.create(admin.getId()));
       cfg.commit(md);
-    } finally {
-      md.close();
     }
     projectCache.evict(cfg.getProject());
   }
 
+  protected void saveProjectConfig(ProjectConfig cfg) throws Exception {
+    saveProjectConfig(project, cfg);
+  }
+
   protected void grant(String permission, Project.NameKey project, String ref)
       throws RepositoryNotFoundException, IOException, ConfigInvalidException {
     grant(permission, project, ref, false);
@@ -595,30 +788,37 @@
 
   protected void grant(String permission, Project.NameKey project, String ref,
       boolean force) throws RepositoryNotFoundException, IOException,
-      ConfigInvalidException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    md.setMessage(String.format("Grant %s on %s", permission, ref));
-    ProjectConfig config = ProjectConfig.read(md);
-    AccessSection s = config.getAccessSection(ref, true);
-    Permission p = s.getPermission(permission, true);
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
-    PermissionRule rule = new PermissionRule(config.resolve(adminGroup));
-    rule.setForce(force);
-    p.add(rule);
-    config.commit(md);
-    projectCache.evict(config.getProject());
+          ConfigInvalidException {
+    AccountGroup adminGroup =
+        groupCache.get(new AccountGroup.NameKey("Administrators"));
+    grant(permission, project, ref, force, adminGroup.getGroupUUID());
   }
 
-  protected void blockRead(Project.NameKey project, String ref) throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    block(cfg, Permission.READ, REGISTERED_USERS, ref);
-    saveProjectConfig(project, cfg);
+  protected void grant(String permission, Project.NameKey project, String ref,
+      boolean force, AccountGroup.UUID groupUUID)
+          throws RepositoryNotFoundException, IOException,
+          ConfigInvalidException {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      md.setMessage(String.format("Grant %s on %s", permission, ref));
+      ProjectConfig config = ProjectConfig.read(md);
+      AccessSection s = config.getAccessSection(ref, true);
+      Permission p = s.getPermission(permission, true);
+      PermissionRule rule = Util.newRule(config, groupUUID);
+      rule.setForce(force);
+      p.add(rule);
+      config.commit(md);
+      projectCache.evict(config.getProject());
+    }
+  }
+
+  protected void blockRead(String ref) throws Exception {
+    block(Permission.READ, REGISTERED_USERS, ref);
   }
 
   protected void blockForgeCommitter(Project.NameKey project, String ref)
       throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    block(cfg, Permission.FORGE_COMMITTER, REGISTERED_USERS, ref);
+    Util.block(cfg, Permission.FORGE_COMMITTER, REGISTERED_USERS, ref);
     saveProjectConfig(project, cfg);
   }
 
@@ -641,17 +841,115 @@
       .actions();
   }
 
+  private static Iterable<String> changeIds(Iterable<ChangeInfo> changes) {
+    return Iterables.transform(changes,
+        new Function<ChangeInfo, String>() {
+          @Override
+          public String apply(ChangeInfo input) {
+            return input.changeId;
+          }
+        });
+  }
+
   protected void assertSubmittedTogether(String chId, String... expected)
       throws Exception {
     List<ChangeInfo> actual = gApi.changes().id(chId).submittedTogether();
+    SubmittedTogetherInfo info =
+        gApi.changes()
+            .id(chId)
+            .submittedTogether(EnumSet.of(NON_VISIBLE_CHANGES));
+
+    assertThat(info.nonVisibleChanges).isEqualTo(0);
     assertThat(actual).hasSize(expected.length);
-    assertThat(Iterables.transform(actual,
-        new Function<ChangeInfo, String>() {
-      @Override
-      public String apply(ChangeInfo input) {
-        return input.changeId;
-      }
-    })).containsExactly((Object[])expected).inOrder();
+    assertThat(changeIds(actual))
+        .containsExactly((Object[])expected).inOrder();
+    assertThat(changeIds(info.changes))
+        .containsExactly((Object[])expected).inOrder();
+  }
+
+  protected PatchSet getPatchSet(PatchSet.Id psId) throws OrmException {
+    return changeDataFactory.create(db, project, psId.getParentKey())
+        .patchSet(psId);
+  }
+
+  protected IdentifiedUser user(TestAccount testAccount) {
+    return identifiedUserFactory.create(testAccount.getId());
+  }
+
+  protected RevisionResource parseCurrentRevisionResource(String changeId)
+      throws Exception {
+    ChangeResource cr = parseChangeResource(changeId);
+    int psId = cr.getChange().currentPatchSetId().get();
+    return revisions.parse(cr,
+        IdString.fromDecoded(Integer.toString(psId)));
+  }
+
+  protected RevisionResource parseRevisionResource(String changeId, int n)
+      throws Exception {
+    return revisions.parse(parseChangeResource(changeId),
+        IdString.fromDecoded(Integer.toString(n)));
+  }
+
+  protected RevisionResource parseRevisionResource(PushOneCommit.Result r)
+      throws Exception {
+    PatchSet.Id psId = r.getPatchSetId();
+    return parseRevisionResource(psId.getParentKey().toString(), psId.get());
+  }
+
+  protected ChangeResource parseChangeResource(String changeId)
+      throws Exception {
+    List<ChangeControl> ctls = changeFinder.find(
+        changeId, atrScope.get().getUser());
+    assertThat(ctls).hasSize(1);
+    return changeResourceFactory.create(ctls.get(0));
+  }
+
+  protected String createGroup(String name) throws Exception {
+    return createGroup(name, "Administrators");
+  }
+
+  protected String createGroup(String name, String owner) throws Exception {
+    name = name(name);
+    GroupInput in = new GroupInput();
+    in.name = name;
+    in.ownerId = owner;
+    gApi.groups().create(in);
+    return name;
+  }
+
+  protected RevCommit getHead(Repository repo, String name) throws Exception {
+    try (RevWalk rw = new RevWalk(repo)) {
+      return rw.parseCommit(repo.exactRef(name).getObjectId());
+    }
+  }
+
+  protected RevCommit getHead(Repository repo) throws Exception {
+    return getHead(repo, "HEAD");
+  }
+
+  protected RevCommit getRemoteHead(Project.NameKey project, String branch)
+      throws Exception {
+    try (Repository repo = repoManager.openRepository(project)) {
+      return getHead(repo,
+          branch.startsWith(Constants.R_REFS) ? branch : "refs/heads/" + branch);
+    }
+  }
+
+  protected RevCommit getRemoteHead(String project, String branch)
+      throws Exception {
+    return getRemoteHead(new Project.NameKey(project), branch);
+  }
+
+  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);
   }
 
   protected TestRepository<?> createProjectWithPush(String name,
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 34379a1..71d738c 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.acceptance;
 
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -32,6 +31,7 @@
 import com.google.inject.Scope;
 import com.google.inject.util.Providers;
 
+import java.util.HashMap;
 import java.util.Map;
 
 /** Guice scopes for state during an Acceptance Test connection. */
@@ -44,7 +44,7 @@
 
   public static class Context implements RequestContext {
     private final RequestCleanup cleanup = new RequestCleanup();
-    private final Map<Key<?>, Object> map = Maps.newHashMap();
+    private final Map<Key<?>, Object> map = new HashMap<>();
     private final SchemaFactory<ReviewDb> schemaFactory;
     private final SshSession session;
     private final CurrentUser user;
@@ -182,6 +182,14 @@
     return old;
   }
 
+  public Context reopenDb() {
+    // Setting a new context with the same fields is enough to get the ReviewDb
+    // provider to reopen the database.
+    Context old = current.get();
+    return set(
+        new Context(old.schemaFactory, old.session, old.user, old.created));
+  }
+
   /** Returns exactly one instance per command executed. */
   static final Scope REQUEST = new Scope() {
     @Override
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 9eaf266..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
@@ -15,19 +15,20 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.charset.StandardCharsets.US_ASCII;
 
 import com.google.gerrit.common.TimeUtil;
 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.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 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.VersionedAuthorizedKeys;
+import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -46,36 +47,41 @@
 public class AccountCreator {
   private final Map<String, TestAccount> accounts;
 
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-  private GroupCache groupCache;
-  private SshKeyCache sshKeyCache;
-  private AccountCache accountCache;
-  private AccountByEmailCache byEmailCache;
+  private final SchemaFactory<ReviewDb> reviewDbProvider;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
+  private final GroupCache groupCache;
+  private final SshKeyCache sshKeyCache;
+  private final AccountCache accountCache;
+  private final AccountByEmailCache byEmailCache;
+  private final AccountIndexer indexer;
 
   @Inject
-  AccountCreator(SchemaFactory<ReviewDb> schema, GroupCache groupCache,
-      SshKeyCache sshKeyCache, AccountCache accountCache,
-      AccountByEmailCache byEmailCache) {
+  AccountCreator(SchemaFactory<ReviewDb> schema,
+      VersionedAuthorizedKeys.Accessor authorizedKeys,
+      GroupCache groupCache,
+      SshKeyCache sshKeyCache,
+      AccountCache accountCache,
+      AccountByEmailCache byEmailCache,
+      AccountIndexer indexer) {
     accounts = new HashMap<>();
     reviewDbProvider = schema;
+    this.authorizedKeys = authorizedKeys;
     this.groupCache = groupCache;
     this.sshKeyCache = sshKeyCache;
     this.accountCache = accountCache;
     this.byEmailCache = byEmailCache;
+    this.indexer = indexer;
   }
 
   public synchronized TestAccount create(String username, String email,
-      String fullName, String... groups)
-      throws OrmException, UnsupportedEncodingException, JSchException {
+      String fullName, String... groups) throws Exception {
     TestAccount account = accounts.get(username);
     if (account != null) {
       return account;
     }
     try (ReviewDb db = reviewDbProvider.open()) {
       Account.Id id = new Account.Id(db.nextAccountId());
-      KeyPair sshKey = genSshKey();
-      AccountSshKey key =
-          new AccountSshKey(new AccountSshKey.Id(id, 1), publicKey(sshKey, email));
+
       AccountExternalId extUser =
           new AccountExternalId(id, new AccountExternalId.Key(
               AccountExternalId.SCHEME_USERNAME, username));
@@ -94,8 +100,6 @@
       a.setPreferredEmail(email);
       db.accounts().insert(Collections.singleton(a));
 
-      db.accountSshKeys().insert(Collections.singleton(key));
-
       if (groups != null) {
         for (String n : groups) {
           AccountGroup.NameKey k = new AccountGroup.NameKey(n);
@@ -106,10 +110,15 @@
         }
       }
 
+      KeyPair sshKey = genSshKey();
+      authorizedKeys.addKey(id, publicKey(sshKey, email));
       sshKeyCache.evict(username);
+
       accountCache.evictByUsername(username);
       byEmailCache.evict(email);
 
+      indexer.index(id);
+
       account =
           new TestAccount(id, username, email, fullName, sshKey, httpPass);
       accounts.put(username, account);
@@ -117,35 +126,29 @@
     }
   }
 
-  public TestAccount create(String username, String group)
-      throws OrmException, UnsupportedEncodingException, JSchException {
+  public TestAccount create(String username, String group) throws Exception {
     return create(username, null, username, group);
   }
 
-  public TestAccount create(String username)
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount create(String username) throws Exception {
     return create(username, null, username, (String[]) null);
   }
 
-  public TestAccount admin()
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount admin() throws Exception {
     return create("admin", "admin@example.com", "Administrator",
       "Administrators");
   }
 
-  public TestAccount admin2()
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount admin2() throws Exception {
     return create("admin2", "admin2@example.com", "Administrator2",
       "Administrators");
   }
 
-  public TestAccount user()
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount user() throws Exception {
     return create("user", "user@example.com", "User");
   }
 
-  public TestAccount user2()
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount user2() throws Exception {
     return create("user2", "user2@example.com", "User2");
   }
 
@@ -159,15 +162,15 @@
     return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
   }
 
-  private static KeyPair genSshKey() throws JSchException {
+  public static KeyPair genSshKey() throws JSchException {
     JSch jsch = new JSch();
     return KeyPair.genKeyPair(jsch, KeyPair.RSA);
   }
 
-  private static String publicKey(KeyPair sshKey, String comment)
+  public static String publicKey(KeyPair sshKey, String comment)
       throws UnsupportedEncodingException {
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     sshKey.writePublicKey(out, comment);
-    return out.toString("ASCII");
+    return out.toString(US_ASCII.name()).trim();
   }
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AssertUtil.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AssertUtil.java
new file mode 100644
index 0000000..a325feb
--- /dev/null
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AssertUtil.java
@@ -0,0 +1,47 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.server.config.ConfigUtil.skipField;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class AssertUtil {
+  public static <T> void assertPrefs(T actual, T expected,
+      String... fieldsToExclude)
+          throws IllegalArgumentException, IllegalAccessException {
+    Set<String> exludedFields = new HashSet<>(Arrays.asList(fieldsToExclude));
+    for (Field field : actual.getClass().getDeclaredFields()) {
+      if (exludedFields.contains(field.getName()) || skipField(field)) {
+        continue;
+      }
+      Object actualVal = field.get(actual);
+      Object expectedVal = field.get(expected);
+      if (field.getType().isAssignableFrom(Boolean.class)) {
+        if (actualVal == null) {
+          actualVal = false;
+        }
+        if (expectedVal == null) {
+          expectedVal = false;
+        }
+      }
+      assertWithMessage(field.getName()).that(actualVal).isEqualTo(expectedVal);
+    }
+  }
+}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/EventRecorder.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/EventRecorder.java
new file mode 100644
index 0000000..6cc8d3c
--- /dev/null
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -0,0 +1,220 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.common.UserScopedEventListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.RefEvent;
+import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.server.events.ReviewerDeletedEvent;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+public class EventRecorder {
+  private final RegistrationHandle eventListenerRegistration;
+  private final Multimap<String, RefEvent> recordedEvents;
+
+  @Singleton
+  public static class Factory {
+    private final DynamicSet<UserScopedEventListener> eventListeners;
+    private final IdentifiedUser.GenericFactory userFactory;
+
+    @Inject
+    Factory(DynamicSet<UserScopedEventListener> eventListeners,
+        IdentifiedUser.GenericFactory userFactory) {
+      this.eventListeners = eventListeners;
+      this.userFactory = userFactory;
+    }
+
+    public EventRecorder create(TestAccount user) {
+      return new EventRecorder(eventListeners, userFactory.create(user.id));
+    }
+  }
+
+  public EventRecorder(DynamicSet<UserScopedEventListener> eventListeners,
+      final IdentifiedUser user) {
+    recordedEvents = LinkedListMultimap.create();
+
+    eventListenerRegistration = eventListeners.add(
+        new UserScopedEventListener() {
+          @Override
+          public void onEvent(Event e) {
+            if (e instanceof ReviewerDeletedEvent) {
+              recordedEvents.put(
+                  ReviewerDeletedEvent.TYPE, (ReviewerDeletedEvent) e);
+            } else if (e instanceof RefEvent) {
+              RefEvent event = (RefEvent) e;
+              String key = refEventKey(event.getType(),
+                  event.getProjectNameKey().get(),
+                  event.getRefName());
+              recordedEvents.put(key, event);
+            }
+          }
+
+          @Override
+          public CurrentUser getUser() {
+            return user;
+          }
+        });
+  }
+
+  private static String refEventKey(String type, String project, String ref) {
+    return String.format("%s-%s-%s", type, project, ref);
+  }
+
+  private static class RefEventTransformer<T extends RefEvent>
+      implements Function<RefEvent, T> {
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public T apply(RefEvent e) {
+      return (T) e;
+    }
+  }
+
+  private ImmutableList<RefUpdatedEvent> getRefUpdatedEvents(String project,
+      String refName, int expectedSize) {
+    String key = refEventKey(RefUpdatedEvent.TYPE, project, refName);
+    if (expectedSize == 0) {
+      assertThat(recordedEvents).doesNotContainKey(key);
+      return ImmutableList.of();
+    }
+
+    assertThat(recordedEvents).containsKey(key);
+    ImmutableList<RefUpdatedEvent> events = FluentIterable
+        .from(recordedEvents.get(key))
+        .transform(new RefEventTransformer<RefUpdatedEvent>())
+        .toList();
+    assertThat(events).hasSize(expectedSize);
+    return events;
+  }
+
+  private ImmutableList<ChangeMergedEvent> getChangeMergedEvents(String project,
+      String branch, int expectedSize) {
+    String key = refEventKey(ChangeMergedEvent.TYPE, project, branch);
+    if (expectedSize == 0) {
+      assertThat(recordedEvents).doesNotContainKey(key);
+      return ImmutableList.of();
+    }
+
+    assertThat(recordedEvents).containsKey(key);
+    ImmutableList<ChangeMergedEvent> events = FluentIterable
+        .from(recordedEvents.get(key))
+        .transform(new RefEventTransformer<ChangeMergedEvent>())
+        .toList();
+    assertThat(events).hasSize(expectedSize);
+    return events;
+  }
+
+  private ImmutableList<ReviewerDeletedEvent> getReviewerDeletedEvents(
+      int expectedSize) {
+    String key = ReviewerDeletedEvent.TYPE;
+    if (expectedSize == 0) {
+      assertThat(recordedEvents).doesNotContainKey(key);
+      return ImmutableList.of();
+    }
+    assertThat(recordedEvents).containsKey(key);
+    ImmutableList<ReviewerDeletedEvent> events = FluentIterable
+        .from(recordedEvents.get(key))
+        .transform(new RefEventTransformer<ReviewerDeletedEvent>())
+        .toList();
+    assertThat(events).hasSize(expectedSize);
+    return events;
+  }
+
+  public void assertRefUpdatedEvents(String project, String branch,
+      String... expected) throws Exception {
+    ImmutableList<RefUpdatedEvent> events = getRefUpdatedEvents(project,
+        branch, expected.length / 2);
+    int i = 0;
+    for (RefUpdatedEvent event : events) {
+      RefUpdateAttribute actual = event.refUpdate.get();
+      String oldRev = expected[i] == null
+          ? ObjectId.zeroId().name()
+          : expected[i];
+      String newRev = expected[i+1] == null
+          ? ObjectId.zeroId().name()
+          : expected[i+1];
+      assertThat(actual.oldRev).isEqualTo(oldRev);
+      assertThat(actual.newRev).isEqualTo(newRev);
+      i += 2;
+    }
+  }
+
+  public void assertRefUpdatedEvents(String project, String branch,
+      RevCommit... expected) throws Exception {
+    ImmutableList<RefUpdatedEvent> events = getRefUpdatedEvents(project,
+        branch, expected.length / 2);
+    int i = 0;
+    for (RefUpdatedEvent event : events) {
+      RefUpdateAttribute actual = event.refUpdate.get();
+      String oldRev = expected[i] == null
+          ? ObjectId.zeroId().name()
+          : expected[i].name();
+      String newRev = expected[i+1] == null
+          ? ObjectId.zeroId().name()
+          : expected[i+1].name();
+      assertThat(actual.oldRev).isEqualTo(oldRev);
+      assertThat(actual.newRev).isEqualTo(newRev);
+      i += 2;
+    }
+  }
+
+  public void assertChangeMergedEvents(String project, String branch,
+      String... expected) throws Exception {
+    ImmutableList<ChangeMergedEvent> events = getChangeMergedEvents(project,
+        branch, expected.length / 2);
+    int i = 0;
+    for (ChangeMergedEvent event : events) {
+      String id = event.change.get().id;
+      assertThat(id).isEqualTo(expected[i]);
+      assertThat(event.newRev).isEqualTo(expected[i+1]);
+      i += 2;
+    }
+  }
+
+  public void assertReviewerDeletedEvents(String... expected) {
+    ImmutableList<ReviewerDeletedEvent> events =
+        getReviewerDeletedEvents(expected.length / 2);
+    int i = 0;
+    for (ReviewerDeletedEvent event : events) {
+      String id = event.change.get().id;
+      assertThat(id).isEqualTo(expected[i]);
+      String reviewer = event.reviewer.get().email;
+      assertThat(reviewer).isEqualTo(expected[i+1]);
+      i += 2;
+    }
+  }
+
+  public void close() {
+    eventListenerRegistration.remove();
+  }
+}
\ No newline at end of file
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
index 58bb9f2..e0f9d4a 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
@@ -23,5 +23,5 @@
 @Target({METHOD})
 @Retention(RUNTIME)
 public @interface GerritConfigs {
-  public GerritConfig[] value();
+  GerritConfig[] value();
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 5b0d311..d1ec9e6 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -24,11 +24,12 @@
 import com.google.gerrit.pgm.Init;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.AsyncReceiveCommits;
-import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.testutil.FakeEmailSender;
+import com.google.gerrit.testutil.NoteDbChecker;
+import com.google.gerrit.testutil.NoteDbMode;
 import com.google.gerrit.testutil.TempFileUtil;
 import com.google.inject.Injector;
 import com.google.inject.Key;
@@ -144,8 +145,7 @@
       cfg.setBoolean("index", "lucene", "testInmemory", true);
       cfg.setString("gitweb", null, "cgi", "");
       daemon.setEnableHttpd(desc.httpd());
-      daemon.setLuceneModule(new LuceneIndexModule(
-          ChangeSchemas.getLatest().getVersion(), 0, null));
+      daemon.setLuceneModule(LuceneIndexModule.singleVersionAllLatest(0));
       daemon.setDatabaseForTesting(ImmutableList.<Module>of(
           new InMemoryTestingDatabaseModule(cfg)));
       daemon.start();
@@ -157,7 +157,7 @@
         public Void call() throws Exception {
           int rc = daemon.main(new String[] {
               "-d", site.getPath(),
-              "--headless", "--console-log", "--show-stack-trace"});
+              "--headless", "--console-log", "--show-stack-trace",});
           if (rc != 0) {
             System.err.println("Failed to start Gerrit daemon");
             serverStarted.reset();
@@ -178,7 +178,7 @@
     Init init = new Init();
     int rc = init.main(new String[] {
         "-d", tmp.getPath(), "--batch", "--no-auto-start",
-        "--skip-plugins"});
+        "--skip-plugins",});
     if (rc != 0) {
       throw new RuntimeException("Couldn't initialize site");
     }
@@ -289,13 +289,20 @@
   }
 
   void stop() throws Exception {
-    daemon.getLifecycleManager().stop();
-    if (daemonService != null) {
-      System.out.println("Gerrit Server Shutdown");
-      daemonService.shutdownNow();
-      daemonService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+    try {
+      if (NoteDbMode.get().equals(NoteDbMode.CHECK)) {
+        testInjector.getInstance(NoteDbChecker.class)
+            .rebuildAndCheckAllChanges();
+      }
+    } finally {
+      daemon.getLifecycleManager().stop();
+      if (daemonService != null) {
+        System.out.println("Gerrit Server Shutdown");
+        daemonService.shutdownNow();
+        daemonService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+      }
+      RepositoryCache.clear();
     }
-    RepositoryCache.clear();
   }
 
   @Override
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
index e8f8925..f1700a7 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import com.google.common.base.Optional;
 import com.google.common.collect.Iterables;
 import com.google.common.primitives.Ints;
@@ -39,6 +41,7 @@
 import org.eclipse.jgit.transport.OpenSshConfig.Host;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.SshSessionFactory;
 import org.eclipse.jgit.util.FS;
 
@@ -103,12 +106,18 @@
       Project.NameKey project, String uri) throws Exception {
     DfsRepositoryDescription desc =
         new DfsRepositoryDescription("clone of " + project.get());
+
+    FS fs = FS.detect();
+
+    // Avoid leaking user state into our tests.
+    fs.setUserHome(null);
+
     InMemoryRepository dest = new InMemoryRepository.Builder()
         .setRepositoryDescription(desc)
         // SshTransport depends on a real FS to read ~/.ssh/config, but
         // InMemoryRepository by default uses a null FS.
         // TODO(dborowitz): Remove when we no longer depend on SSH.
-        .setFS(FS.detect())
+        .setFS(fs)
         .build();
     Config cfg = dest.getConfig();
     cfg.setString("remote", "origin", "url", uri);
@@ -135,6 +144,11 @@
     fetch.call();
   }
 
+  public static PushResult pushHead(TestRepository<?> testRepo, String ref)
+      throws GitAPIException {
+    return pushHead(testRepo, ref, false);
+  }
+
   public static PushResult pushHead(TestRepository<?> testRepo, String ref,
       boolean pushTags) throws GitAPIException {
     return pushHead(testRepo, ref, pushTags, false);
@@ -152,6 +166,20 @@
     return Iterables.getOnlyElement(r);
   }
 
+  public static void assertPushOk(PushResult result, String ref) {
+    RemoteRefUpdate rru = result.getRemoteUpdate(ref);
+    assertThat(rru.getStatus()).named(rru.toString())
+        .isEqualTo(RemoteRefUpdate.Status.OK);
+  }
+
+  public static void assertPushRejected(PushResult result, String ref,
+      String expectedMessage) {
+    RemoteRefUpdate rru = result.getRemoteUpdate(ref);
+    assertThat(rru.getStatus()).named(rru.toString())
+        .isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
+    assertThat(rru.getMessage()).isEqualTo(expectedMessage);
+  }
+
   public static Optional<String> getChangeId(TestRepository<?> tr, ObjectId id)
       throws IOException {
     RevCommit c = tr.getRevWalk().parseCommit(id);
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index 634db7c..443c580 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -18,6 +18,8 @@
 
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePath;
@@ -25,24 +27,32 @@
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.config.TrackingFootersProvider;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.schema.DataSourceType;
+import com.google.gerrit.server.schema.NotesMigrationSchemaFactory;
+import com.google.gerrit.server.schema.ReviewDbFactory;
 import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gerrit.server.schema.SchemaVersion;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryH2Type;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gerrit.testutil.TestNotesMigration;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
+import com.google.inject.Key;
 import com.google.inject.Provides;
+import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 
-import org.apache.sshd.common.KeyPairProvider;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
 import org.eclipse.jgit.lib.Config;
 
+import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
@@ -60,17 +70,26 @@
       .toInstance(cfg);
 
     // TODO(dborowitz): Use jimfs.
+    Path p = Paths.get(cfg.getString("gerrit", null, "tempSiteDir"));
     bind(Path.class)
       .annotatedWith(SitePath.class)
-      .toInstance(Paths.get(cfg.getString("gerrit", null, "tempSiteDir")));
+      .toInstance(p);
+    makeSiteDirs(p);
 
     bind(GitRepositoryManager.class)
-      .toInstance(new InMemoryRepositoryManager());
+      .to(InMemoryRepositoryManager.class);
+    bind(InMemoryRepositoryManager.class).in(SINGLETON);
 
+    bind(MetricMaker.class).to(DisabledMetricMaker.class);
     bind(DataSourceType.class).to(InMemoryH2Type.class);
+
+    bind(NotesMigration.class).to(TestNotesMigration.class);
+    TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory =
+        new TypeLiteral<SchemaFactory<ReviewDb>>() {};
+    bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
+    bind(Key.get(schemaFactory, ReviewDbFactory.class))
+        .to(InMemoryDatabase.class);
     bind(InMemoryDatabase.class).in(SINGLETON);
-    bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {})
-      .to(InMemoryDatabase.class);
 
     listener().to(CreateDatabase.class);
 
@@ -122,4 +141,14 @@
       mem.drop();
     }
   }
+
+  private static void makeSiteDirs(Path p) {
+    try {
+      Files.createDirectories(p.resolve("etc"));
+    } catch (IOException e) {
+      ProvisionException pe = new ProvisionException(e.getMessage());
+      pe.initCause(e);
+      throw pe;
+    }
+  }
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
index c16eed7..1d03acd 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.InProcessProtocol.Context;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Account;
@@ -29,13 +30,14 @@
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.git.AsyncReceiveCommits;
-import com.google.gerrit.server.git.ChangeCache;
 import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.gerrit.server.git.ReceivePackInitializer;
+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.VisibleRefFilter;
 import com.google.gerrit.server.git.validators.UploadValidators;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.util.RequestContext;
@@ -211,8 +213,9 @@
     private final Provider<ReviewDb> dbProvider;
     private final Provider<CurrentUser> userProvider;
     private final TagCache tagCache;
-    private final ChangeCache changeCache;
+    @Nullable private final SearchingChangeCacheImpl changeCache;
     private final ProjectControl.GenericFactory projectControlFactory;
+    private final ChangeNotes.Factory changeNotesFactory;
     private final TransferConfig transferConfig;
     private final DynamicSet<PreUploadHook> preUploadHooks;
     private final UploadValidators.Factory uploadValidatorsFactory;
@@ -223,8 +226,9 @@
         Provider<ReviewDb> dbProvider,
         Provider<CurrentUser> userProvider,
         TagCache tagCache,
-        ChangeCache changeCache,
+        @Nullable SearchingChangeCacheImpl changeCache,
         ProjectControl.GenericFactory projectControlFactory,
+        ChangeNotes.Factory changeNotesFactory,
         TransferConfig transferConfig,
         DynamicSet<PreUploadHook> preUploadHooks,
         UploadValidators.Factory uploadValidatorsFactory,
@@ -234,6 +238,7 @@
       this.tagCache = tagCache;
       this.changeCache = changeCache;
       this.projectControlFactory = projectControlFactory;
+      this.changeNotesFactory = changeNotesFactory;
       this.transferConfig = transferConfig;
       this.preUploadHooks = preUploadHooks;
       this.uploadValidatorsFactory = uploadValidatorsFactory;
@@ -259,11 +264,9 @@
         UploadPack up = new UploadPack(repo);
         up.setPackConfig(transferConfig.getPackConfig());
         up.setTimeout(transferConfig.getTimeout());
-
-        if (!ctl.allRefsAreVisible()) {
-          up.setAdvertiseRefsHook(new VisibleRefFilter(
-              tagCache, changeCache, repo, ctl, dbProvider.get(), true));
-        }
+        up.setAdvertiseRefsHook(new VisibleRefFilter(
+            tagCache, changeNotesFactory, changeCache, repo, ctl,
+            dbProvider.get(), true));
         List<PreUploadHook> hooks = Lists.newArrayList(preUploadHooks);
         hooks.add(uploadValidatorsFactory.create(
             ctl.getProject(), repo, "localhost-test"));
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 f0b9f46..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
@@ -41,16 +41,18 @@
 
   private static final String BUCKLC = "buck";
   private static final String BUCKOUT = "buck-out";
+  private static final String ECLIPSE = "eclipse-out";
 
   private Path gen;
-  private Path testSite;
   private Path pluginRoot;
   private Path pluginsSitePath;
   private Path pluginSubPath;
   private Path pluginSource;
-  private String pluginName;
   private boolean standalone;
 
+  protected String pluginName;
+  protected Path testSite;
+
   @Override
   protected void beforeTest(Description description) throws Exception {
     locatePaths();
@@ -58,9 +60,13 @@
     buildPluginJar();
     createTestSiteDirs();
     copyJarToTestSite();
+    beforeTestServerStarts();
     super.beforeTest(description);
   }
 
+  protected void beforeTestServerStarts() throws Exception {
+  }
+
   protected void setPluginConfigString(String name, String value)
       throws IOException, ConfigInvalidException {
     SitePaths sitePath = new SitePaths(testSite);
@@ -93,7 +99,7 @@
       if (subPath.endsWith("plugins")) {
         pluginsIdx = idx;
       }
-      if (subPath.endsWith(BUCKOUT)) {
+      if (subPath.endsWith(BUCKOUT) || subPath.endsWith(ECLIPSE)) {
         buckOutIdx = idx;
       }
       idx++;
@@ -121,7 +127,8 @@
         String gerritDirCandidate =
             partialPath.subpath(count - 2, count - 1).toString();
         if (pattern.matcher(gerritDirCandidate).matches()) {
-          if (partialPath.endsWith(gerritDirCandidate + "/" + BUCKOUT)) {
+          if (partialPath.endsWith(gerritDirCandidate + "/" + BUCKOUT) ||
+              partialPath.endsWith(gerritDirCandidate + "/" + ECLIPSE)) {
             return false;
           }
         }
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 6a090fd..d79e573 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;
@@ -27,6 +28,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
@@ -36,7 +38,6 @@
 
 import org.eclipse.jgit.api.TagCommand;
 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.eclipse.jgit.transport.PushResult;
@@ -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";
@@ -77,6 +79,12 @@
         ReviewDb db,
         PersonIdent i,
         TestRepository<?> testRepo,
+        @Assisted("changeId") String changeId);
+
+    PushOneCommit create(
+        ReviewDb db,
+        PersonIdent i,
+        TestRepository<?> testRepo,
         @Assisted("subject") String subject,
         @Assisted("fileName") String fileName,
         @Assisted("content") String content);
@@ -85,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,
@@ -117,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;
@@ -143,6 +157,18 @@
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
+      @Assisted("changeId") String changeId) throws Exception {
+    this(notesFactory, approvalsUtil, queryProvider,
+        db, i, testRepo, SUBJECT, FILE_NAME, FILE_CONTENT, changeId);
+  }
+
+  @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) throws Exception {
@@ -157,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")
@@ -188,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);
   }
 
@@ -227,6 +287,10 @@
     this.force = force;
   }
 
+  public void noParents() {
+    commitBuilder.noParents();
+  }
+
   public class Result {
     private final String ref;
     private final PushResult result;
@@ -258,17 +322,13 @@
       return changeId;
     }
 
-    public ObjectId getCommitId() {
-      return commit;
-    }
-
     public RevCommit getCommit() {
       return commit;
     }
 
     public void assertChange(Change.Status expectedStatus,
         String expectedTopic, TestAccount... expectedReviewers)
-        throws OrmException {
+        throws OrmException, NoSuchChangeException {
       Change c = getChange().change();
       assertThat(c.getSubject()).isEqualTo(resSubj);
       assertThat(c.getStatus()).isEqualTo(expectedStatus);
@@ -277,9 +337,10 @@
     }
 
     private void assertReviewers(Change c, TestAccount... expectedReviewers)
-        throws OrmException {
-      Iterable<Account.Id> actualIds =
-          approvalsUtil.getReviewers(db, notesFactory.create(c)).values();
+        throws OrmException, NoSuchChangeException {
+      Iterable<Account.Id> actualIds = approvalsUtil
+          .getReviewers(db, notesFactory.createChecked(db, c))
+          .all();
       assertThat(actualIds).containsExactlyElementsIn(
           Sets.newHashSet(TestAccount.ids(expectedReviewers)));
     }
@@ -310,10 +371,14 @@
     public void assertMessage(String expectedMessage) {
       RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
       assertThat(message(refUpdate).toLowerCase())
-        .named(message(refUpdate))
         .contains(expectedMessage.toLowerCase());
     }
 
+    public String getMessage() {
+      RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
+      return message(refUpdate);
+    }
+
     private String message(RemoteRefUpdate refUpdate) {
       StringBuilder b = new StringBuilder();
       if (refUpdate.getMessage() != null) {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java
index 261b894..d76cb81 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.httpd.restapi.RestApiServlet.JSON_MAGIC;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import org.apache.http.HttpStatus;
+
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.Reader;
@@ -36,4 +39,51 @@
     }
     return reader;
   }
+
+  public void assertStatus(int status) throws Exception {
+    assert_()
+        .withFailureMessage(String.format("Expected status code %d", status))
+        .that(getStatusCode())
+        .isEqualTo(status);
+  }
+
+  public void assertOK() throws Exception {
+    assertStatus(HttpStatus.SC_OK);
+  }
+
+  public void assertNotFound() throws Exception {
+    assertStatus(HttpStatus.SC_NOT_FOUND);
+  }
+
+  public void assertConflict() throws Exception {
+    assertStatus(HttpStatus.SC_CONFLICT);
+  }
+
+  public void assertForbidden() throws Exception {
+    assertStatus(HttpStatus.SC_FORBIDDEN);
+  }
+
+  public void assertNoContent() throws Exception {
+    assertStatus(HttpStatus.SC_NO_CONTENT);
+  }
+
+  public void assertBadRequest() throws Exception {
+    assertStatus(HttpStatus.SC_BAD_REQUEST);
+  }
+
+  public void assertUnprocessableEntity() throws Exception {
+    assertStatus(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+  }
+
+  public void assertMethodNotAllowed() throws Exception {
+    assertStatus(HttpStatus.SC_METHOD_NOT_ALLOWED);
+  }
+
+  public void assertCreated() throws Exception {
+    assertStatus(HttpStatus.SC_CREATED);
+  }
+
+  public void assertPreconditionFailed() throws Exception {
+    assertStatus(HttpStatus.SC_PRECONDITION_FAILED);
+  }
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
index 4b22d0a..9c59e10 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -28,9 +28,7 @@
 import org.apache.http.entity.StringEntity;
 import org.apache.http.message.BasicHeader;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.io.InputStream;
 
 public class RestSession extends HttpSession {
 
@@ -56,6 +54,10 @@
     return execute(get);
   }
 
+  public RestResponse head(String endPoint) throws IOException {
+    return execute(Request.Head(url + "/a" + endPoint));
+  }
+
   public RestResponse put(String endPoint) throws IOException {
     return put(endPoint, null);
   }
@@ -113,33 +115,4 @@
   public RestResponse delete(String endPoint) throws IOException {
     return execute(Request.Delete(url + "/a" + endPoint));
   }
-
-  public RestResponse head(String endPoint) throws IOException {
-    return execute(Request.Head(url + "/a" + endPoint));
-  }
-
-  public static RawInput newRawInput(String content) {
-    return newRawInput(content.getBytes(UTF_8));
-  }
-
-  public static RawInput newRawInput(final byte[] bytes) {
-    Preconditions.checkNotNull(bytes);
-    Preconditions.checkArgument(bytes.length > 0);
-    return new RawInput() {
-      @Override
-      public InputStream getInputStream() throws IOException {
-        return new ByteArrayInputStream(bytes);
-      }
-
-      @Override
-      public String getContentType() {
-        return "application/octet-stream";
-      }
-
-      @Override
-      public long getContentLength() {
-        return bytes.length;
-      }
-    };
-  }
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
index 4a6d22d..7f08b6f 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Function;
 import com.google.common.collect.FluentIterable;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.mail.Address;
 
 import com.jcraft.jsch.KeyPair;
 
@@ -58,6 +59,7 @@
   public final Account.Id id;
   public final String username;
   public final String email;
+  public final Address emailAddress;
   public final String fullName;
   public final KeyPair sshKey;
   public final String httpPassword;
@@ -67,6 +69,7 @@
     this.id = id;
     this.username = username;
     this.email = email;
+    this.emailAddress = new Address(fullName, email);
     this.fullName = fullName;
     this.sshKey = sshKey;
     this.httpPassword = httpPassword;
diff --git a/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK
index 0a39ea7..d5d0b0d 100644
--- a/gerrit-acceptance-tests/BUCK
+++ b/gerrit-acceptance-tests/BUCK
@@ -32,7 +32,8 @@
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
-    '//lib/jgit:jgit',
+    '//lib/log:api',
+    '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib/mina:sshd',
   ],
   visibility = [
diff --git a/gerrit-acceptance-tests/BUILD b/gerrit-acceptance-tests/BUILD
new file mode 100644
index 0000000..2ec7a05
--- /dev/null
+++ b/gerrit-acceptance-tests/BUILD
@@ -0,0 +1,42 @@
+load('//tools/bzl:java.bzl', 'java_library2')
+
+java_library2(
+  name = 'lib',
+  srcs = glob(['src/test/java/com/google/gerrit/acceptance/*.java']),
+  exported_deps = [
+    '//gerrit-acceptance-framework:lib',
+    '//gerrit-common:annotations',
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-gpg:testutil',
+    '//gerrit-launcher:launcher',
+    '//gerrit-lucene:lucene',
+    '//gerrit-httpd:httpd',
+    '//gerrit-pgm:init',
+    '//gerrit-pgm:pgm',
+    '//gerrit-pgm:util',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//gerrit-server:testutil',
+    '//gerrit-server/src/main/prolog:common',
+    '//gerrit-sshd:sshd',
+
+    '//lib:args4j',
+    '//lib:gson',
+    '//lib:gwtjsonrpc',
+    '//lib:gwtorm',
+    '//lib:h2',
+    '//lib:jsch',
+    '//lib:servlet-api-3_1-without-neverlink',
+
+    '//lib/bouncycastle:bcpg',
+    '//lib/bouncycastle:bcprov',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice-servlet',
+    '//lib/log:api',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/mina:sshd',
+  ],
+  visibility = ['//visibility:public'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SandboxTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SandboxTest.java
index 6ead346..29aadc1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SandboxTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SandboxTest.java
@@ -16,9 +16,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.server.account.PutUsername;
-
-import org.apache.http.HttpStatus;
 import org.junit.After;
 import org.junit.Test;
 
@@ -26,16 +23,11 @@
 public class SandboxTest extends AbstractDaemonTest {
   @After
   public void addUser() throws Exception {
-    PutUsername.Input in = new PutUsername.Input();
-    in.username = "sandboxuser";
-    RestResponse r =
-        adminSession.put("/accounts/sandboxuser", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
+    gApi.accounts().create("sandboxuser");
   }
 
   private void testUserNotPresent() throws Exception {
-    RestResponse r = adminSession.get("/accounts/sandboxuser");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    assertThat(gApi.accounts().query("sandboxuser").get()).isEmpty();
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
index f4d156c..2f50480 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
@@ -29,15 +29,15 @@
   Config serverConfig;
 
   @Test
-  @GerritConfig(name="x.y", value="z")
+  @GerritConfig(name = "x.y", value = "z")
   public void testOne() {
     assertThat(serverConfig.getString("x", null, "y")).isEqualTo("z");
   }
 
   @Test
   @GerritConfigs({
-      @GerritConfig(name="x.y", value="z"),
-      @GerritConfig(name="a.b", value="c")
+      @GerritConfig(name = "x.y", value = "z"),
+      @GerritConfig(name = "a.b", value = "c")
   })
   public void testMultiple() {
     assertThat(serverConfig.getString("x", null, "y")).isEqualTo("z");
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 b6a54b6..6ad20ff 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
@@ -17,24 +17,39 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
 import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
 import static com.google.gerrit.gpg.testutil.TestKeys.allValidKeys;
 import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
 import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
 import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
+import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL;
+import static com.google.gerrit.server.StarredChangesUtil.IGNORE_LABEL;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Function;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.StarsInput;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.common.SshKeyInfo;
+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.ResourceNotFoundException;
@@ -44,27 +59,40 @@
 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;
+import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.FakeEmailSender.Message;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import org.bouncycastle.bcpg.ArmoredOutputStream;
 import org.bouncycastle.openpgp.PGPPublicKey;
 import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.eclipse.jgit.api.errors.TransportException;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.PushCertificateIdent;
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
 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;
@@ -97,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
@@ -111,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
@@ -141,14 +171,31 @@
   }
 
   @Test
+  public void getByIntId() throws Exception {
+    AccountInfo info = gApi
+        .accounts()
+        .id("admin")
+        .get();
+    AccountInfo infoByIntId = gApi
+        .accounts()
+        .id(info._accountId)
+        .get();
+    assertThat(info.name).isEqualTo(infoByIntId.name);
+  }
+
+  @Test
   public void self() throws Exception {
     AccountInfo info = gApi
         .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
@@ -158,11 +205,139 @@
     gApi.accounts()
         .self()
         .starChange(triplet);
-    assertThat(info(triplet).starred).isTrue();
+    ChangeInfo change = info(triplet);
+    assertThat(change.starred).isTrue();
+    assertThat(change.stars).contains(DEFAULT_LABEL);
+
     gApi.accounts()
         .self()
         .unstarChange(triplet);
-    assertThat(info(triplet).starred).isNull();
+    change = info(triplet);
+    assertThat(change.starred).isNull();
+    assertThat(change.stars).isNull();
+  }
+
+  @Test
+  public void starUnstarChangeWithLabels() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String triplet = project.get() + "~master~" + r.getChangeId();
+    assertThat(gApi.accounts().self().getStars(triplet)).isEmpty();
+    assertThat(gApi.accounts().self().getStarredChanges()).isEmpty();
+
+    gApi.accounts().self().setStars(triplet,
+        new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "red", "blue")));
+    ChangeInfo change = info(triplet);
+    assertThat(change.starred).isTrue();
+    assertThat(change.stars)
+        .containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
+    assertThat(gApi.accounts().self().getStars(triplet))
+        .containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
+    List<ChangeInfo> starredChanges =
+        gApi.accounts().self().getStarredChanges();
+    assertThat(starredChanges).hasSize(1);
+    ChangeInfo starredChange = starredChanges.get(0);
+    assertThat(starredChange._number).isEqualTo(r.getChange().getId().get());
+    assertThat(starredChange.starred).isTrue();
+    assertThat(starredChange.stars)
+        .containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
+
+    gApi.accounts().self().setStars(triplet,
+        new StarsInput(ImmutableSet.of("yellow"),
+            ImmutableSet.of(DEFAULT_LABEL, "blue")));
+    change = info(triplet);
+    assertThat(change.starred).isNull();
+    assertThat(change.stars).containsExactly("red", "yellow").inOrder();
+    assertThat(gApi.accounts().self().getStars(triplet)).containsExactly(
+        "red", "yellow").inOrder();
+    starredChanges = gApi.accounts().self().getStarredChanges();
+    assertThat(starredChanges).hasSize(1);
+    starredChange = starredChanges.get(0);
+    assertThat(starredChange._number).isEqualTo(r.getChange().getId().get());
+    assertThat(starredChange.starred).isNull();
+    assertThat(starredChange.stars).containsExactly("red", "yellow").inOrder();
+
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage("not allowed to get stars of another account");
+    gApi.accounts().id(Integer.toString((admin.id.get()))).getStars(triplet);
+  }
+
+  @Test
+  public void starWithInvalidLabels() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String triplet = project.get() + "~master~" + r.getChangeId();
+    exception.expect(BadRequestException.class);
+    exception.expectMessage(
+        "invalid labels: another invalid label, invalid label");
+    gApi.accounts().self().setStars(triplet,
+        new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "invalid label", "blue",
+            "another invalid label")));
+  }
+
+  @Test
+  public void starWithDefaultAndIgnoreLabel() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String triplet = project.get() + "~master~" + r.getChangeId();
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("The labels " + DEFAULT_LABEL
+        + " and " + IGNORE_LABEL + " are mutually exclusive."
+        + " Only one of them can be set.");
+    gApi.accounts().self().setStars(triplet,
+        new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "blue", IGNORE_LABEL)));
+  }
+
+  @Test
+  public void ignoreChange() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    gApi.changes()
+        .id(r.getChangeId())
+        .addReviewer(in);
+
+    TestAccount user2 = accounts.user2();
+    in = new AddReviewerInput();
+    in.reviewer = user2.email;
+    gApi.changes()
+        .id(r.getChangeId())
+        .addReviewer(in);
+
+    setApiUser(user);
+    gApi.accounts().self().setStars(r.getChangeId(),
+        new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
+
+    sender.clear();
+    setApiUser(admin);
+    gApi.changes()
+        .id(r.getChangeId())
+        .abandon();
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    assertThat(messages.get(0).rcpt()).containsExactly(user2.emailAddress);
+  }
+
+  @Test
+  public void addReviewerToIgnoredChange() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    setApiUser(user);
+    gApi.accounts().self().setStars(r.getChangeId(),
+        new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
+
+    sender.clear();
+    setApiUser(admin);
+
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    gApi.changes()
+        .id(r.getChangeId())
+        .addReviewer(in);
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message message = messages.get(0);
+    assertThat(message.rcpt()).containsExactly(user.emailAddress);
+    assertMailFrom(message, admin.email);
   }
 
   @Test
@@ -206,6 +381,145 @@
   }
 
   @Test
+  public void fetchUserBranch() throws Exception {
+    // change something in the user preferences to ensure that the user branch
+    // is created
+    setApiUser(user);
+    GeneralPreferencesInfo input = new GeneralPreferencesInfo();
+    input.changesPerPage =
+        GeneralPreferencesInfo.defaults().changesPerPage + 10;
+    gApi.accounts().self().setPreferences(input);
+
+    TestRepository<InMemoryRepository> allUsersRepo =
+        cloneProject(allUsers, user);
+    String userRefName = RefNames.refsUsers(user.id);
+
+    // remove default READ permissions
+    ProjectConfig cfg = projectCache.checkedGet(allUsers).getConfig();
+    cfg.getAccessSection(
+        RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", true)
+        .remove(new Permission(Permission.READ));
+    saveProjectConfig(allUsers, cfg);
+
+    // deny READ permission that is inherited from All-Projects
+    deny(allUsers, Permission.READ, ANONYMOUS_USERS, RefNames.REFS + "*");
+
+    // fetching user branch without READ permission fails
+    try {
+      fetch(allUsersRepo, userRefName + ":userRef");
+      Assert.fail(
+          "user branch is visible although no READ permission is granted");
+    } catch (TransportException e) {
+      // expected because no READ granted on user branch
+    }
+
+    // allow each user to read its own user branch
+    grant(Permission.READ, allUsers,
+        RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", false,
+        REGISTERED_USERS);
+
+    // fetch user branch using refs/users/YY/XXXXXXX
+    fetch(allUsersRepo, userRefName + ":userRef");
+    Ref userRef = allUsersRepo.getRepository().exactRef("userRef");
+    assertThat(userRef).isNotNull();
+
+    // fetch user branch using refs/users/self
+    fetch(allUsersRepo, RefNames.REFS_USERS_SELF + ":userSelfRef");
+    Ref userSelfRef =
+        allUsersRepo.getRepository().getRefDatabase().exactRef("userSelfRef");
+    assertThat(userSelfRef).isNotNull();
+    assertThat(userSelfRef.getObjectId()).isEqualTo(userRef.getObjectId());
+
+    // fetching user branch of another user fails
+    String otherUserRefName = RefNames.refsUsers(admin.id);
+    exception.expect(TransportException.class);
+    exception.expectMessage(
+        "Remote does not have " + otherUserRefName + " available for fetch.");
+    fetch(allUsersRepo, otherUserRefName + ":otherUserRef");
+  }
+
+  @Test
+  public void pushToUserBranch() 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");
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
+    push.to(RefNames.refsUsers(admin.id)).assertOkStatus();
+
+    push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
+    push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
+  }
+
+  @Test
+  public void pushToUserBranchForReview() 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);
+
+    String userRefName = RefNames.refsUsers(admin.id);
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    fetch(allUsersRepo, userRefName + ":userRef");
+    allUsersRepo.reset("userRef");
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
+    PushOneCommit.Result r = push.to(MagicBranch.NEW_CHANGE + userRefName);
+    r.assertOkStatus();
+    assertThat(r.getChange().change().getDest().get()).isEqualTo(userRefName);
+    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).current().submit();
+
+    push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
+    r = push.to(MagicBranch.NEW_CHANGE + RefNames.REFS_USERS_SELF);
+    r.assertOkStatus();
+    assertThat(r.getChange().change().getDest().get()).isEqualTo(userRefName);
+    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).current().submit();
+  }
+
+  @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();
@@ -245,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());
@@ -320,6 +635,70 @@
         ImmutableList.of(key2.getKeyIdString()));
   }
 
+  @Test
+  public void sshKeys() throws Exception {
+    // The test account should initially have exactly one ssh key
+    List<SshKeyInfo> info = gApi.accounts().self().listSshKeys();
+    assertThat(info).hasSize(1);
+    assertSequenceNumbers(info);
+    SshKeyInfo key = info.get(0);
+    String inital = AccountCreator.publicKey(admin.sshKey, admin.email);
+    assertThat(key.sshPublicKey).isEqualTo(inital);
+
+    // Add a new key
+    String newKey = AccountCreator.publicKey(
+        AccountCreator.genSshKey(), admin.email);
+    gApi.accounts().self().addSshKey(newKey);
+    info = gApi.accounts().self().listSshKeys();
+    assertThat(info).hasSize(2);
+    assertSequenceNumbers(info);
+
+    // Add an existing key (the request succeeds, but the key isn't added again)
+    gApi.accounts().self().addSshKey(inital);
+    info = gApi.accounts().self().listSshKeys();
+    assertThat(info).hasSize(2);
+    assertSequenceNumbers(info);
+
+    // Add another new key
+    String newKey2 = AccountCreator.publicKey(
+        AccountCreator.genSshKey(), admin.email);
+    gApi.accounts().self().addSshKey(newKey2);
+    info = gApi.accounts().self().listSshKeys();
+    assertThat(info).hasSize(3);
+    assertSequenceNumbers(info);
+
+    // Delete second key
+    gApi.accounts().self().deleteSshKey(2);
+    info = gApi.accounts().self().listSshKeys();
+    assertThat(info).hasSize(2);
+    assertThat(info.get(0).seq).isEqualTo(1);
+    assertThat(info.get(1).seq).isEqualTo(3);
+  }
+
+  // reindex is tested by {@link AbstractQueryAccountsTest#reindex}
+  @Test
+  public void reindexPermissions() throws Exception {
+    // admin can reindex any account
+    setApiUser(admin);
+    gApi.accounts().id(user.username).index();
+
+    // user can reindex own account
+    setApiUser(user);
+    gApi.accounts().self().index();
+
+    // user cannot reindex any account
+    exception.expect(AuthException.class);
+    exception.expectMessage("not allowed to index account");
+    gApi.accounts().id(admin.username).index();
+  }
+
+  private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
+    int seq = 1;
+    for (SshKeyInfo key : sshKeys) {
+      assertThat(key.seq).isEqualTo(seq++);
+    }
+  }
+
   private PGPPublicKey getOnlyKeyFromStore(TestKey key) throws Exception {
     try (PublicKeyStore store = publicKeyStoreProvider.get()) {
       Iterable<PGPPublicKeyRing> keys = store.get(key.getKeyId());
@@ -434,4 +813,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..8cd696c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -0,0 +1,246 @@
+// 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 static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.fail;
+
+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.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.api.groups.GroupApi;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.common.AgreementInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
+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.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.TestTimeUtil;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+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;
+  }
+
+  @BeforeClass
+  public static void setTimeForTesting() {
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
+  }
+
+  @AfterClass
+  public static void restoreTime() {
+    TestTimeUtil.useSystemTime();
+  }
+
+  @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 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());
+
+    // Explicitly reset the user to force a new request context
+    setApiUser(user);
+
+    // Verify that the agreement was signed
+    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());
+
+    // 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();
+  }
+
+  @Test
+  public void revertChangeWithoutCLA() throws Exception {
+    assume().that(isContributorAgreementsEnabled()).isTrue();
+
+    // Create a change succeeds when agreement is not required
+    setUseContributorAgreements(InheritableBoolean.FALSE);
+    ChangeInfo change = gApi.changes().create(newChangeInput()).get();
+
+    // Approve and submit it
+    setApiUser(admin);
+    gApi.changes().id(change.changeId).current().review(ReviewInput.approve());
+    gApi.changes().id(change.changeId).current().submit(new SubmitInput());
+
+    // Revert is not allowed when CLA is required but not signed
+    setApiUser(user);
+    setUseContributorAgreements(InheritableBoolean.TRUE);
+    exception.expect(AuthException.class);
+    exception.expectMessage("A Contributor Agreement must be completed");
+    gApi.changes().id(change.changeId).revert();
+  }
+
+  @Test
+  public void cherrypickChangeWithoutCLA() throws Exception {
+    assume().that(isContributorAgreementsEnabled()).isTrue();
+
+    // Create a new branch
+    setApiUser(admin);
+    BranchInfo dest = gApi.projects().name(project.get())
+        .branch("cherry-pick-to").create(new BranchInput()).get();
+
+    // Create a change succeeds when agreement is not required
+    setUseContributorAgreements(InheritableBoolean.FALSE);
+    ChangeInfo change = gApi.changes().create(newChangeInput()).get();
+
+    // Approve and submit it
+    gApi.changes().id(change.changeId).current().review(ReviewInput.approve());
+    gApi.changes().id(change.changeId).current().submit(new SubmitInput());
+
+    // Cherry-pick is not allowed when CLA is required but not signed
+    setApiUser(user);
+    setUseContributorAgreements(InheritableBoolean.TRUE);
+    CherryPickInput in = new CherryPickInput();
+    in.destination = dest.ref;
+    in.message = change.subject;
+    exception.expect(AuthException.class);
+    exception.expectMessage("A Contributor Agreement must be completed");
+    gApi.changes().id(change.changeId).current().cherryPick(in);
+  }
+
+  @Test
+  public void createChangeRespectsCLA() throws Exception {
+    assume().that(isContributorAgreementsEnabled()).isTrue();
+
+    // Create a change succeeds when agreement is not required
+    setUseContributorAgreements(InheritableBoolean.FALSE);
+    gApi.changes().create(newChangeInput());
+
+    // Create a change is not allowed when CLA is required but not signed
+    setUseContributorAgreements(InheritableBoolean.TRUE);
+    try {
+      gApi.changes().create(newChangeInput());
+      fail("Expected AuthException");
+    } catch (AuthException e) {
+      assertThat(e.getMessage()).contains(
+          "A Contributor Agreement must be completed");
+    }
+
+    // Sign the agreement
+    gApi.accounts().self().signAgreement(ca.getName());
+
+    // Explicitly reset the user to force a new request context
+    setApiUser(user);
+
+    // Create a change succeeds after signing the agreement
+    gApi.changes().create(newChangeInput());
+  }
+
+  private ChangeInput newChangeInput() {
+    ChangeInput in = new ChangeInput();
+    in.branch = "master";
+    in.subject = "test";
+    in.project = project.get();
+    return in;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK
index 814dcf4..4e3c880 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK
@@ -1,7 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  group = 'api-account',
+  group = 'api_account',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUILD
new file mode 100644
index 0000000..9935eeb
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUILD
@@ -0,0 +1,7 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'api_account',
+  srcs = glob(['*IT.java']),
+  labels = ['api'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
new file mode 100644
index 0000000..9236176
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
@@ -0,0 +1,140 @@
+// 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.acceptance.api.accounts;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.AssertUtil.assertPrefs;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.extensions.client.Theme;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.errors.TransportException;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.After;
+import org.junit.Test;
+
+@NoHttpd
+public class DiffPreferencesIT extends AbstractDaemonTest {
+  @Inject
+  private AllUsersName allUsers;
+
+  @After
+  public void cleanUp() throws Exception {
+    gApi.accounts().id(admin.getId().toString())
+        .setDiffPreferences(DiffPreferencesInfo.defaults());
+
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    try {
+      fetch(allUsersRepo, RefNames.REFS_USERS_DEFAULT + ":defaults");
+    } catch (TransportException e) {
+      if (e.getMessage().equals("Remote does not have "
+          + RefNames.REFS_USERS_DEFAULT + " available for fetch.")) {
+        return;
+      }
+      throw e;
+    }
+    allUsersRepo.reset("defaults");
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo,
+        "Delete default preferences", VersionedAccountPreferences.PREFERENCES,
+        "");
+    push.rm(RefNames.REFS_USERS_DEFAULT).assertOkStatus();
+  }
+
+  @Test
+  public void getDiffPreferences() throws Exception {
+    DiffPreferencesInfo d = DiffPreferencesInfo.defaults();
+    DiffPreferencesInfo o = gApi.accounts()
+        .id(admin.getId().toString())
+        .getDiffPreferences();
+    assertPrefs(o, d);
+  }
+
+  @Test
+  public void setDiffPreferences() throws Exception {
+    DiffPreferencesInfo i = DiffPreferencesInfo.defaults();
+
+    // change all default values
+    i.context *= -1;
+    i.tabSize *= -1;
+    i.lineLength *= -1;
+    i.cursorBlinkRate = 500;
+    i.theme = Theme.MIDNIGHT;
+    i.ignoreWhitespace = Whitespace.IGNORE_ALL;
+    i.expandAllComments ^= true;
+    i.intralineDifference ^= true;
+    i.manualReview ^= true;
+    i.retainHeader ^= true;
+    i.showLineEndings ^= true;
+    i.showTabs ^= true;
+    i.showWhitespaceErrors ^= true;
+    i.skipDeleted ^= true;
+    i.skipUnchanged ^= true;
+    i.skipUncommented ^= true;
+    i.syntaxHighlighting ^= true;
+    i.hideTopMenu ^= true;
+    i.autoHideDiffTableHeader ^= true;
+    i.hideLineNumbers ^= true;
+    i.renderEntireFile ^= true;
+    i.hideEmptyPane ^= true;
+    i.matchBrackets ^= true;
+    i.lineWrapping ^= true;
+
+    DiffPreferencesInfo o = gApi.accounts()
+        .id(admin.getId().toString())
+        .setDiffPreferences(i);
+    assertPrefs(o, i);
+
+    // Partially fill input record
+    i = new DiffPreferencesInfo();
+    i.tabSize = 42;
+    DiffPreferencesInfo a = gApi.accounts()
+        .id(admin.getId().toString())
+        .setDiffPreferences(i);
+    assertPrefs(a, o, "tabSize");
+    assertThat(a.tabSize).isEqualTo(42);
+  }
+
+  @Test
+  public void getDiffPreferencesWithConfiguredDefaults() throws Exception {
+    DiffPreferencesInfo d = DiffPreferencesInfo.defaults();
+    int newLineLength = d.lineLength + 10;
+    int newTabSize = d.tabSize * 2;
+    DiffPreferencesInfo update = new DiffPreferencesInfo();
+    update.lineLength = newLineLength;
+    update.tabSize = newTabSize;
+    gApi.config().server().setDefaultDiffPreferences(update);
+
+    DiffPreferencesInfo o = gApi.accounts()
+        .id(admin.getId().toString())
+        .getDiffPreferences();
+
+    // assert configured defaults
+    assertThat(o.lineLength).isEqualTo(newLineLength);
+    assertThat(o.tabSize).isEqualTo(newTabSize);
+
+    // assert hard-coded defaults
+    assertPrefs(o, d, "lineLength", "tabSize");
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
new file mode 100644
index 0000000..9eb6918
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
@@ -0,0 +1,104 @@
+// 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.api.accounts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.client.KeyMapType;
+import com.google.gerrit.extensions.client.Theme;
+
+import org.junit.Test;
+
+@NoHttpd
+public class EditPreferencesIT extends AbstractDaemonTest {
+  @Test
+  public void getSetEditPreferences() throws Exception {
+    EditPreferencesInfo out = gApi.accounts()
+        .id(admin.getId().toString())
+        .getEditPreferences();
+
+    assertThat(out.lineLength).isEqualTo(100);
+    assertThat(out.indentUnit).isEqualTo(2);
+    assertThat(out.tabSize).isEqualTo(8);
+    assertThat(out.cursorBlinkRate).isEqualTo(0);
+    assertThat(out.hideTopMenu).isNull();
+    assertThat(out.showTabs).isTrue();
+    assertThat(out.showWhitespaceErrors).isNull();
+    assertThat(out.syntaxHighlighting).isTrue();
+    assertThat(out.hideLineNumbers).isNull();
+    assertThat(out.matchBrackets).isTrue();
+    assertThat(out.lineWrapping).isNull();
+    assertThat(out.autoCloseBrackets).isNull();
+    assertThat(out.showBase).isNull();
+    assertThat(out.theme).isEqualTo(Theme.DEFAULT);
+    assertThat(out.keyMapType).isEqualTo(KeyMapType.DEFAULT);
+
+    // change some default values
+    out.lineLength = 80;
+    out.indentUnit = 4;
+    out.tabSize = 4;
+    out.cursorBlinkRate = 500;
+    out.hideTopMenu = true;
+    out.showTabs = false;
+    out.showWhitespaceErrors = true;
+    out.syntaxHighlighting = false;
+    out.hideLineNumbers = true;
+    out.matchBrackets = false;
+    out.lineWrapping = true;
+    out.autoCloseBrackets = true;
+    out.showBase = true;
+    out.theme = Theme.TWILIGHT;
+    out.keyMapType = KeyMapType.EMACS;
+
+    EditPreferencesInfo info = gApi.accounts()
+        .id(admin.getId().toString())
+        .setEditPreferences(out);
+
+    assertEditPreferences(info, out);
+
+    // Partially filled input record
+    EditPreferencesInfo in = new EditPreferencesInfo();
+    in.tabSize = 42;
+
+    info = gApi.accounts()
+        .id(admin.getId().toString())
+        .setEditPreferences(in);
+
+    out.tabSize = in.tabSize;
+    assertEditPreferences(info, out);
+  }
+
+  private void assertEditPreferences(EditPreferencesInfo out,
+      EditPreferencesInfo in) throws Exception {
+    assertThat(out.lineLength).isEqualTo(in.lineLength);
+    assertThat(out.indentUnit).isEqualTo(in.indentUnit);
+    assertThat(out.tabSize).isEqualTo(in.tabSize);
+    assertThat(out.cursorBlinkRate).isEqualTo(in.cursorBlinkRate);
+    assertThat(out.hideTopMenu).isEqualTo(in.hideTopMenu);
+    assertThat(out.showTabs).isNull();
+    assertThat(out.showWhitespaceErrors).isEqualTo(in.showWhitespaceErrors);
+    assertThat(out.syntaxHighlighting).isNull();
+    assertThat(out.hideLineNumbers).isEqualTo(in.hideLineNumbers);
+    assertThat(out.matchBrackets).isNull();
+    assertThat(out.lineWrapping).isEqualTo(in.lineWrapping);
+    assertThat(out.autoCloseBrackets).isEqualTo(in.autoCloseBrackets);
+    assertThat(out.showBase).isEqualTo(in.showBase);
+    assertThat(out.theme).isEqualTo(in.theme);
+    assertThat(out.keyMapType).isEqualTo(in.keyMapType);
+  }
+}
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
new file mode 100644
index 0000000..f45bfbbe
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -0,0 +1,127 @@
+// 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.acceptance.api.accounts;
+
+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.acceptance.TestAccount;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DateFormat;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
+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;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+@NoHttpd
+public class GeneralPreferencesIT extends AbstractDaemonTest {
+  @Inject
+  private AllUsersName allUsers;
+
+  private TestAccount user42;
+
+  @Before
+  public void setUp() throws Exception {
+    String name = name("user42");
+    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()
+        .id(user42.id.toString())
+        .getPreferences();
+    assertPrefs(o, GeneralPreferencesInfo.defaults(), "my");
+    assertThat(o.my).hasSize(7);
+
+    GeneralPreferencesInfo i = GeneralPreferencesInfo.defaults();
+
+    // change all default values
+    i.changesPerPage *= -1;
+    i.showSiteHeader ^= true;
+    i.useFlashClipboard ^= true;
+    i.downloadCommand = DownloadCommand.REPO_DOWNLOAD;
+    i.dateFormat = DateFormat.US;
+    i.timeFormat = TimeFormat.HHMM_24;
+    i.emailStrategy = EmailStrategy.DISABLED;
+    i.relativeDateInChangeTable ^= true;
+    i.sizeBarInChangeTable ^= true;
+    i.legacycidInChangeTable ^= true;
+    i.muteCommonPathPrefixes ^= true;
+    i.signedOffBy ^= true;
+    i.reviewCategoryStrategy = ReviewCategoryStrategy.ABBREV;
+    i.diffView = DiffView.UNIFIED_DIFF;
+    i.my = new ArrayList<>();
+    i.my.add(new MenuItem("name", "url"));
+    i.urlAliases = new HashMap<>();
+    i.urlAliases.put("foo", "bar");
+
+    o = gApi.accounts()
+        .id(user42.getId().toString())
+        .setPreferences(i);
+    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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK
index 5db2054..e8963be 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK
@@ -1,7 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  group = 'api-change',
+  group = 'api_change',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUILD
new file mode 100644
index 0000000..2502cad
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUILD
@@ -0,0 +1,7 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'api_change',
+  srcs = glob(['*IT.java']),
+  labels = ['api'],
+)
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 d92a887..15ac366 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -15,56 +15,110 @@
 package com.google.gerrit.acceptance.api.change;
 
 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.pushHead;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.extensions.client.ReviewerState.CC;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.Util.blockLabel;
 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.common.collect.Sets;
 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.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.RevisionApi;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.SubmitType;
+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.ChangeInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 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;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.Util;
+import com.google.gerrit.testutil.FakeEmailSender.Message;
+import com.google.gerrit.testutil.NoteDbMode;
+import com.google.gerrit.testutil.TestTimeUtil;
 
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Constants;
+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.PushResult;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.EnumSet;
+import java.util.Iterator;
 import java.util.List;
-import java.util.Set;
+import java.util.Map;
 
 @NoHttpd
 public class ChangeIT extends AbstractDaemonTest {
+  private String systemTimeZone;
+
+  @Before
+  public void setTimeForTesting() {
+    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
+  }
+
+  @After
+  public void resetTime() {
+    TestTimeUtil.useSystemTime();
+    System.setProperty("user.timezone", systemTimeZone);
+  }
 
   @Test
   public void get() throws Exception {
@@ -76,6 +130,7 @@
     assertThat(c.branch).isEqualTo("master");
     assertThat(c.status).isEqualTo(ChangeStatus.NEW);
     assertThat(c.subject).isEqualTo("test commit");
+    assertThat(c.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
     assertThat(c.mergeable).isTrue();
     assertThat(c.changeId).isEqualTo(r.getChangeId());
     assertThat(c.created).isEqualTo(c.updated);
@@ -89,34 +144,85 @@
   }
 
   @Test
+  public void getAmbiguous() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    String changeId = r1.getChangeId();
+    gApi.changes().id(changeId).get();
+
+    BranchInput b = new BranchInput();
+    b.revision = repo().exactRef("HEAD").getObjectId().name();
+    gApi.projects()
+        .name(project.get())
+        .branch("other")
+        .create(b);
+
+    PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME,
+        PushOneCommit.FILE_CONTENT, changeId);
+    PushOneCommit.Result r2 = push2.to("refs/for/other");
+    assertThat(r2.getChangeId()).isEqualTo(changeId);
+
+    exception.expect(ResourceNotFoundException.class);
+    exception.expectMessage("Multiple changes found for " + changeId);
+    gApi.changes().id(changeId).get();
+  }
+
+  @Test
   public void abandon() throws Exception {
     PushOneCommit.Result r = createChange();
-    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.NEW);
+    String changeId = r.getChangeId();
+    assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
     gApi.changes()
-        .id(r.getChangeId())
+        .id(changeId)
         .abandon();
-    ChangeInfo info = get(r.getChangeId());
+    ChangeInfo info = get(changeId);
     assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
     assertThat(Iterables.getLast(info.messages).message.toLowerCase())
         .contains("abandoned");
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("change is abandoned");
+    gApi.changes()
+        .id(changeId)
+        .abandon();
+  }
+
+  @Test
+  public void abandonDraft() throws Exception {
+    PushOneCommit.Result r = createDraftChange();
+    String changeId = r.getChangeId();
+    assertThat(info(changeId).status).isEqualTo(ChangeStatus.DRAFT);
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("draft changes cannot be abandoned");
+    gApi.changes()
+        .id(changeId)
+        .abandon();
   }
 
   @Test
   public void restore() throws Exception {
     PushOneCommit.Result r = createChange();
-    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.NEW);
+    String changeId = r.getChangeId();
+    assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
     gApi.changes()
-        .id(r.getChangeId())
+        .id(changeId)
         .abandon();
-    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.ABANDONED);
+    assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED);
 
     gApi.changes()
-        .id(r.getChangeId())
+        .id(changeId)
         .restore();
-    ChangeInfo info = get(r.getChangeId());
+    ChangeInfo info = get(changeId);
     assertThat(info.status).isEqualTo(ChangeStatus.NEW);
     assertThat(Iterables.getLast(info.messages).message.toLowerCase())
         .contains("restored");
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("change is new");
+    gApi.changes()
+        .id(changeId)
+        .restore();
   }
 
   @Test
@@ -144,8 +250,7 @@
         gApi.changes().id(r.getChangeId()).get().messages);
     assertThat(sourceMessages).hasSize(4);
     String expectedMessage = String.format(
-        "Patch Set 1: Reverted\n\n" +
-        "This patchset was reverted in change: %s",
+        "Created a revert of this change as %s",
         revertChange.changeId);
     assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage);
 
@@ -188,29 +293,102 @@
     revision.review(ReviewInput.approve());
     revision.submit();
 
+    String changeId = r2.getChangeId();
     // Rebase the second change
     gApi.changes()
-        .id(r2.getChangeId())
+        .id(changeId)
         .current()
         .rebase();
 
     // Second change should have 2 patch sets
-    assertThat(r2.getPatchSetId().get()).isEqualTo(2);
+    ChangeInfo c2 = gApi.changes().id(changeId).get();
+    assertThat(c2.revisions.get(c2.currentRevision)._number).isEqualTo(2);
 
     // ...and the committer should be correct
     ChangeInfo info = gApi.changes()
-        .id(r2.getChangeId()).get(EnumSet.of(
+        .id(changeId).get(EnumSet.of(
             ListChangesOption.CURRENT_REVISION,
             ListChangesOption.CURRENT_COMMIT));
     GitPerson committer = info.revisions.get(
         info.currentRevision).commit.committer;
     assertThat(committer.name).isEqualTo(admin.fullName);
     assertThat(committer.email).isEqualTo(admin.email);
+
+    // Rebasing the second change again should fail
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Change is already up to date");
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .rebase();
   }
 
-  @Test(expected = ResourceConflictException.class)
+  @Test
+  public void publish() throws Exception {
+    PushOneCommit.Result r = createChange("refs/drafts/master");
+    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.DRAFT);
+    gApi.changes()
+      .id(r.getChangeId())
+      .publish();
+    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.NEW);
+  }
+
+  @Test
+  public void delete() throws Exception {
+    PushOneCommit.Result r = createChange("refs/drafts/master");
+    assertThat(query(r.getChangeId())).hasSize(1);
+    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.DRAFT);
+    gApi.changes()
+      .id(r.getChangeId())
+      .delete();
+    assertThat(query(r.getChangeId())).isEmpty();
+  }
+
+  @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();
+    String forCodeReviewAs = Permission.forLabelAs(codeReviewType.getName());
+    String heads = "refs/heads/*";
+    AccountGroup.UUID owner =
+        SystemGroupBackend.getGroup(CHANGE_OWNER).getUUID();
+    Util.allow(cfg, forCodeReviewAs, -1, 1, owner, heads);
+    saveProjectConfig(project, cfg);
+
+    PushOneCommit.Result r = createChange();
+    RevisionApi revision = gApi.changes()
+        .id(r.getChangeId())
+        .current();
+
+    ReviewInput in = ReviewInput.recommend();
+    in.onBehalfOf = user.id.toString();
+    revision.review(in);
+
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+
+    LabelInfo codeReview = c.labels.get("Code-Review");
+    assertThat(codeReview.all).hasSize(1);
+    ApprovalInfo approval = codeReview.all.get(0);
+    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval.value).isEqualTo(1);
+  }
+
+  @Test
   public void rebaseUpToDateChange() throws Exception {
     PushOneCommit.Result r = createChange();
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Change is already up to date");
     gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().name())
@@ -285,41 +463,246 @@
     assertThat(r1.getPatchSetId().get()).isEqualTo(3);
   }
 
-  @Test(expected = ResourceConflictException.class)
+  @Test
   public void rebaseChangeBaseRecursion() throws Exception {
     PushOneCommit.Result r1 = createChange();
     PushOneCommit.Result r2 = createChange();
 
     RebaseInput ri = new RebaseInput();
     ri.base = r2.getCommit().name();
+    String expectedMessage = "base change " + r2.getChangeId()
+        + " is a descendant of the current change - recursion not allowed";
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage(expectedMessage);
     gApi.changes()
         .id(r1.getChangeId())
         .revision(r1.getCommit().name())
         .rebase(ri);
   }
 
-  private Set<Account.Id> getReviewers(String changeId) throws Exception {
-    ChangeInfo ci = gApi.changes().id(changeId).get();
-    Set<Account.Id> result = Sets.newHashSet();
-    for (LabelInfo li : ci.labels.values()) {
-      for (ApprovalInfo ai : li.all) {
-        result.add(new Account.Id(ai._accountId));
-      }
+  @Test
+  public void rebaseAbandonedChange() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
+    gApi.changes()
+        .id(changeId)
+        .abandon();
+    ChangeInfo info = get(changeId);
+    assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("change is abandoned");
+    gApi.changes()
+        .id(changeId)
+        .revision(r.getCommit().name())
+        .rebase();
+  }
+
+  @Test
+  public void rebaseOntoAbandonedChange() throws Exception {
+    // Create two changes both with the same parent
+    PushOneCommit.Result r = createChange();
+    testRepo.reset("HEAD~1");
+    PushOneCommit.Result r2 = createChange();
+
+    // Abandon the first change
+    String changeId = r.getChangeId();
+    assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
+    gApi.changes()
+        .id(changeId)
+        .abandon();
+    ChangeInfo info = get(changeId);
+    assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
+
+    RebaseInput ri = new RebaseInput();
+    ri.base = r.getCommit().name();
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("base change is abandoned: " + changeId);
+    gApi.changes()
+        .id(r2.getChangeId())
+        .revision(r2.getCommit().name())
+        .rebase(ri);
+  }
+
+  @Test
+  public void rebaseOntoSelf() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String commit = r.getCommit().name();
+    RebaseInput ri = new RebaseInput();
+    ri.base = commit;
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("cannot rebase change onto itself");
+    gApi.changes()
+        .id(changeId)
+        .revision(commit)
+        .rebase(ri);
+  }
+
+  @Test
+  @TestProjectInput(createEmptyCommit = false)
+  public void changeNoParentToOneParent() throws Exception {
+    // create initial commit with no parent and push it as change, so that patch
+    // set 1 has no parent
+    RevCommit c =
+        testRepo.commit().message("Initial commit").insertChangeId().create();
+    String id = GitUtil.getChangeId(testRepo, c).get();
+    testRepo.reset(c);
+
+    PushResult pr = pushHead(testRepo, "refs/for/master", false);
+    assertPushOk(pr, "refs/for/master");
+
+    ChangeInfo change = gApi.changes().id(id).get();
+    assertThat(change.revisions.get(change.currentRevision).commit.parents)
+        .isEmpty();
+
+    // create another initial commit with no parent and push it directly into
+    // the remote repository
+    c = testRepo.amend(c.getId()).message("Initial Empty Commit").create();
+    testRepo.reset(c);
+    pr = pushHead(testRepo, "refs/heads/master", false);
+    assertPushOk(pr, "refs/heads/master");
+
+    // create a successor commit and push it as second patch set to the change,
+    // so that patch set 2 has 1 parent
+    RevCommit c2 = testRepo.commit().message("Initial commit").parent(c)
+        .insertChangeId(id.substring(1)).create();
+    testRepo.reset(c2);
+
+    pr = pushHead(testRepo, "refs/for/master", false);
+    assertPushOk(pr, "refs/for/master");
+
+    change = gApi.changes().id(id).get();
+    RevisionInfo rev = change.revisions.get(change.currentRevision);
+    assertThat(rev.commit.parents).hasSize(1);
+    assertThat(rev.commit.parents.get(0).commit).isEqualTo(c.name());
+
+    // check that change kind is correctly detected as REWORK
+    assertThat(rev.kind).isEqualTo(ChangeKind.REWORK);
+  }
+
+  @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.
     }
-    return result;
+
+    // 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);
     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;
     gApi.changes()
         .id(r.getChangeId())
         .addReviewer(in);
 
-    assertThat(getReviewers(r.getChangeId()))
-        .containsExactlyElementsIn(ImmutableSet.of(user.id));
+    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);
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+
+    // 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.
+    Collection<AccountInfo> reviewers = NoteDbMode.readWrite()
+        ? c.reviewers.get(REVIEWER)
+        : c.reviewers.get(CC);
+    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 = NoteDbMode.readWrite()
+        ? c.reviewers.get(REVIEWER)
+        : c.reviewers.get(CC);
+    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
@@ -334,16 +717,314 @@
         .revision(r.getCommit().name())
         .submit();
 
-    assertThat(getReviewers(r.getChangeId()))
-      .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+    Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId)
+        .isEqualTo(admin.getId().get());
+    assertThat(c.reviewers).doesNotContainKey(CC);
 
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = user.email;
     gApi.changes()
         .id(r.getChangeId())
         .addReviewer(in);
-    assertThat(getReviewers(r.getChangeId()))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.id));
+
+    c = gApi.changes()
+        .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());
+    }
+  }
+
+  @Test
+  public void listVotes() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+
+    Map<String, Short> m = gApi.changes()
+        .id(r.getChangeId())
+        .reviewer(admin.getId().toString())
+        .votes();
+
+    assertThat(m).hasSize(1);
+    assertThat(m).containsEntry("Code-Review", Short.valueOf((short)2));
+
+    setApiUser(user);
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.dislike());
+
+    m = gApi.changes()
+        .id(r.getChangeId())
+        .reviewer(user.getId().toString())
+        .votes();
+
+    assertThat(m).hasSize(1);
+    assertThat(m).containsEntry("Code-Review", Short.valueOf((short)-1));
+  }
+
+  @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()
+        .id(changeId)
+        .addReviewer(user.getId().toString());
+
+    // ReviewerState will vary between ReviewDb and NoteDb; we just care that it
+    // shows up somewhere.
+    Iterable<AccountInfo> reviewers = Iterables.concat(
+        gApi.changes().id(changeId).get().reviewers.values());
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId)
+        .isEqualTo(user.getId().get());
+
+    gApi.changes()
+        .id(changeId)
+        .reviewer(user.getId().toString())
+        .remove();
+    assertThat(gApi.changes().id(changeId).get().reviewers.isEmpty());
+
+    // 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)
+        .reviewer(user.getId().toString())
+        .remove();
+  }
+
+  @Test
+  public void removeReviewer() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    gApi.changes()
+        .id(changeId)
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+
+    setApiUser(user);
+    gApi.changes()
+        .id(changeId)
+        .revision(r.getCommit().name())
+        .review(ReviewInput.recommend());
+
+    Collection<AccountInfo> reviewers = gApi.changes()
+        .id(changeId)
+        .get()
+        .reviewers.get(REVIEWER);
+
+    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());
+
+    setApiUser(admin);
+    gApi.changes()
+        .id(changeId)
+        .reviewer(user.getId().toString())
+        .remove();
+
+    reviewers = gApi.changes()
+        .id(changeId)
+        .get()
+        .reviewers.get(REVIEWER);
+    assertThat(reviewers).hasSize(1);
+    reviewerIt = reviewers.iterator();
+    assertThat(reviewerIt.next()._accountId)
+      .isEqualTo(admin.getId().get());
+
+    eventRecorder.assertReviewerDeletedEvents(changeId, user.email);
+  }
+
+  @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()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+
+    setApiUser(user);
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.recommend());
+
+    setApiUser(admin);
+    sender.clear();
+    gApi.changes()
+        .id(r.getChangeId())
+        .reviewer(user.getId().toString())
+        .deleteVote("Code-Review");
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message msg = messages.get(0);
+    assertThat(msg.rcpt()).containsExactly(user.emailAddress);
+    assertThat(msg.body()).contains(
+        admin.fullName + " has removed a vote on this change.\n");
+    assertThat(msg.body()).contains(
+        "Removed Code-Review+1 by "
+            + user.fullName + " <" + user.email + ">" + "\n");
+
+    Map<String, Short> m = gApi.changes()
+        .id(r.getChangeId())
+        .reviewer(user.getId().toString())
+        .votes();
+
+    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, hence there is no dummy 0 approval left when a vote is
+      // deleted.
+      assertThat(m).isEmpty();
+    } else {
+      // When NoteDb is disabled there is a dummy 0 approval on the change so
+      // that the user is still returned as CC when all votes of that user have
+      // been deleted.
+      assertThat(m).containsEntry("Code-Review", Short.valueOf((short)0));
+    }
+
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+
+    ChangeMessageInfo message = Iterables.getLast(c.messages);
+    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()));
+    }
+  }
+
+  @Test
+  public void deleteVoteNotifyNone() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+
+    setApiUser(user);
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.recommend());
+
+    setApiUser(admin);
+    sender.clear();
+    DeleteVoteInput in = new DeleteVoteInput();
+    in.label = "Code-Review";
+    in.notify = NotifyHandling.NONE;
+    gApi.changes()
+        .id(r.getChangeId())
+        .reviewer(user.getId().toString())
+        .deleteVote(in);
+    assertThat(sender.getMessages()).hasSize(0);
+  }
+
+  @Test
+  public void deleteVoteNotPermitted() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage("delete vote not permitted");
+    gApi.changes()
+        .id(r.getChangeId())
+        .reviewer(admin.getId().toString())
+        .deleteVote("Code-Review");
   }
 
   @Test
@@ -375,8 +1056,10 @@
         .review(input);
 
     // Reviewers should only be "admin"
-    assertThat(getReviewers(changeId))
+    ChangeInfo c = gApi.changes().id(changeId).get();
+    assertThat(getReviewers(c.reviewers.get(REVIEWER)))
         .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
+    assertThat(c.reviewers.get(CC)).isNull();
 
     // Add the user as reviewer
     AddReviewerInput in = new AddReviewerInput();
@@ -384,8 +1067,17 @@
     gApi.changes()
         .id(changeId)
         .addReviewer(in);
-    assertThat(getReviewers(changeId))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+    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()));
+    }
 
     // Approve the change as user, then remove the approval
     // (only to confirm that the user does have Code-Review+2 permission)
@@ -407,13 +1099,22 @@
         .submit();
 
     // User should still be on the change
-    assertThat(getReviewers(changeId))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+    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()));
+    }
   }
 
   @Test
   public void createEmptyChange() throws Exception {
-    ChangeInfo in = new ChangeInfo();
+    ChangeInput in = new ChangeInput();
     in.branch = Constants.MASTER;
     in.subject = "Create a change from the API";
     in.project = project.get();
@@ -496,9 +1197,24 @@
   @Test
   public void queryChangesOptions() throws Exception {
     PushOneCommit.Result r = createChange();
+
     ChangeInfo result = Iterables.getOnlyElement(gApi.changes()
         .query(r.getChangeId())
-        .withOptions(EnumSet.allOf(ListChangesOption.class))
+        .get());
+    assertThat(result.labels).isNull();
+    assertThat(result.messages).isNull();
+    assertThat(result.actions).isNull();
+    assertThat(result.revisions).isNull();
+
+    EnumSet<ListChangesOption> options = EnumSet.of(
+        ListChangesOption.ALL_REVISIONS,
+        ListChangesOption.CHANGE_ACTIONS,
+        ListChangesOption.CURRENT_ACTIONS,
+        ListChangesOption.DETAILED_LABELS,
+        ListChangesOption.MESSAGES);
+    result = Iterables.getOnlyElement(gApi.changes()
+        .query(r.getChangeId())
+        .withOptions(options)
         .get());
     assertThat(Iterables.getOnlyElement(result.labels.keySet()))
         .isEqualTo("Code-Review");
@@ -560,7 +1276,28 @@
   }
 
   @Test
+  public void submitted() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .info().submitted).isNull();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .submit();
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .info().submitted).isNotNull();
+  }
+
+  @Test
   public void check() throws Exception {
+    // TODO(dborowitz): Re-enable when ConsistencyChecker supports NoteDb.
+    assume().that(notesMigration.enabled()).isFalse();
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes()
         .id(r.getChangeId())
@@ -637,6 +1374,7 @@
 
   @Test
   public void defaultSearchDoesNotTouchDatabase() throws Exception {
+    setApiUser(admin);
     PushOneCommit.Result r1 = createChange();
     gApi.changes()
         .id(r1.getChangeId())
@@ -648,18 +1386,23 @@
         .submit();
 
     createChange();
+    createDraftChange();
 
-    setApiUserAnonymous(); // Identified user may async get stars from DB.
-    atrScope.disableDb();
-    assertThat(gApi.changes().query()
-          .withQuery(
-            "project:{" + project.get() + "} (status:open OR status:closed)")
-          // Options should match defaults in AccountDashboardScreen.
-          .withOption(ListChangesOption.LABELS)
-          .withOption(ListChangesOption.DETAILED_ACCOUNTS)
-          .withOption(ListChangesOption.REVIEWED)
-          .get())
-        .hasSize(2);
+    setApiUser(user);
+    AcceptanceTestRequestScope.Context ctx = disableDb();
+    try {
+      assertThat(gApi.changes().query()
+            .withQuery(
+              "project:{" + project.get() + "} (status:open OR status:closed)")
+            // Options should match defaults in AccountDashboardScreen.
+            .withOption(ListChangesOption.LABELS)
+            .withOption(ListChangesOption.DETAILED_ACCOUNTS)
+            .withOption(ListChangesOption.REVIEWED)
+            .get())
+          .hasSize(2);
+    } finally {
+      enableDb(ctx);
+    }
   }
 
   @Test
@@ -688,6 +1431,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());
@@ -710,4 +1457,325 @@
     assertThat(rev2.pushCertificate.certificate).isNull();
     assertThat(rev2.pushCertificate.key).isNull();
   }
+
+  @Test
+  public void anonymousRestApi() throws Exception {
+    setApiUserAnonymous();
+    PushOneCommit.Result r = createChange();
+
+    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    assertThat(info.changeId).isEqualTo(r.getChangeId());
+
+    String triplet = project.get() + "~master~" + r.getChangeId();
+    info = gApi.changes().id(triplet).get();
+    assertThat(info.changeId).isEqualTo(r.getChangeId());
+
+    info = gApi.changes().id(info._number).get();
+    assertThat(info.changeId).isEqualTo(r.getChangeId());
+
+    exception.expect(AuthException.class);
+    gApi.changes()
+        .id(triplet)
+        .current()
+        .review(ReviewInput.approve());
+  }
+
+  @Test
+  public void noteDbCommitsOnPatchSetCreation() throws Exception {
+    assume().that(notesMigration.readChanges()).isTrue();
+
+    PushOneCommit.Result r = createChange();
+    pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+        "b.txt", "4711", r.getChangeId()).to("refs/for/master").assertOkStatus();
+    ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      RevCommit commitPatchSetCreation = rw.parseCommit(
+          repo.exactRef(changeMetaRef(new Change.Id(c._number))).getObjectId());
+
+      assertThat(commitPatchSetCreation.getShortMessage())
+          .isEqualTo("Create patch set 2");
+      PersonIdent expectedAuthor = changeNoteUtil.newIdent(
+          accountCache.get(admin.id).getAccount(), c.updated,
+          serverIdent.get(), AnonymousCowardNameProvider.DEFAULT);
+      assertThat(commitPatchSetCreation.getAuthorIdent())
+          .isEqualTo(expectedAuthor);
+      assertThat(commitPatchSetCreation.getCommitterIdent())
+          .isEqualTo(new PersonIdent(serverIdent.get(), c.updated));
+      assertThat(commitPatchSetCreation.getParentCount()).isEqualTo(1);
+
+      RevCommit commitChangeCreation =
+          rw.parseCommit(commitPatchSetCreation.getParent(0));
+      assertThat(commitChangeCreation.getShortMessage())
+          .isEqualTo("Create change");
+      expectedAuthor = changeNoteUtil.newIdent(
+          accountCache.get(admin.id).getAccount(), c.created, serverIdent.get(),
+          AnonymousCowardNameProvider.DEFAULT);
+      assertThat(commitChangeCreation.getAuthorIdent())
+          .isEqualTo(expectedAuthor);
+      assertThat(commitChangeCreation.getCommitterIdent())
+          .isEqualTo(new PersonIdent(serverIdent.get(), c.created));
+      assertThat(commitChangeCreation.getParentCount()).isEqualTo(0);
+    }
+  }
+
+  @Test
+  public void createEmptyChangeOnNonExistingBranch() throws Exception {
+    ChangeInput in = new ChangeInput();
+    in.branch = "foo";
+    in.subject = "Create a change on new branch from the API";
+    in.project = project.get();
+    in.newBranch = true;
+    ChangeInfo info = gApi
+        .changes()
+        .create(in)
+        .get();
+    assertThat(info.project).isEqualTo(in.project);
+    assertThat(info.branch).isEqualTo(in.branch);
+    assertThat(info.subject).isEqualTo(in.subject);
+    assertThat(Iterables.getOnlyElement(info.messages).message)
+        .isEqualTo("Uploaded patch set 1.");
+  }
+
+  @Test
+  public void createEmptyChangeOnExistingBranchWithNewBranch() throws Exception {
+    ChangeInput in = new ChangeInput();
+    in.branch = Constants.MASTER;
+    in.subject = "Create a change on new branch from the API";
+    in.project = project.get();
+    in.newBranch = true;
+
+    exception.expect(ResourceConflictException.class);
+    gApi.changes()
+        .create(in)
+        .get();
+  }
+
+  @Test
+  public void createNewPatchSetOnVisibleDraftPatchSet() throws Exception {
+    // Clone separate repositories of the same project as admin and as user
+    TestRepository<InMemoryRepository> adminTestRepo =
+        cloneProject(project, admin);
+    TestRepository<InMemoryRepository> 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();
+
+    // Amend draft as admin
+    PushOneCommit.Result r2 = amendChange(
+        r1.getChangeId(), "refs/drafts/master", admin, adminTestRepo);
+    r2.assertOkStatus();
+
+    // Add user as reviewer to make this patch set visible
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    gApi.changes()
+        .id(r1.getChangeId())
+        .addReviewer(in);
+
+    // Fetch change
+    GitUtil.fetch(userTestRepo, r2.getPatchSet().getRefName() + ":ps");
+    userTestRepo.reset("ps");
+
+    // Amend change as user
+    PushOneCommit.Result r3 = amendChange(
+        r2.getChangeId(), "refs/drafts/master", user, userTestRepo);
+    r3.assertOkStatus();
+  }
+
+  @Test
+  public void createNewPatchSetOnInvisibleDraftPatchSet() throws Exception {
+    // Clone separate repositories of the same project as admin and as user
+    TestRepository<InMemoryRepository> adminTestRepo =
+        cloneProject(project, admin);
+    TestRepository<InMemoryRepository> 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();
+
+    // Amend draft as admin
+    PushOneCommit.Result r2 = amendChange(
+        r1.getChangeId(), "refs/drafts/master", admin, adminTestRepo);
+    r2.assertOkStatus();
+
+    // Fetch change
+    GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps");
+    userTestRepo.reset("ps");
+
+    // Amend change as user
+    PushOneCommit.Result r3 = amendChange(
+        r1.getChangeId(), "refs/for/master", user, userTestRepo);
+    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>() {
+      @Override
+      public Account.Id apply(AccountInfo account) {
+        return new Account.Id(account._accountId);
+      }
+    });
+  }
+
+  private ChangeResource parseResource(PushOneCommit.Result r)
+      throws Exception {
+    List<ChangeControl> ctls = changeFinder.find(
+        r.getChangeId(), atrScope.get().getUser());
+    assertThat(ctls).hasSize(1);
+    return changeResourceFactory.create(ctls.get(0));
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java
deleted file mode 100644
index 475803a..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java
+++ /dev/null
@@ -1,90 +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.api.change;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.extensions.api.changes.FixInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.client.ChangeStatus;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ProblemInfo;
-import com.google.gerrit.reviewdb.client.Change;
-
-import org.junit.Test;
-
-import java.util.Collections;
-import java.util.List;
-
-@NoHttpd
-public class CheckIT extends AbstractDaemonTest {
-  // Most types of tests belong in ConsistencyCheckerTest; these mostly just
-  // test paths outside of ConsistencyChecker, like API wiring.
-  @Test
-  public void currentPatchSetMissing() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change c = getChange(r);
-    db.patchSets().deleteKeys(Collections.singleton(c.currentPatchSetId()));
-    indexer.index(db, c);
-
-    List<ProblemInfo> problems = gApi.changes()
-        .id(r.getChangeId())
-        .check()
-        .problems;
-    assertThat(problems).hasSize(1);
-    assertThat(problems.get(0).message)
-        .isEqualTo("Current patch set 1 not found");
-  }
-
-  @Test
-  public void fixReturnsUpdatedValue() throws Exception {
-    PushOneCommit.Result r = createChange();
-    gApi.changes()
-        .id(r.getChangeId())
-        .revision(r.getCommit().name())
-        .review(ReviewInput.approve());
-    gApi.changes()
-        .id(r.getChangeId())
-        .revision(r.getCommit().name())
-        .submit();
-
-    Change c = getChange(r);
-    c.setStatus(Change.Status.NEW);
-    db.changes().update(Collections.singleton(c));
-    indexer.index(db, c);
-
-    ChangeInfo info = gApi.changes()
-        .id(r.getChangeId())
-        .check();
-    assertThat(info.problems).hasSize(1);
-    assertThat(info.problems.get(0).status).isNull();
-    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
-
-    info = gApi.changes()
-        .id(r.getChangeId())
-        .check(new FixInput());
-    assertThat(info.problems).hasSize(1);
-    assertThat(info.problems.get(0).status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
-  }
-
-  private Change getChange(PushOneCommit.Result r) throws Exception {
-    return db.changes().get(new Change.Id(
-        gApi.changes().id(r.getChangeId()).get()._number));
-  }
-}
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/change/SubmitTypeRuleIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
new file mode 100644
index 0000000..1033164
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
@@ -0,0 +1,261 @@
+// 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.acceptance.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.extensions.client.SubmitType.CHERRY_PICK;
+import static com.google.gerrit.extensions.client.SubmitType.FAST_FORWARD_ONLY;
+import static com.google.gerrit.extensions.client.SubmitType.MERGE_ALWAYS;
+import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY;
+import static com.google.gerrit.extensions.client.SubmitType.REBASE_IF_NECESSARY;
+import static org.junit.Assert.fail;
+
+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.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.gerrit.testutil.ConfigSuite;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@NoHttpd
+public class SubmitTypeRuleIT extends AbstractDaemonTest {
+  @ConfigSuite.Default
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
+
+  private class RulesPl extends VersionedMetaData {
+    private static final String FILENAME = "rules.pl";
+
+    private String rule;
+
+    @Override
+    protected String getRefName() {
+      return RefNames.REFS_CONFIG;
+    }
+
+    @Override
+    protected void onLoad() throws IOException, ConfigInvalidException {
+      rule = readUTF8(FILENAME);
+    }
+
+    @Override
+    protected boolean onSave(CommitBuilder commit)
+        throws IOException, ConfigInvalidException {
+      TestSubmitRuleInput in = new TestSubmitRuleInput();
+      in.rule = rule;
+      try {
+        gApi.changes().id(testChangeId.get()).current().testSubmitType(in);
+      } catch (RestApiException e) {
+        throw new ConfigInvalidException("Invalid submit type rule", e);
+      }
+
+      saveUTF8(FILENAME, rule);
+      return true;
+    }
+  }
+
+  private AtomicInteger fileCounter;
+  private Change.Id testChangeId;
+
+  @Before
+  public void setUp() throws Exception {
+    fileCounter = new AtomicInteger();
+    gApi.projects().name(project.get()).branch("test")
+        .create(new BranchInput());
+    testChangeId = createChange("test", "test change").getChange().getId();
+  }
+
+  private void setRulesPl(String rule) throws Exception {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      RulesPl r = new RulesPl();
+      r.load(md);
+      r.rule = rule;
+      r.commit(md);
+    }
+  }
+
+  private static final String SUBMIT_TYPE_FROM_SUBJECT =
+      "submit_type(fast_forward_only) :-"
+      + "gerrit:commit_message(M),"
+      + "regex_matches('.*FAST_FORWARD_ONLY.*', M),"
+      + "!.\n"
+      + "submit_type(merge_if_necessary) :-"
+      + "gerrit:commit_message(M),"
+      + "regex_matches('.*MERGE_IF_NECESSARY.*', M),"
+      + "!.\n"
+      + "submit_type(rebase_if_necessary) :-"
+      + "gerrit:commit_message(M),"
+      + "regex_matches('.*REBASE_IF_NECESSARY.*', M),"
+      + "!.\n"
+      + "submit_type(merge_always) :-"
+      + "gerrit:commit_message(M),"
+      + "regex_matches('.*MERGE_ALWAYS.*', M),"
+      + "!.\n"
+      + "submit_type(cherry_pick) :-"
+      + "gerrit:commit_message(M),"
+      + "regex_matches('.*CHERRY_PICK.*', M),"
+      + "!.\n"
+      + "submit_type(T) :- gerrit:project_default_submit_type(T).";
+
+  private PushOneCommit.Result createChange(String dest, String subject)
+      throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+        subject, "file" + fileCounter.incrementAndGet(),
+        PushOneCommit.FILE_CONTENT);
+    PushOneCommit.Result r = push.to("refs/for/" + dest);
+    r.assertOkStatus();
+    return r;
+  }
+
+  @Test
+  public void unconditionalCherryPick() throws Exception {
+    PushOneCommit.Result r = createChange();
+    assertSubmitType(MERGE_IF_NECESSARY, r.getChangeId());
+    setRulesPl("submit_type(cherry_pick).");
+    assertSubmitType(CHERRY_PICK, r.getChangeId());
+  }
+
+  @Test
+  public void submitTypeFromSubject() throws Exception {
+    PushOneCommit.Result r1 = createChange("master", "Default 1");
+    PushOneCommit.Result r2 = createChange("master", "FAST_FORWARD_ONLY 2");
+    PushOneCommit.Result r3 = createChange("master", "MERGE_IF_NECESSARY 3");
+    PushOneCommit.Result r4 = createChange("master", "REBASE_IF_NECESSARY 4");
+    PushOneCommit.Result r5 = createChange("master", "MERGE_ALWAYS 5");
+    PushOneCommit.Result r6 = createChange("master", "CHERRY_PICK 6");
+
+    assertSubmitType(MERGE_IF_NECESSARY, r1.getChangeId());
+    assertSubmitType(MERGE_IF_NECESSARY, r2.getChangeId());
+    assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId());
+    assertSubmitType(MERGE_IF_NECESSARY, r4.getChangeId());
+    assertSubmitType(MERGE_IF_NECESSARY, r5.getChangeId());
+    assertSubmitType(MERGE_IF_NECESSARY, r6.getChangeId());
+
+    setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
+
+    assertSubmitType(MERGE_IF_NECESSARY, r1.getChangeId());
+    assertSubmitType(FAST_FORWARD_ONLY, r2.getChangeId());
+    assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId());
+    assertSubmitType(REBASE_IF_NECESSARY, r4.getChangeId());
+    assertSubmitType(MERGE_ALWAYS, r5.getChangeId());
+    assertSubmitType(CHERRY_PICK, r6.getChangeId());
+  }
+
+  @Test
+  public void submitTypeIsUsedForSubmit() throws Exception {
+    setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
+
+    PushOneCommit.Result r = createChange("master", "CHERRY_PICK 1");
+
+    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).current().submit();
+
+    List<RevCommit> log = log("master", 1);
+    assertThat(log.get(0).getShortMessage()).isEqualTo("CHERRY_PICK 1");
+    assertThat(log.get(0).name()).isNotEqualTo(r.getCommit().name());
+    assertThat(log.get(0).getFullMessage())
+        .contains("Change-Id: " + r.getChangeId());
+    assertThat(log.get(0).getFullMessage()).contains("Reviewed-on: ");
+  }
+
+  @Test
+  public void mixingSubmitTypesAcrossBranchesSucceeds() throws Exception {
+    setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
+
+    PushOneCommit.Result r1 = createChange("master", "MERGE_IF_NECESSARY 1");
+
+    RevCommit initialCommit = r1.getCommit().getParent(0);
+    BranchInput bin = new BranchInput();
+    bin.revision = initialCommit.name();
+    gApi.projects().name(project.get()).branch("branch").create(bin);
+
+    testRepo.reset(initialCommit);
+    PushOneCommit.Result r2 = createChange("branch", "MERGE_ALWAYS 1");
+
+    gApi.changes().id(r1.getChangeId()).topic(name("topic"));
+    gApi.changes().id(r1.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(r2.getChangeId()).topic(name("topic"));
+    gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(r2.getChangeId()).current().submit();
+
+    assertThat(log("master", 1).get(0).name()).isEqualTo(r1.getCommit().name());
+
+    List<RevCommit> branchLog = log("branch", 1);
+    assertThat(branchLog.get(0).getParents()).hasLength(2);
+    assertThat(branchLog.get(0).getParent(1).name())
+        .isEqualTo(r2.getCommit().name());
+  }
+
+  @Test
+  public void mixingSubmitTypesOnOneBranchFails() throws Exception {
+    setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
+
+    PushOneCommit.Result r1 = createChange("master", "CHERRY_PICK 1");
+    PushOneCommit.Result r2 = createChange("master", "MERGE_IF_NECESSARY 2");
+
+    gApi.changes().id(r1.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.approve());
+
+    try {
+      gApi.changes().id(r2.getChangeId()).current().submit();
+      fail("Expected ResourceConflictException");
+    } catch (ResourceConflictException e) {
+      assertThat(e).hasMessage(
+          "Failed to submit 2 changes due to the following problems:\n"
+          + "Change " + r1.getChange().getId() + ": Change has submit type "
+          + "CHERRY_PICK, but previously chose submit type MERGE_IF_NECESSARY "
+          + "from change " + r2.getChange().getId() + " in the same batch");
+    }
+  }
+
+  private List<RevCommit> log(String commitish, int n) throws Exception {
+    try (Repository repo = repoManager.openRepository(project);
+        Git git = new Git(repo)) {
+      ObjectId id = repo.resolve(commitish);
+      assertThat(id).isNotNull();
+      return ImmutableList.copyOf(git.log().add(id).setMaxCount(n).call());
+    }
+  }
+
+  private void assertSubmitType(SubmitType expected, String id)
+      throws Exception {
+    assertThat(gApi.changes().id(id).current().submitType())
+        .isEqualTo(expected);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK
index 4918a95..3b3d362 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK
@@ -1,7 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  group = 'api-config',
+  group = 'api_config',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUILD
new file mode 100644
index 0000000..da8274d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUILD
@@ -0,0 +1,7 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'api_config',
+  srcs = glob(['*IT.java']),
+  labels = ['api'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/DiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/DiffPreferencesIT.java
new file mode 100644
index 0000000..047305c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/DiffPreferencesIT.java
@@ -0,0 +1,50 @@
+// 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.DiffPreferencesInfo;
+
+import org.junit.Test;
+
+@NoHttpd
+public class DiffPreferencesIT extends AbstractDaemonTest {
+
+  @Test
+  public void getDiffPreferences() throws Exception {
+    DiffPreferencesInfo result =
+        gApi.config().server().getDefaultDiffPreferences();
+    assertPrefs(result, DiffPreferencesInfo.defaults());
+  }
+
+  @Test
+  public void setDiffPreferences() throws Exception {
+    int newLineLength = DiffPreferencesInfo.defaults().lineLength + 10;
+    DiffPreferencesInfo update = new DiffPreferencesInfo();
+    update.lineLength = newLineLength;
+    DiffPreferencesInfo result =
+        gApi.config().server().setDefaultDiffPreferences(update);
+    assertThat(result.lineLength).named("lineLength").isEqualTo(newLineLength);
+
+    result = gApi.config().server().getDefaultDiffPreferences();
+    DiffPreferencesInfo expected = DiffPreferencesInfo.defaults();
+    expected.lineLength = newLineLength;
+    assertPrefs(result, expected);
+  }
+}
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/group/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK
index 06be8ee..cea23dd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK
@@ -1,13 +1,13 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  group = 'api-group',
+  group = 'api_group',
   srcs = glob(['*IT.java']),
   deps = [
     ':util',
     '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:util',
   ],
-  labels = ['api']
+  labels = ['api'],
 )
 
 java_library(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUILD
new file mode 100644
index 0000000..1a374f0
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUILD
@@ -0,0 +1,23 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'api_group',
+  srcs = glob(['*IT.java']),
+  deps = [
+    ':util',
+    '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:util',
+  ],
+  labels = ['api'],
+)
+
+java_library(
+  name = 'util',
+  srcs = ['GroupAssert.java'],
+  deps = [
+    '//gerrit-extension-api:api',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//lib:gwtorm',
+    '//lib:truth',
+  ],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 8d1d89f..f699f61 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -21,7 +21,6 @@
 import com.google.common.base.Function;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -49,6 +48,7 @@
 
 import java.sql.Timestamp;
 import java.util.Arrays;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
@@ -100,6 +100,18 @@
   }
 
   @Test
+  public void addMembersWithAtSign() throws Exception {
+    String g = createGroup("users");
+    TestAccount u10 = accounts.create("u10", "u10@example.com", "Full Name 10");
+    TestAccount u11_at = accounts.create("u11@something", "u11@example.com",
+                                         "Full Name 11 With At");
+    accounts.create("u11", "u11.another@example.com",
+                    "Full Name 11 Without At");
+    gApi.groups().id(g).addMembers(u10.username, u11_at.username);
+    assertMembers(g, u10, u11_at);
+  }
+
+  @Test
   public void includeRemoveGroup() throws Exception {
     String p = createGroup("parent");
     String g = createGroup("newGroup");
@@ -125,7 +137,7 @@
     String p = createGroup("parent");
     String g1 = createGroup("newGroup1");
     String g2 = createGroup("newGroup2");
-    List<String> groups = Lists.newLinkedList();
+    List<String> groups = new LinkedList<>();
     groups.add(g1);
     groups.add(g2);
     gApi.groups().id(p).addGroups(g1, g2);
@@ -140,6 +152,45 @@
   }
 
   @Test
+  public void testCreateDuplicateInternalGroupCaseSensitiveName_Conflict()
+      throws Exception {
+    String dupGroupName = name("dupGroup");
+    gApi.groups().create(dupGroupName);
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("group '" + dupGroupName + "' already exists");
+    gApi.groups().create(dupGroupName);
+  }
+
+  @Test
+  public void testCreateDuplicateInternalGroupCaseInsensitiveName()
+      throws Exception {
+    String dupGroupName = name("dupGroupA");
+    String dupGroupNameLowerCase = name("dupGroupA").toLowerCase();
+    gApi.groups().create(dupGroupName);
+    gApi.groups().create(dupGroupNameLowerCase);
+    assertThat(gApi.groups().list().getAsMap().keySet()).contains(dupGroupName);
+    assertThat(gApi.groups().list().getAsMap().keySet()).contains(dupGroupNameLowerCase);
+  }
+
+  @Test
+  public void testCreateDuplicateSystemGroupCaseSensitiveName_Conflict()
+      throws Exception {
+    String newGroupName = "Registered Users";
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("group 'Registered Users' already exists");
+    gApi.groups().create(newGroupName);
+  }
+
+  @Test
+  public void testCreateDuplicateSystemGroupCaseInsensitiveName_Conflict()
+      throws Exception {
+    String newGroupName = "registered users";
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("group 'Registered Users' already exists");
+    gApi.groups().create(newGroupName);
+  }
+
+  @Test
   public void testCreateGroupWithProperties() throws Exception {
     GroupInput in = new GroupInput();
     in.name = name("newGroup");
@@ -160,13 +211,6 @@
   }
 
   @Test
-  public void testCreateGroupWhenGroupAlreadyExists_Conflict()
-      throws Exception {
-    exception.expect(ResourceConflictException.class);
-    gApi.groups().create("Administrators");
-  }
-
-  @Test
   public void testGetGroup() throws Exception {
     AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
     testGetGroup(adminGroup.getGroupUUID().get(), adminGroup);
@@ -528,19 +572,6 @@
     return groupCache.get(new AccountGroup.NameKey(name));
   }
 
-  private String createGroup(String name) throws Exception {
-    return createGroup(name, "Administrators");
-  }
-
-  private String createGroup(String name, String owner) throws Exception {
-    name = name(name);
-    GroupInput in = new GroupInput();
-    in.name = name;
-    in.ownerId = owner;
-    gApi.groups().create(in);
-    return name;
-  }
-
   private String createAccount(String name, String group) throws Exception {
     name = name(name);
     accounts.create(name, group);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK
index 9dab9f8..0b293f3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK
@@ -1,7 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  group = 'api-project',
+  group = 'api_project',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUILD
new file mode 100644
index 0000000..4fb65ff
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUILD
@@ -0,0 +1,7 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'api_project',
+  srcs = glob(['*IT.java']),
+  labels = ['api'],
+)
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 d837565..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
@@ -21,34 +21,74 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.ConfigInfo;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.api.projects.DescriptionInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
-import com.google.gerrit.extensions.api.projects.PutDescriptionInput;
+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;
 
 @NoHttpd
 public class ProjectIT extends AbstractDaemonTest  {
 
   @Test
-  public void createProjectFoo() throws Exception {
+  public void createProject() throws Exception {
     String name = name("foo");
     assertThat(name).isEqualTo(
         gApi.projects()
             .create(name)
             .get()
             .name);
+
+    RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
+    eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG,
+        null, head);
+
+    eventRecorder.assertRefUpdatedEvents(name, "refs/heads/master",
+        new String[]{});
   }
 
   @Test
-  public void createProjectFooWithGitSuffix() throws Exception {
+  public void createProjectWithGitSuffix() throws Exception {
     String name = name("foo");
     assertThat(name).isEqualTo(
         gApi.projects()
             .create(name + ".git")
             .get()
             .name);
+
+    RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
+    eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG,
+        null, head);
+
+    eventRecorder.assertRefUpdatedEvents(name, "refs/heads/master",
+        new String[]{});
+  }
+
+  @Test
+  public void createProjectWithInitialCommit() throws Exception {
+    String name = name("foo");
+    ProjectInput input = new ProjectInput();
+    input.name = name;
+    input.createEmptyCommit = true;
+    assertThat(name).isEqualTo(
+        gApi.projects()
+            .create(input)
+            .get()
+            .name);
+
+    RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
+    eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG,
+        null, head);
+
+    head = getRemoteHead(name, "refs/heads/master");
+    eventRecorder.assertRefUpdatedEvents(name, "refs/heads/master",
+        null, head);
   }
 
   @Test
@@ -63,6 +103,15 @@
   }
 
   @Test
+  public void createProjectNoNameInInput() throws Exception {
+    ProjectInput in = new ProjectInput();
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("input.name is required");
+    gApi.projects()
+        .create(in);
+  }
+
+  @Test
   public void createProjectDuplicate() throws Exception {
     ProjectInput in = new ProjectInput();
     in.name = name("baz");
@@ -85,11 +134,12 @@
 
   @Test
   public void description() throws Exception {
+    RevCommit initialHead = getRemoteHead(project, RefNames.REFS_CONFIG);
     assertThat(gApi.projects()
             .name(project.get())
             .description())
         .isEmpty();
-    PutDescriptionInput in = new PutDescriptionInput();
+    DescriptionInput in = new DescriptionInput();
     in.description = "new project description";
     gApi.projects()
         .name(project.get())
@@ -98,5 +148,27 @@
             .name(project.get())
             .description())
         .isEqualTo(in.description);
+
+    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, RefNames.REFS_CONFIG);
+
+    ConfigInfo info = gApi.projects().name(project.get()).config();
+    assertThat(info.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+    ConfigInput input = new ConfigInput();
+    input.submitType = SubmitType.CHERRY_PICK;
+    info = gApi.projects().name(project.get()).config(input);
+    assertThat(info.submitType).isEqualTo(SubmitType.CHERRY_PICK);
+    info = gApi.projects().name(project.get()).config();
+    assertThat(info.submitType).isEqualTo(SubmitType.CHERRY_PICK);
+
+    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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK
index c916755..76ae637 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK
@@ -1,7 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  group = 'api-revision',
+  group = 'api_revision',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUILD
new file mode 100644
index 0000000..e527b9d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUILD
@@ -0,0 +1,7 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'api_revision',
+  srcs = glob(['*IT.java']),
+  labels = ['api'],
+)
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 74bfa4a..ee2dbfe 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
@@ -18,9 +18,11 @@
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
+import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.http.HttpStatus.SC_OK;
 import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.junit.Assert.fail;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
@@ -28,6 +30,7 @@
 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;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.DraftApi;
@@ -37,6 +40,7 @@
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -46,8 +50,16 @@
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 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.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.server.change.GetRevisionActions;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.Util;
+import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -57,6 +69,7 @@
 import java.io.ByteArrayOutputStream;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -66,6 +79,9 @@
 
 public class RevisionIT extends AbstractDaemonTest {
 
+  @Inject
+  private GetRevisionActions getRevisionActions;
+
   private TestAccount admin2;
 
   @Before
@@ -109,18 +125,68 @@
   @Test
   public void submit() throws Exception {
     PushOneCommit.Result r = createChange();
+    String changeId = project.get() + "~master~" + r.getChangeId();
     gApi.changes()
-        .id(project.get() + "~master~" + r.getChangeId())
+        .id(changeId)
         .current()
         .review(ReviewInput.approve());
     gApi.changes()
-        .id(project.get() + "~master~" + r.getChangeId())
+        .id(changeId)
         .current()
         .submit();
+    assertThat(gApi.changes().id(changeId).get().status)
+        .isEqualTo(ChangeStatus.MERGED);
   }
 
-  @Test(expected = AuthException.class)
+  private void allowSubmitOnBehalfOf() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    Util.allow(cfg,
+        Permission.SUBMIT_AS,
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(),
+        "refs/heads/*");
+    saveProjectConfig(project, cfg);
+  }
+
+  @Test
   public void submitOnBehalfOf() throws Exception {
+    allowSubmitOnBehalfOf();
+    PushOneCommit.Result r = createChange();
+    String changeId = project.get() + "~master~" + r.getChangeId();
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .review(ReviewInput.approve());
+    SubmitInput in = new SubmitInput();
+    in.onBehalfOf = admin2.email;
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .submit(in);
+    assertThat(gApi.changes().id(changeId).get().status)
+        .isEqualTo(ChangeStatus.MERGED);
+  }
+
+  @Test
+  public void submitOnBehalfOfInvalidUser() throws Exception {
+    allowSubmitOnBehalfOf();
+    PushOneCommit.Result r = createChange();
+    String changeId = project.get() + "~master~" + r.getChangeId();
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .review(ReviewInput.approve());
+    SubmitInput in = new SubmitInput();
+    in.onBehalfOf = "doesnotexist";
+    exception.expect(UnprocessableEntityException.class);
+    exception.expectMessage("Account Not Found: doesnotexist");
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .submit(in);
+  }
+
+  @Test
+  public void submitOnBehalfOfNotPermitted() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
         .id(project.get() + "~master~" + r.getChangeId())
@@ -128,6 +194,8 @@
         .review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
     in.onBehalfOf = admin2.email;
+    exception.expect(AuthException.class);
+    exception.expectMessage("submit on behalf of not permitted");
     gApi.changes()
         .id(project.get() + "~master~" + r.getChangeId())
         .current()
@@ -159,7 +227,11 @@
     assertThat(orig.get().messages).hasSize(1);
     ChangeApi cherry = orig.revision(r.getCommit().name())
         .cherryPick(in);
-    assertThat(orig.get().messages).hasSize(2);
+
+    Collection<ChangeMessageInfo> messages = gApi.changes()
+        .id(project.get() + "~master~" + r.getChangeId())
+        .get().messages;
+    assertThat(messages).hasSize(2);
 
     String cherryPickedRevision = cherry.get().currentRevision;
     String expectedMessage = String.format(
@@ -167,7 +239,7 @@
         "This patchset was cherry picked to branch %s as commit %s",
         in.destination, cherryPickedRevision);
 
-    Iterator<ChangeMessageInfo> origIt = orig.get().messages.iterator();
+    Iterator<ChangeMessageInfo> origIt = messages.iterator();
     origIt.next();
     assertThat(origIt.next().message).isEqualTo(expectedMessage);
 
@@ -279,7 +351,11 @@
     assertThat(orig.get().messages).hasSize(1);
     ChangeApi cherry = orig.revision(r.getCommit().name())
         .cherryPick(in);
-    assertThat(orig.get().messages).hasSize(2);
+
+    Collection<ChangeMessageInfo> messages = gApi.changes()
+        .id(project.get() + "~master~" + r.getChangeId())
+        .get().messages;
+    assertThat(messages).hasSize(2);
 
     assertThat(cherry.get().subject).contains(in.message);
     cherry.current().review(ReviewInput.approve());
@@ -316,6 +392,47 @@
   }
 
   @Test
+  public void cherryPickToExistingChange() throws Exception {
+    PushOneCommit.Result r1 = pushFactory.create(
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "a")
+        .to("refs/for/master");
+    String t1 = project.get() + "~master~" + r1.getChangeId();
+
+    BranchInput bin = new BranchInput();
+    bin.revision = r1.getCommit().getParent(0).name();
+    gApi.projects()
+        .name(project.get())
+        .branch("foo")
+        .create(bin);
+
+    PushOneCommit.Result r2 = pushFactory.create(
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "b",
+          r1.getChangeId())
+        .to("refs/for/foo");
+    String t2 = project.get() + "~foo~" + r2.getChangeId();
+    gApi.changes().id(t2).abandon();
+
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "foo";
+    in.message = r1.getCommit().getFullMessage();
+    try {
+      gApi.changes().id(t1).current().cherryPick(in);
+      fail();
+    } catch (ResourceConflictException e) {
+      assertThat(e.getMessage()).isEqualTo(
+          "Cannot create new patch set of change " + info(t2)._number
+          + " because it is abandoned");
+    }
+
+    gApi.changes().id(t2).restore();
+    gApi.changes().id(t1).current().cherryPick(in);
+    assertThat(get(t2).revisions).hasSize(2);
+    assertThat(
+          gApi.changes().id(t2).current().file(FILE_NAME).content().asString())
+        .isEqualTo("a");
+  }
+
+  @Test
   public void canRebase() throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
@@ -408,6 +525,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()
@@ -420,6 +566,48 @@
   }
 
   @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()
@@ -441,8 +629,8 @@
       + "/revisions/" + r.getCommit().name()
       + "/files/" + FILE_NAME
       + "/content";
-    RestResponse response = adminSession.head(endPoint);
-    assertThat(response.getStatusCode()).isEqualTo(SC_OK);
+    RestResponse response = adminRestSession.head(endPoint);
+    response.assertOK();
     assertThat(response.getContentType()).startsWith("text/plain");
     assertThat(response.hasContent()).isFalse();
   }
@@ -577,12 +765,39 @@
         Locale.US);
     String date = df.format(rev.commit.author.date);
     assertThat(res).isEqualTo(
-        String.format(PATCH, r.getCommitId().name(), date, r.getChangeId()));
+        String.format(PATCH, r.getCommit().name(), date, r.getChangeId()));
   }
 
-  private void merge(PushOneCommit.Result r) throws Exception {
-    revision(r).review(ReviewInput.approve());
-    revision(r).submit();
+  @Test
+  public void actions() throws Exception {
+    PushOneCommit.Result r = createChange();
+    assertThat(current(r).actions().keySet())
+        .containsExactly("cherrypick", "rebase");
+
+    current(r).review(ReviewInput.approve());
+    assertThat(current(r).actions().keySet())
+        .containsExactly("submit", "cherrypick", "rebase");
+
+    current(r).submit();
+    assertThat(current(r).actions().keySet())
+        .containsExactly("cherrypick");
+  }
+
+  @Test
+  public void actionsETag() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = createChange();
+
+    String oldETag = checkETag(getRevisionActions, r2, null);
+    current(r2).review(ReviewInput.approve());
+    oldETag = checkETag(getRevisionActions, r2, oldETag);
+
+    // Dependent change is included in ETag.
+    current(r1).review(ReviewInput.approve());
+    oldETag = checkETag(getRevisionActions, r2, oldETag);
+
+    current(r2).submit();
+    oldETag = checkETag(getRevisionActions, r2, oldETag);
   }
 
   private PushOneCommit.Result updateChange(PushOneCommit.Result r,
@@ -596,4 +811,15 @@
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
     return push.to("refs/drafts/master");
   }
+
+  private RevisionApi current(PushOneCommit.Result r) throws Exception {
+    return gApi.changes().id(r.getChangeId()).current();
+  }
+
+  private String checkETag(ETagView<RevisionResource> view,
+      PushOneCommit.Result r, String oldETag) throws Exception {
+    String eTag = view.getETag(parseRevisionResource(r));
+    assertThat(eTag).isNotEqualTo(oldETag);
+    return eTag;
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUILD
new file mode 100644
index 0000000..3fcf2d8
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUILD
@@ -0,0 +1,11 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'edit',
+  srcs = ['ChangeEditIT.java'],
+  deps = [
+    '//lib/commons:codec',
+    '//lib/joda:joda-time',
+  ],
+  labels = ['edit'],
+)
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 815ea3f..e47d570 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,13 +16,9 @@
 
 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;
-import static org.apache.http.HttpStatus.SC_CONFLICT;
-import static org.apache.http.HttpStatus.SC_FORBIDDEN;
-import static org.apache.http.HttpStatus.SC_NOT_FOUND;
-import static org.apache.http.HttpStatus.SC_NO_CONTENT;
-import static org.apache.http.HttpStatus.SC_OK;
 
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
@@ -31,20 +27,24 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
 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;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 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;
@@ -58,14 +58,20 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.testutil.TestTimeUtil;
+import com.google.gson.reflect.TypeToken;
 import com.google.gson.stream.JsonReader;
 import com.google.gwtorm.server.SchemaFactory;
 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;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
@@ -73,11 +79,11 @@
 import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 
 public class ChangeEditIT extends AbstractDaemonTest {
 
@@ -93,7 +99,7 @@
   private SchemaFactory<ReviewDb> reviewDbProvider;
 
   @Inject
-  ChangeEditUtil editUtil;
+  private ChangeEditUtil editUtil;
 
   @Inject
   private ChangeEditModifier modifier;
@@ -139,11 +145,22 @@
   }
 
   @Test
+  public void parseEditRevision() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+
+    // check that '0' is parsed as edit revision
+    gApi.changes().id(change.getChangeId()).revision(0).comments();
+
+    // check that 'edit' is parsed as edit revision
+    gApi.changes().id(change.getChangeId()).revision("edit").comments();
+  }
+
+  @Test
   public void deleteEdit() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     assertThat(
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
-            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+            RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
     editUtil.delete(editUtil.byChange(change).get());
     assertThat(editUtil.byChange(change).isPresent()).isFalse();
   }
@@ -154,7 +171,7 @@
         .isEqualTo(RefUpdate.Result.NEW);
     assertThat(
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
-            RestSession.newRawInput(CONTENT_NEW2))).isEqualTo(RefUpdate.Result.FORCED);
+            RawInputUtil.create(CONTENT_NEW2))).isEqualTo(RefUpdate.Result.FORCED);
     editUtil.publish(editUtil.byChange(change).get());
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertThat(edit.isPresent()).isFalse();
@@ -171,10 +188,9 @@
         RefUpdate.Result.NEW);
     assertThat(
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
-            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+            RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    RestResponse r = adminSession.post(urlPublish());
-    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    adminRestSession.post(urlPublish()).assertNoContent();
     edit = editUtil.byChange(change);
     assertThat(edit.isPresent()).isFalse();
     PatchSet newCurrentPatchSet = getCurrentPatchSet(changeId);
@@ -190,10 +206,9 @@
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     assertThat(
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
-            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+            RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    RestResponse r = adminSession.delete(urlEdit());
-    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    adminRestSession.delete(urlEdit()).assertNoContent();
     edit = editUtil.byChange(change);
     assertThat(edit.isPresent()).isFalse();
   }
@@ -206,12 +221,10 @@
         RefUpdate.Result.NEW);
     assertThat(
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
-            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
-    RestResponse r = adminSession.post(urlPublish());
-    assertThat(r.getStatusCode()).isEqualTo(SC_FORBIDDEN);
+            RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+    adminRestSession.post(urlPublish()).assertForbidden();
     setUseContributorAgreements(InheritableBoolean.FALSE);
-    r = adminSession.post(urlPublish());
-    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    adminRestSession.post(urlPublish()).assertNoContent();
   }
 
   @Test
@@ -219,7 +232,7 @@
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     assertThat(
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
-            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+            RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
     ChangeEdit edit = editUtil.byChange(change).get();
     PatchSet current = getCurrentPatchSet(changeId);
     assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
@@ -242,14 +255,13 @@
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     assertThat(
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
-            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+            RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
     ChangeEdit edit = editUtil.byChange(change).get();
     PatchSet current = getCurrentPatchSet(changeId);
     assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
         current.getPatchSetId() - 1);
     Date beforeRebase = edit.getEditCommit().getCommitterIdent().getWhen();
-    RestResponse r = adminSession.post(urlRebase());
-    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    adminRestSession.post(urlRebase()).assertNoContent();
     edit = editUtil.byChange(change).get();
     assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()),
         ObjectId.fromString(edit.getRevision().get()), FILE_NAME), CONTENT_NEW);
@@ -267,7 +279,7 @@
     assertThat(modifier.createEdit(change2, current)).isEqualTo(RefUpdate.Result.NEW);
     assertThat(
         modifier.modifyFile(editUtil.byChange(change2).get(), FILE_NAME,
-            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+            RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
     ChangeEdit edit = editUtil.byChange(change2).get();
     assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
         current.getPatchSetId());
@@ -275,15 +287,14 @@
         pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, FILE_NAME,
             new String(CONTENT_NEW2), changeId2);
     push.to("refs/for/master").assertOkStatus();
-    RestResponse r = adminSession.post(urlRebase());
-    assertThat(r.getStatusCode()).isEqualTo(SC_CONFLICT);
+    adminRestSession.post(urlRebase()).assertConflict();
   }
 
   @Test
   public void updateExistingFile() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW)))
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW)))
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
@@ -329,12 +340,24 @@
   }
 
   @Test
+  public void updateMessageOnlyAddTrailingNewLines() throws Exception {
+    assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId)))
+        .isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+
+    exception.expect(UnchangedCommitMessageException.class);
+    exception.expectMessage(
+        "New commit message cannot be same as existing commit message");
+    modifier.modifyMessage(
+        edit.get(),
+        edit.get().getEditCommit().getFullMessage() + "\n\n");
+  }
+
+  @Test
   public void updateMessage() throws Exception {
     assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId)))
         .isEqualTo(RefUpdate.Result.NEW);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    assertUnchangedMessage(edit, edit.get().getEditCommit().getFullMessage());
-    assertUnchangedMessage(edit, edit.get().getEditCommit().getFullMessage() + "\n\n");
     String msg = String.format("New commit message\n\nChange-Id: %s\n",
         change.getKey());
     assertThat(modifier.modifyMessage(edit.get(), msg)).isEqualTo(
@@ -358,27 +381,33 @@
 
   @Test
   public void updateMessageRest() throws Exception {
-    assertThat(adminSession.get(urlEditMessage()).getStatusCode())
-        .isEqualTo(SC_NOT_FOUND);
+    adminRestSession.get(urlEditMessage(false)).assertNotFound();
     EditMessage.Input in = new EditMessage.Input();
     in.message = String.format("New commit message\n\n" +
         CONTENT_NEW2_STR + "\n\nChange-Id: %s\n",
         change.getKey());
-    assertThat(adminSession.put(urlEditMessage(), in).getStatusCode())
-        .isEqualTo(SC_NO_CONTENT);
-    RestResponse r = adminSession.getJsonAccept(urlEditMessage());
-    assertThat(r.getStatusCode()).isEqualTo(SC_OK);
+    adminRestSession.put(urlEditMessage(false), in).assertNoContent();
+    RestResponse r = adminRestSession.getJsonAccept(urlEditMessage(false));
+    r.assertOK();
     assertThat(readContentFromJson(r)).isEqualTo(in.message);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertThat(edit.get().getEditCommit().getFullMessage())
         .isEqualTo(in.message);
     in.message = String.format("New commit message2\n\nChange-Id: %s\n",
         change.getKey());
-    assertThat(adminSession.put(urlEditMessage(), in).getStatusCode())
-        .isEqualTo(SC_NO_CONTENT);
+    adminRestSession.put(urlEditMessage(false), in).assertNoContent();
     edit = editUtil.byChange(change);
     assertThat(edit.get().getEditCommit().getFullMessage())
         .isEqualTo(in.message);
+
+    r = adminRestSession.getJsonAccept(urlEditMessage(true));
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      RevCommit commit = rw.parseCommit(
+          ObjectId.fromString(ps.getRevision().get()));
+      assertThat(readContentFromJson(r)).isEqualTo(commit.getFullMessage());
+    }
+
     editUtil.publish(edit.get());
     assertChangeMessages(change,
         ImmutableList.of("Uploaded patch set 1.",
@@ -388,11 +417,10 @@
 
   @Test
   public void retrieveEdit() throws Exception {
-    RestResponse r = adminSession.get(urlEdit());
-    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    adminRestSession.get(urlEdit()).assertNoContent();
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW)))
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW)))
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change);
     EditInfo info = toEditInfo(false);
@@ -402,15 +430,14 @@
     edit = editUtil.byChange(change);
     editUtil.delete(edit.get());
 
-    r = adminSession.get(urlEdit());
-    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    adminRestSession.get(urlEdit()).assertNoContent();
   }
 
   @Test
   public void retrieveFilesInEdit() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW)))
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW)))
         .isEqualTo(RefUpdate.Result.FORCED);
 
     EditInfo info = toEditInfo(true);
@@ -448,8 +475,7 @@
 
   @Test
   public void createEditByDeletingExistingFileRest() throws Exception {
-    RestResponse r = adminSession.delete(urlEditFile());
-    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    adminRestSession.delete(urlEditFile()).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     exception.expect(ResourceNotFoundException.class);
     fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
@@ -458,15 +484,13 @@
 
   @Test
   public void deletingNonExistingEditRest() throws Exception {
-    RestResponse r = adminSession.delete(urlEdit());
-    assertThat(r.getStatusCode()).isEqualTo(SC_NOT_FOUND);
+    adminRestSession.delete(urlEdit()).assertNotFound();
   }
 
   @Test
   public void deleteExistingFileRest() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
-    assertThat(adminSession.delete(urlEditFile()).getStatusCode()).isEqualTo(
-        SC_NO_CONTENT);
+    adminRestSession.delete(urlEditFile()).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     exception.expect(ResourceNotFoundException.class);
     fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
@@ -497,7 +521,7 @@
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
     assertThat(
         modifier.modifyFile(editUtil.byChange(change2).get(), FILE_NAME,
-            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+            RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change2);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
@@ -515,8 +539,7 @@
     Post.Input in = new Post.Input();
     in.oldPath = FILE_NAME;
     in.newPath = FILE_NAME3;
-    assertThat(adminSession.post(urlEdit(), in).getStatusCode()).isEqualTo(
-        SC_NO_CONTENT);
+    adminRestSession.post(urlEdit(), in).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME3), CONTENT_OLD);
@@ -529,8 +552,7 @@
   public void restoreDeletedFileInPatchSetRest() throws Exception {
     Post.Input in = new Post.Input();
     in.restorePath = FILE_NAME;
-    assertThat(adminSession.post(urlEdit2(), in).getStatusCode()).isEqualTo(
-        SC_NO_CONTENT);
+    adminRestSession.post(urlEdit2(), in).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change2);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
@@ -540,12 +562,12 @@
   public void amendExistingFile() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW)))
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW)))
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
-    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW2)))
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW2)))
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
@@ -555,15 +577,13 @@
   @Test
   public void createAndChangeEditInOneRequestRest() throws Exception {
     Put.Input in = new Put.Input();
-    in.content = RestSession.newRawInput(CONTENT_NEW);
-    assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
-        .isEqualTo(SC_NO_CONTENT);
+    in.content = RawInputUtil.create(CONTENT_NEW);
+    adminRestSession.putRaw(urlEditFile(), in.content).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
-    in.content = RestSession.newRawInput(CONTENT_NEW2);
-    assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
-        .isEqualTo(SC_NO_CONTENT);
+    in.content = RawInputUtil.create(CONTENT_NEW2);
+    adminRestSession.putRaw(urlEditFile(), in.content).assertNoContent();
     edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW2);
@@ -573,9 +593,8 @@
   public void changeEditRest() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     Put.Input in = new Put.Input();
-    in.content = RestSession.newRawInput(CONTENT_NEW);
-    assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
-        .isEqualTo(SC_NO_CONTENT);
+    in.content = RawInputUtil.create(CONTENT_NEW);
+    adminRestSession.putRaw(urlEditFile(), in.content).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
@@ -584,8 +603,7 @@
   @Test
   public void emptyPutRequest() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
-    assertThat(adminSession.put(urlEditFile()).getStatusCode()).isEqualTo(
-        SC_NO_CONTENT);
+    adminRestSession.put(urlEditFile()).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), "".getBytes());
@@ -593,8 +611,7 @@
 
   @Test
   public void createEmptyEditRest() throws Exception {
-    assertThat(adminSession.post(urlEdit()).getStatusCode()).isEqualTo(
-        SC_NO_CONTENT);
+    adminRestSession.post(urlEdit()).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
@@ -603,27 +620,29 @@
   @Test
   public void getFileContentRest() throws Exception {
     Put.Input in = new Put.Input();
-    in.content = RestSession.newRawInput(CONTENT_NEW);
-    assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
-        .isEqualTo(SC_NO_CONTENT);
+    in.content = RawInputUtil.create(CONTENT_NEW);
+    adminRestSession.putRaw(urlEditFile(), in.content).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW2)))
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW2)))
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change);
-    RestResponse r = adminSession.getJsonAccept(urlEditFile());
-    assertThat(r.getStatusCode()).isEqualTo(SC_OK);
+    RestResponse r = adminRestSession.getJsonAccept(urlEditFile());
+    r.assertOK();
     assertThat(readContentFromJson(r)).isEqualTo(
         StringUtils.newStringUtf8(CONTENT_NEW2));
+
+    r = adminRestSession.getJsonAccept(urlEditFile(true));
+    r.assertOK();
+    assertThat(readContentFromJson(r)).isEqualTo(
+        StringUtils.newStringUtf8(CONTENT_OLD));
   }
 
   @Test
   public void getFileNotFoundRest() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
-    assertThat(adminSession.delete(urlEditFile()).getStatusCode()).isEqualTo(
-        SC_NO_CONTENT);
+    adminRestSession.delete(urlEditFile()).assertNoContent();
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    RestResponse r = adminSession.get(urlEditFile());
-    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    adminRestSession.get(urlEditFile()).assertNoContent();
     exception.expect(ResourceNotFoundException.class);
     fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
@@ -633,7 +652,7 @@
   public void addNewFile() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RestSession.newRawInput(CONTENT_NEW)))
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RawInputUtil.create(CONTENT_NEW)))
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
@@ -644,12 +663,12 @@
   public void addNewFileAndAmend() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RestSession.newRawInput(CONTENT_NEW)))
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RawInputUtil.create(CONTENT_NEW)))
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME2), CONTENT_NEW);
-    assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RestSession.newRawInput(CONTENT_NEW2)))
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RawInputUtil.create(CONTENT_NEW2)))
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
@@ -664,7 +683,7 @@
     modifier.modifyFile(
         editUtil.byChange(change).get(),
         FILE_NAME,
-        RestSession.newRawInput(CONTENT_OLD));
+        RawInputUtil.create(CONTENT_OLD));
   }
 
   @Test
@@ -701,14 +720,6 @@
     assertThat(approvals.get(0).value).isEqualTo(1);
   }
 
-  private void assertUnchangedMessage(Optional<ChangeEdit> edit, String message)
-      throws Exception {
-    exception.expect(UnchangedCommitMessageException.class);
-    exception.expectMessage(
-        "New commit message cannot be same as existing commit message");
-    modifier.modifyMessage(edit.get(), message);
-  }
-
   @Test
   public void testHasEditPredicate() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
@@ -718,12 +729,12 @@
     assertThat(modifier.createEdit(change2, current)).isEqualTo(RefUpdate.Result.NEW);
     assertThat(
         modifier.modifyFile(editUtil.byChange(change2).get(), FILE_NAME,
-            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+            RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
     assertThat(queryEdits()).hasSize(2);
 
     assertThat(
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
-            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+            RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
     editUtil.delete(editUtil.byChange(change).get());
     assertThat(queryEdits()).hasSize(1);
 
@@ -738,6 +749,63 @@
     assertThat(queryEdits()).hasSize(0);
   }
 
+  @Test
+  public void files() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    ChangeEdit edit = editUtil.byChange(change).get();
+    assertThat(modifier.modifyFile(edit, FILE_NAME, RawInputUtil.create(CONTENT_NEW)))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change).get();
+
+    RestResponse r = adminRestSession.getJsonAccept(urlRevisionFiles(edit));
+    Map<String, FileInfo> files = readContentFromJson(
+        r, new TypeToken<Map<String, FileInfo>>() {});
+    assertThat(files).containsKey(FILE_NAME);
+
+    r = adminRestSession.getJsonAccept(urlRevisionFiles());
+    files = readContentFromJson(r, new TypeToken<Map<String, FileInfo>>() {});
+    assertThat(files).containsKey(FILE_NAME);
+  }
+
+  @Test
+  public void diff() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    ChangeEdit edit = editUtil.byChange(change).get();
+    assertThat(modifier.modifyFile(edit, FILE_NAME, RawInputUtil.create(CONTENT_NEW)))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change).get();
+
+    RestResponse r = adminRestSession.getJsonAccept(urlDiff(edit));
+    DiffInfo diff = readContentFromJson(r, DiffInfo.class);
+    assertThat(diff.diffHeader.get(0)).contains(FILE_NAME);
+
+    r = adminRestSession.getJsonAccept(urlDiff());
+    diff = readContentFromJson(r, DiffInfo.class);
+    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");
   }
@@ -768,8 +836,8 @@
   }
 
   private PatchSet getCurrentPatchSet(String changeId) throws Exception {
-    return db.patchSets()
-        .get(getChange(changeId).currentPatchSetId());
+    return getOnlyElement(queryProvider.get().byKeyPrefix(changeId))
+        .currentPatchSet();
   }
 
   private static void assertByteArray(BinaryResult result, byte[] expected)
@@ -791,16 +859,22 @@
         + "/edit/";
   }
 
-  private String urlEditMessage() {
+  private String urlEditMessage(boolean base) {
     return "/changes/"
         + change.getChangeId()
-        + "/edit:message";
+        + "/edit:message"
+        + (base ? "?base" : "");
   }
 
   private String urlEditFile() {
+    return urlEditFile(false);
+  }
+
+  private String urlEditFile(boolean base) {
     return urlEdit()
         + "/"
-        + FILE_NAME;
+        + FILE_NAME
+        + (base ? "?base" : "");
   }
 
   private String urlGetFiles() {
@@ -808,6 +882,20 @@
         + "?list";
   }
 
+  private String urlRevisionFiles(ChangeEdit edit) {
+    return "/changes/"
+      + change.getChangeId()
+      + "/revisions/"
+      + edit.getRevision().get()
+      + "/files";
+  }
+
+  private String urlRevisionFiles() {
+    return "/changes/"
+      + change.getChangeId()
+      + "/revisions/0/files";
+  }
+
   private String urlPublish() {
     return "/changes/"
         + change.getChangeId()
@@ -820,16 +908,47 @@
         + "/edit:rebase";
   }
 
-  private EditInfo toEditInfo(boolean files) throws IOException {
-    RestResponse r = adminSession.get(files ? urlGetFiles() : urlEdit());
-    assertThat(r.getStatusCode()).isEqualTo(SC_OK);
-    return newGson().fromJson(r.getReader(), EditInfo.class);
+  private String urlDiff() {
+    return "/changes/"
+        + change.getChangeId()
+        + "/revisions/0/files/"
+        + FILE_NAME
+        + "/diff?context=ALL&intraline";
   }
 
-  private String readContentFromJson(RestResponse r) throws IOException {
+  private String urlDiff(ChangeEdit edit) {
+    return "/changes/"
+        + change.getChangeId()
+        + "/revisions/"
+        + edit.getRevision().get()
+        + "/files/"
+        + FILE_NAME
+        + "/diff?context=ALL&intraline";
+  }
+
+  private EditInfo toEditInfo(boolean files) throws Exception {
+    RestResponse r = adminRestSession.get(files ? urlGetFiles() : urlEdit());
+    return readContentFromJson(r, EditInfo.class);
+  }
+
+  private <T> T readContentFromJson(RestResponse r, Class<T> clazz)
+      throws Exception {
+    r.assertOK();
     JsonReader jsonReader = new JsonReader(r.getReader());
     jsonReader.setLenient(true);
-    return newGson().fromJson(jsonReader, String.class);
+    return newGson().fromJson(jsonReader, clazz);
+  }
+
+  private <T> T readContentFromJson(RestResponse r, TypeToken<T> typeToken)
+      throws Exception {
+    r.assertOK();
+    JsonReader jsonReader = new JsonReader(r.getReader());
+    jsonReader.setLenient(true);
+    return newGson().fromJson(jsonReader, typeToken.getType());
+  }
+
+  private String readContentFromJson(RestResponse r) throws Exception {
+    return readContentFromJson(r, String.class);
   }
 
   private void assertChangeMessages(Change c, List<String> expectedMessages)
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 87e4656..5cdd2f4 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
@@ -16,34 +16,63 @@
 
 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.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.projects.BranchInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.testutil.TestTimeUtil;
+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;
+import com.google.gerrit.testutil.TestTimeUtil;
 
+import org.eclipse.jgit.api.errors.GitAPIException;
+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.transport.PushResult;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
@@ -53,6 +82,7 @@
   }
 
   private String sshUrl;
+  private LabelType patchSetLock;
 
   @BeforeClass
   public static void setTimeForTesting() {
@@ -66,7 +96,16 @@
 
   @Before
   public void setUp() throws Exception {
-    sshUrl = sshSession.getUrl();
+    sshUrl = adminSshSession.getUrl();
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    patchSetLock = Util.patchSetLock();
+    cfg.getLabelSections().put(patchSetLock.getName(), patchSetLock);
+    AccountGroup.UUID anonymousUsers =
+        SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
+    Util.allow(cfg, Permission.forLabel(patchSetLock.getName()), 0, 1, anonymousUsers,
+        "refs/heads/*");
+    saveProjectConfig(cfg);
+    grant(Permission.LABEL + "Patch-Set-Lock", project, "refs/heads/*");
   }
 
   protected void selectProtocol(Protocol p) throws Exception {
@@ -85,14 +124,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");
@@ -125,7 +164,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);
@@ -139,7 +178,41 @@
   }
 
   @Test
-  public void testPushForMasterWithCc() throws Exception {
+  public void pushForMasterWithNotify() throws Exception {
+    TestAccount user2 = accounts.user2();
+    String pushSpec = "refs/for/master"
+        + "%reviewer=" + user.email
+        + ",cc=" + user2.email;
+
+    sender.clear();
+    PushOneCommit.Result r =
+        pushTo(pushSpec + ",notify=" + NotifyHandling.NONE);
+    r.assertOkStatus();
+    assertThat(sender.getMessages()).hasSize(0);
+
+    sender.clear();
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.OWNER);
+    r.assertOkStatus();
+    // no email notification about own changes
+    assertThat(sender.getMessages()).hasSize(0);
+
+    sender.clear();
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.OWNER_REVIEWERS);
+    r.assertOkStatus();
+    assertThat(sender.getMessages()).hasSize(1);
+    Message m = sender.getMessages().get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+
+    sender.clear();
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.ALL);
+    r.assertOkStatus();
+    assertThat(sender.getMessages()).hasSize(1);
+    m = sender.getMessages().get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress, user2.emailAddress);
+  }
+
+  @Test
+  public void pushForMasterWithCc() throws Exception {
     // cc one user
     String topic = "my/topic";
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
@@ -162,7 +235,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);
@@ -186,7 +259,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();
@@ -199,7 +272,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());
@@ -217,7 +303,21 @@
   }
 
   @Test
-  public void testPushForMasterWithApprovals() 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);
+    ChangeInfo ci = get(r.getChangeId());
+    Collection<ChangeMessageInfo> changeMessages = ci.messages;
+    assertThat(changeMessages).hasSize(1);
+    for (ChangeMessageInfo cm : changeMessages) {
+      assertThat(cm.message).isEqualTo(
+          "Uploaded patch set 1.\nmy test message");
+    }
+  }
+
+  @Test
+  public void pushForMasterWithApprovals() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review");
     r.assertOkStatus();
     ChangeInfo ci = get(r.getChangeId());
@@ -262,7 +362,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()
@@ -296,7 +396,38 @@
   }
 
   @Test
-  public void testPushNewPatchsetToRefsChanges() throws Exception {
+  public void pushWithMultipleApprovals()
+      throws Exception {
+    LabelType Q = category("Custom-Label",
+        value(1, "Positive"),
+        value(0, "No score"),
+        value(-1, "Negative"));
+    ProjectConfig config = projectCache.checkedGet(project).getConfig();
+    AccountGroup.UUID anon =
+        SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
+    String heads = "refs/heads/*";
+    Util.allow(config, Permission.forLabel("Custom-Label"), -1, 1, anon, heads);
+    config.getLabelSections().put(Q.getName(), Q);
+    saveProjectConfig(project, config);
+
+    RevCommit c = commitBuilder()
+        .author(admin.getIdent())
+        .committer(admin.getIdent())
+        .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
+        .message(PushOneCommit.SUBJECT)
+        .create();
+
+    pushHead(testRepo, "refs/for/master/%l=Code-Review+1,l=Custom-Label-1", false);
+
+    ChangeInfo ci = get(GitUtil.getChangeId(testRepo, c).get());
+    LabelInfo cr = ci.labels.get("Code-Review");
+    assertThat(cr.all).hasSize(1);
+    cr = ci.labels.get("Custom-Label");
+    assertThat(cr.all).hasSize(1);
+  }
+
+  @Test
+  public void pushNewPatchsetToRefsChanges() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
     PushOneCommit push =
@@ -307,29 +438,41 @@
   }
 
   @Test
-  public void testPushForMasterWithApprovals_MissingLabel() 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 add patch set to "
+        + r.getChange().change().getChangeId()
+        + ". Change is patch set locked.");
+  }
+
+  @Test
+  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 {
-
-    // Hashtags currently only work when noteDB is enabled
-    assume().that(notesMigration.enabled()).isTrue();
+  public void pushForMasterWithHashtags() throws Exception {
+    // Hashtags only work when reading from NoteDB is enabled
+    assume().that(notesMigration.readChanges()).isTrue();
 
     // specify a single hashtag as option
     String hashtag1 = "tag1";
@@ -354,10 +497,9 @@
   }
 
   @Test
-  public void testPushForMasterWithMultipleHashtags() throws Exception {
-
-    // Hashtags currently only work when noteDB is enabled
-    assume().that(notesMigration.enabled()).isTrue();
+  public void pushForMasterWithMultipleHashtags() throws Exception {
+    // Hashtags only work when reading from NoteDB is enabled
+    assume().that(notesMigration.readChanges()).isTrue();
 
     // specify multiple hashtags as options
     String hashtag1 = "tag1";
@@ -385,15 +527,15 @@
   }
 
   @Test
-  public void testPushForMasterWithHashtagsNoteDbDisabled() throws Exception {
-    // push with hashtags should fail when noteDb is disabled
-    assume().that(notesMigration.enabled()).isFalse();
+  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");
     r.assertErrorStatus("cannot add hashtags; noteDb is disabled");
   }
 
   @Test
-  public void testPushCommitUsingSignedOffBy() throws Exception {
+  public void pushCommitUsingSignedOffBy() throws Exception {
     PushOneCommit push =
         pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             "b.txt", "anotherContent");
@@ -418,10 +560,8 @@
   }
 
   @Test
-  public void testCreateNewChangeForAllNotInTarget() throws Exception {
-    ProjectConfig config = projectCache.checkedGet(project).getConfig();
-    config.getProject().setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
-    saveProjectConfig(project, config);
+  public void createNewChangeForAllNotInTarget() throws Exception {
+    enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
         pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
@@ -446,7 +586,7 @@
   }
 
   @Test
-  public void testPushSameCommitTwiceUsingMagicBranchBaseOption()
+  public void pushSameCommitTwiceUsingMagicBranchBaseOption()
       throws Exception {
     grant(Permission.PUSH, project, "refs/heads/master");
     PushOneCommit.Result rBase = pushTo("refs/heads/master");
@@ -465,7 +605,7 @@
     r.assertOkStatus();
 
     PushResult pr = GitUtil.pushHead(
-        testRepo, "refs/for/foo%base=" + rBase.getCommitId().name(), false, false);
+        testRepo, "refs/for/foo%base=" + rBase.getCommit().name(), false, false);
     assertThat(pr.getMessages()).containsMatch("changes: .*new: 1.*done");
 
     assertTwoChangesWithSameRevision(r);
@@ -473,7 +613,7 @@
 
   private void assertTwoChangesWithSameRevision(PushOneCommit.Result result)
       throws Exception {
-    List<ChangeInfo> changes = query(result.getCommitId().name());
+    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);
@@ -482,4 +622,416 @@
     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 = createChanges(n, r);
+
+    // Check that a change was created for each.
+    for (RevCommit c : commits) {
+      assertThat(byCommit(c).change().getSubject())
+          .named("change for " + c.name())
+          .isEqualTo(c.getShortMessage());
+    }
+
+    List<RevCommit> commits2 = amendChanges(initialHead, commits, r);
+
+    // Check that there are correct patch sets.
+    for (int i = 0; i < n; i++) {
+      RevCommit c = commits.get(i);
+      RevCommit c2 = commits2.get(i);
+      String name = "change for " + c2.name();
+      ChangeData cd = byCommit(c);
+      assertThat(cd.change().getSubject())
+          .named(name)
+          .isEqualTo(c2.getShortMessage());
+      assertThat(getPatchSetRevisions(cd)).named(name).containsExactlyEntriesIn(
+          ImmutableMap.of(1, c.name(), 2, c2.name()));
+    }
+
+    // Pushing again results in "no new changes".
+    assertPushRejected(pushHead(testRepo, r, false), r, "no new changes");
+  }
+
+  @Test
+  public void pushWithoutChangeId() throws Exception {
+    testPushWithoutChangeId();
+  }
+
+  @Test
+  public void pushWithoutChangeIdWithCreateNewChangeForAllNotInTarget()
+      throws Exception {
+    enableCreateNewChangeForAllNotInTarget();
+    testPushWithoutChangeId();
+  }
+
+  private void testPushWithoutChangeId() throws Exception {
+    RevCommit c = createCommit(testRepo, "Message without Change-Id");
+    assertThat(GitUtil.getChangeId(testRepo, c).isPresent()).isFalse();
+    pushForReviewRejected(testRepo,
+        "missing Change-Id in commit message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewOk(testRepo);
+  }
+
+  @Test
+  public void pushWithMultipleChangeIds() throws Exception {
+    testPushWithMultipleChangeIds();
+  }
+
+  @Test
+  public void pushWithMultipleChangeIdsWithCreateNewChangeForAllNotInTarget()
+      throws Exception {
+    enableCreateNewChangeForAllNotInTarget();
+    testPushWithMultipleChangeIds();
+  }
+
+  private void testPushWithMultipleChangeIds() throws Exception {
+    createCommit(testRepo,
+        "Message with multiple Change-Id\n"
+            + "\n"
+            + "Change-Id: I10f98c2ef76e52e23aa23be5afeb71e40b350e86\n"
+            + "Change-Id: Ie9a132e107def33bdd513b7854b50de911edba0a\n");
+    pushForReviewRejected(testRepo,
+        "multiple Change-Id lines in commit message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewRejected(testRepo,
+        "multiple Change-Id lines in commit message footer");
+  }
+
+  @Test
+  public void pushWithInvalidChangeId() throws Exception {
+    testpushWithInvalidChangeId();
+  }
+
+  @Test
+  public void pushWithInvalidChangeIdWithCreateNewChangeForAllNotInTarget()
+      throws Exception {
+    enableCreateNewChangeForAllNotInTarget();
+    testpushWithInvalidChangeId();
+  }
+
+  private void testpushWithInvalidChangeId() throws Exception {
+    createCommit(testRepo, "Message with invalid Change-Id\n"
+        + "\n"
+        + "Change-Id: X\n");
+    pushForReviewRejected(testRepo,
+        "invalid Change-Id line format in commit message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewRejected(testRepo,
+        "invalid Change-Id line format in commit message footer");
+  }
+
+  @Test
+  public void pushWithInvalidChangeIdFromEgit() throws Exception {
+    testPushWithInvalidChangeIdFromEgit();
+  }
+
+  @Test
+  public void pushWithInvalidChangeIdFromEgitWithCreateNewChangeForAllNotInTarget()
+      throws Exception {
+    enableCreateNewChangeForAllNotInTarget();
+    testPushWithInvalidChangeIdFromEgit();
+  }
+
+  private void testPushWithInvalidChangeIdFromEgit() throws Exception {
+    createCommit(testRepo, "Message with invalid Change-Id\n"
+        + "\n"
+        + "Change-Id: I0000000000000000000000000000000000000000\n");
+    pushForReviewRejected(testRepo,
+        "invalid Change-Id line format in commit message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewRejected(testRepo,
+        "invalid Change-Id line format in commit message footer");
+  }
+
+  @Test
+  public void pushWithChangeIdInSubjectLine() throws Exception {
+    createCommit(testRepo, "Change-Id: I1234000000000000000000000000000000000000");
+    pushForReviewRejected(testRepo,
+        "missing subject; Change-Id must be in commit message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewRejected(testRepo,
+        "missing subject; Change-Id must be in commit message footer");
+  }
+
+  private static RevCommit createCommit(TestRepository<?> testRepo,
+      String message) throws Exception {
+    return testRepo.branch("HEAD").commit().message(message)
+        .add("a.txt", "content").create();
+  }
+
+  @Test
+  public void cantAutoCloseChangeAlreadyMergedToBranch() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    Change.Id id1 = r1.getChange().getId();
+    PushOneCommit.Result r2 = createChange();
+    Change.Id id2 = r2.getChange().getId();
+
+    // Merge change 1 behind Gerrit's back.
+    try (Repository repo = repoManager.openRepository(project)) {
+      TestRepository<?> tr = new TestRepository<>(repo);
+      tr.branch("refs/heads/master").update(r1.getCommit());
+    }
+
+    assertThat(gApi.changes().id(id1.get()).info().status)
+        .isEqualTo(ChangeStatus.NEW);
+    assertThat(gApi.changes().id(id2.get()).info().status)
+        .isEqualTo(ChangeStatus.NEW);
+    r2 = amendChange(r2.getChangeId());
+    r2.assertOkStatus();
+
+    // Change 1 is still new despite being merged into the branch, because
+    // ReceiveCommits only considers commits between the branch tip (which is
+    // now the merged change 1) and the push tip (new patch set of change 2).
+    assertThat(gApi.changes().id(id1.get()).info().status)
+        .isEqualTo(ChangeStatus.NEW);
+    assertThat(gApi.changes().id(id2.get()).info().status)
+        .isEqualTo(ChangeStatus.NEW);
+  }
+
+  @Test
+  public void accidentallyPushNewPatchSetDirectlyToBranchAndRecoverByPushingToRefsChanges()
+      throws Exception {
+    Change.Id id = accidentallyPushNewPatchSetDirectlyToBranch();
+    ChangeData cd = byChangeId(id);
+    String ps1Rev =
+        Iterables.getOnlyElement(cd.patchSets()).getRevision().get();
+
+    String r = "refs/changes/" + id;
+    assertPushOk(pushHead(testRepo, r, false), r);
+
+    // Added a new patch set and auto-closed the change.
+    cd = byChangeId(id);
+    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(getPatchSetRevisions(cd)).containsExactlyEntriesIn(
+        ImmutableMap.of(
+            1, ps1Rev,
+            2, testRepo.getRepository().resolve("HEAD").name()));
+  }
+
+  @Test
+  public void accidentallyPushNewPatchSetDirectlyToBranchAndCantRecoverByPushingToRefsFor()
+      throws Exception {
+    Change.Id id = accidentallyPushNewPatchSetDirectlyToBranch();
+    ChangeData cd = byChangeId(id);
+    String ps1Rev =
+        Iterables.getOnlyElement(cd.patchSets()).getRevision().get();
+
+    String r = "refs/for/master";
+    assertPushRejected(pushHead(testRepo, r, false), r, "no new changes");
+
+    // Change not updated.
+    cd = byChangeId(id);
+    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.NEW);
+    assertThat(getPatchSetRevisions(cd)).containsExactlyEntriesIn(
+        ImmutableMap.of(1, ps1Rev));
+  }
+
+  private Change.Id accidentallyPushNewPatchSetDirectlyToBranch()
+      throws Exception {
+    PushOneCommit.Result r = createChange();
+    RevCommit ps1Commit = r.getCommit();
+    Change c = r.getChange().change();
+
+    RevCommit ps2Commit;
+    try (Repository repo = repoManager.openRepository(project)) {
+      // Create a new patch set of the change directly in Gerrit's repository,
+      // without pushing it. In reality it's more likely that the client would
+      // create and push this behind Gerrit's back (e.g. an admin accidentally
+      // using direct ssh access to the repo), but that's harder to do in tests.
+      TestRepository<?> tr = new TestRepository<>(repo);
+      ps2Commit = tr.branch("refs/heads/master").commit()
+          .message(ps1Commit.getShortMessage() + " v2")
+          .insertChangeId(r.getChangeId().substring(1))
+          .create();
+    }
+
+    testRepo.git().fetch()
+        .setRefSpecs(new RefSpec("refs/heads/master")).call();
+    testRepo.reset(ps2Commit);
+
+    ChangeData cd = byCommit(ps1Commit);
+    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.NEW);
+    assertThat(getPatchSetRevisions(cd)).containsExactlyEntriesIn(
+        ImmutableMap.of(1, ps1Commit.name()));
+    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);
+  }
+
+  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<>();
+    for (PatchSet ps : cd.patchSets()) {
+      revisions.put(ps.getPatchSetId(), ps.getRevision().get());
+    }
+    return revisions;
+  }
+
+  private ChangeData byCommit(ObjectId id) throws Exception {
+    List<ChangeData> cds = queryProvider.get().byCommit(id);
+    assertThat(cds).named("change for " + id.name()).hasSize(1);
+    return cds.get(0);
+  }
+
+  private ChangeData byChangeId(Change.Id id) throws Exception {
+    List<ChangeData> cds = queryProvider.get().byLegacyChangeId(id);
+    assertThat(cds).named("change " + id).hasSize(1);
+    return cds.get(0);
+  }
+
+  private static void pushForReviewOk(TestRepository<?> testRepo)
+      throws GitAPIException {
+    pushForReview(testRepo, RemoteRefUpdate.Status.OK, null);
+  }
+
+  private static void pushForReviewRejected(TestRepository<?> testRepo,
+      String expectedMessage) throws GitAPIException {
+    pushForReview(testRepo, RemoteRefUpdate.Status.REJECTED_OTHER_REASON,
+        expectedMessage);
+  }
+
+  private static void pushForReview(TestRepository<?> testRepo,
+      RemoteRefUpdate.Status expectedStatus, String expectedMessage)
+          throws GitAPIException {
+    String ref = "refs/for/master";
+    PushResult r = pushHead(testRepo, ref);
+    RemoteRefUpdate refUpdate = r.getRemoteUpdate(ref);
+    assertThat(refUpdate.getStatus()).isEqualTo(expectedStatus);
+    if (expectedMessage != null) {
+      assertThat(refUpdate.getMessage()).contains(expectedMessage);
+    }
+  }
+
+  private void enableCreateNewChangeForAllNotInTarget() throws Exception {
+    ProjectConfig config = projectCache.checkedGet(project).getConfig();
+    config.getProject()
+        .setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
+    saveProjectConfig(project, config);
+  }
 }
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 53412cb..8b77238 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
@@ -16,52 +16,166 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.Nullable;
+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;
 
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 
 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, boolean createEmptyCommit,
+      SubmitType submitType) throws Exception {
+    Project.NameKey project = createProject(name, parent, createEmptyCommit, submitType);
+    grant("push", project, "refs/heads/*");
+    grant("submit", project, "refs/for/refs/heads/*");
+    return cloneProject(project);
+  }
+
+  protected TestRepository<?> createProjectWithPush(String name,
+      @Nullable Project.NameKey parent) throws Exception {
+    return createProjectWithPush(name, parent, true, getSubmitType());
+  }
+
+  protected TestRepository<?> createProjectWithPush(String name,
+      boolean createEmptyCommit) throws Exception {
+    return createProjectWithPush(name, null, createEmptyCommit, getSubmitType());
+  }
+
   protected TestRepository<?> createProjectWithPush(String name)
       throws Exception {
-    Project.NameKey project = createProject(name);
-    grant(Permission.PUSH, project, "refs/heads/*");
-    grant(Permission.SUBMIT, project, "refs/for/refs/heads/*");
-    return cloneProject(project);
+    return createProjectWithPush(name, null, true, getSubmitType());
   }
 
   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 refspec = "HEAD:" + ref;
+
+    String pushedRef = ref;
     if (!topic.isEmpty()) {
-      refspec += "/" + topic;
+      pushedRef += "/" + name(topic);
     }
-    repo.git().push().setRemote("origin").setRefSpecs(
-        new RefSpec(refspec)).call();
+    String refspec = "HEAD:" + pushedRef;
+
+    Iterable<PushResult> res = repo.git().push()
+        .setRemote("origin").setRefSpecs(new RefSpec(refspec)).call();
+
+    RemoteRefUpdate u = Iterables.getOnlyElement(res).getRemoteUpdate(pushedRef);
+    assertThat(u).isNotNull();
+    assertThat(u.getStatus()).isEqualTo(Status.OK);
+    assertThat(u.getNewObjectId()).isEqualTo(ret);
+
     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, 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");
+      SubscribeSection s;
+      ProjectConfig pc = ProjectConfig.read(md);
+      if (pc.getSubscribeSections().containsKey(superName)) {
+        s = pc.getSubscribeSections().get(superName);
+      } else {
+        s = new SubscribeSection(superName);
+      }
+      String refspec;
+      if (superBranch == null) {
+        refspec = subBranch;
+      } else {
+        refspec = subBranch + ":" + superBranch;
+      }
+      if (match) {
+        s.addMatchingRefSpec(refspec);
+      } else {
+        s.addMultiMatchRefSpec(refspec);
+      }
+      pc.addSubscribeSection(s);
+      ObjectId oldId = pc.getRevision();
+      ObjectId newId = pc.commit(md);
+      assertThat(newId).isNotEqualTo(oldId);
+      projectCache.evict(pc.getProject());
+    }
+  }
+
+  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();
@@ -69,18 +183,48 @@
     pushSubmoduleConfig(repo, branch, config);
   }
 
+  protected void createRelativeSubmoduleSubscription(TestRepository<?> repo,
+      String branch, String subscribeToRepoPrefix, String subscribeToRepo,
+      String subscribeToBranch) throws Exception {
+    Config config = new Config();
+    prepareRelativeSubmoduleConfigEntry(config, subscribeToRepoPrefix,
+        subscribeToRepo, subscribeToBranch);
+    pushSubmoduleConfig(repo, branch, config);
+  }
+
+  protected void prepareRelativeSubmoduleConfigEntry(Config config,
+      String subscribeToRepoPrefix, String subscribeToRepo,
+      String subscribeToBranch) {
+    subscribeToRepo = name(subscribeToRepo);
+    String url = subscribeToRepoPrefix + subscribeToRepo;
+    config.setString("submodule", subscribeToRepo, "path", subscribeToRepo);
+    config.setString("submodule", subscribeToRepo, "url", url);
+    if (subscribeToBranch != null) {
+      config.setString("submodule", subscribeToRepo, "branch", subscribeToBranch);
+    }
+  }
+
   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);
     }
   }
 
@@ -97,20 +241,110 @@
   }
 
   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);
     ObjectId commitId = repo.git().fetch().setRemote("origin").call()
         .getAdvertisedRef("refs/heads/" + branch).getObjectId();
 
-    try (RevWalk rw = repo.getRevWalk()) {
-      RevCommit c = rw.parseCommit(commitId);
-      rw.parseBody(c.getTree());
+    RevWalk rw = repo.getRevWalk();
+    RevCommit c = rw.parseCommit(commitId);
+    rw.parseBody(c.getTree());
 
-      RevTree tree = c.getTree();
-      RevObject actualId = repo.get(tree, submodule);
+    RevTree tree = c.getTree();
+    RevObject actualId = repo.get(tree, submodule);
 
-      assertThat(actualId).isEqualTo(expectedId);
+    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);
+    Ref branchTip = repo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/" + branch);
+    if (branchTip == null) {
+      return false;
     }
+
+    ObjectId commitId = branchTip.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/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUILD
new file mode 100644
index 0000000..db0d8e9
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUILD
@@ -0,0 +1,26 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'git',
+  srcs = glob(['*IT.java']),
+  deps = [
+    ':submodule_util',
+    ':push_for_review',
+  ],
+  labels = ['git'],
+)
+
+java_library(
+  name = 'push_for_review',
+  srcs = ['AbstractPushForReview.java'],
+  deps = [
+    '//gerrit-acceptance-tests:lib',
+    '//lib/joda:joda-time',
+  ],
+)
+
+java_library(
+  name = 'submodule_util',
+  srcs = ['AbstractSubmoduleSubscription.java',],
+  deps = ['//gerrit-acceptance-tests:lib',]
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
index 27b8b0a..41f47a2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
@@ -15,13 +15,11 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
-import static com.google.gerrit.server.project.Util.block;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.server.git.ProjectConfig;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -31,9 +29,7 @@
 
   @Before
   public void setUp() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    block(cfg, Permission.PUSH, ANONYMOUS_USERS, "refs/drafts/*");
-    saveProjectConfig(project, cfg);
+    block(Permission.PUSH, ANONYMOUS_USERS, "refs/drafts/*");
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
new file mode 100644
index 0000000..e7097f0
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.server.git.ProjectConfig;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class ImplicitMergeCheckIT extends AbstractDaemonTest {
+
+  @Test
+  public void implicitMergeViaFastForward() throws Exception {
+    setRejectImplicitMerges();
+
+    pushHead(testRepo, "refs/heads/stable", false);
+    PushOneCommit.Result m = push("refs/heads/master", "0", "file", "0");
+    PushOneCommit.Result c = push("refs/for/stable", "1", "file", "1");
+
+    c.assertMessage(implicitMergeOf(m.getCommit()));
+    c.assertErrorStatus();
+  }
+
+  @Test
+  public void implicitMergeViaRealMerge() throws Exception {
+    setRejectImplicitMerges();
+
+    ObjectId base = repo().exactRef("HEAD").getObjectId();
+    push("refs/heads/stable", "0", "f", "0");
+    testRepo.reset(base);
+    PushOneCommit.Result m = push("refs/heads/master", "1", "f", "1");
+    PushOneCommit.Result c = push("refs/for/stable", "2", "f", "2");
+
+    c.assertMessage(implicitMergeOf(m.getCommit()));
+    c.assertErrorStatus();
+  }
+
+  @Test
+  public void implicitMergeCheckOff() throws Exception {
+    ObjectId base = repo().exactRef("HEAD").getObjectId();
+    push("refs/heads/stable", "0", "f", "0");
+    testRepo.reset(base);
+    PushOneCommit.Result m = push("refs/heads/master", "1", "f", "1");
+    PushOneCommit.Result c = push("refs/for/stable", "2", "f", "2");
+
+    assertThat(c.getMessage().toLowerCase()).doesNotContain(
+        implicitMergeOf(m.getCommit()));
+  }
+
+  @Test
+  public void notImplicitMerge_noWarning() throws Exception {
+    setRejectImplicitMerges();
+
+    ObjectId base = repo().exactRef("HEAD").getObjectId();
+    push("refs/heads/stable", "0", "f", "0");
+    testRepo.reset(base);
+    PushOneCommit.Result m = push("refs/heads/master", "1", "f", "1");
+    PushOneCommit.Result c = push("refs/for/master", "2", "f", "2");
+
+    assertThat(c.getMessage().toLowerCase()).doesNotContain(
+        implicitMergeOf(m.getCommit()));
+  }
+
+  private static String implicitMergeOf(ObjectId commit) {
+    return "implicit merge of " + commit.abbreviate(7).name();
+  }
+
+  private void setRejectImplicitMerges() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getProject().setRejectImplicitMerges(InheritableBoolean.TRUE);
+    saveProjectConfig(project, cfg);
+  }
+
+  private PushOneCommit.Result push(String ref, String subject,
+      String fileName, String content) throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+        subject, fileName, content);
+    return push.to(ref);
+  }
+}
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 cbfa8c7..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
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -25,10 +27,10 @@
 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.git.CommitMergeStatus;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -41,16 +43,11 @@
 import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Test;
 
-import java.io.IOException;
-
 @NoHttpd
 public class SubmitOnPushIT extends AbstractDaemonTest {
   @Inject
   private ApprovalsUtil approvalsUtil;
 
-  @Inject
-  private ChangeNotes.Factory changeNotesFactory;
-
   @Test
   public void submitOnPush() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
@@ -97,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
@@ -117,7 +114,8 @@
         push("refs/for/master%submit", "other change", "a.txt", "other content");
     r.assertErrorStatus();
     r.assertChange(Change.Status.NEW, null);
-    r.assertMessage(CommitMergeStatus.PATH_CONFLICT.getMessage());
+    r.assertMessage("Change " + r.getChange().getId()
+        + ": change could not be merged due to a path conflict.");
   }
 
   @Test
@@ -145,9 +143,9 @@
         "other content", r.getChangeId());
     r.assertOkStatus();
     r.assertChange(Change.Status.MERGED, null, admin);
-    Change c = Iterables.getOnlyElement(
-        queryProvider.get().byKeyPrefix(r.getChangeId())).change();
-    assertThat(db.patchSets().byChange(c.getId()).toList()).hasSize(2);
+    ChangeData cd = Iterables.getOnlyElement(
+        queryProvider.get().byKeyPrefix(r.getChangeId()));
+    assertThat(cd.patchSets()).hasSize(2);
     assertSubmitApproval(r.getPatchSetId());
     assertCommit(project, "refs/heads/master");
   }
@@ -189,12 +187,20 @@
     r.assertOkStatus();
 
     git().push()
-        .setRefSpecs(new RefSpec(r.getCommitId().name() + ":refs/heads/master"))
+        .setRefSpecs(new RefSpec(r.getCommit().name() + ":refs/heads/master"))
         .call();
     assertCommit(project, "refs/heads/master");
-    assertThat(getSubmitter(r.getPatchSetId())).isNull();
-    Change c = db.changes().get(r.getPatchSetId().getParentKey());
-    assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED);
+
+    ChangeData cd = Iterables.getOnlyElement(
+        queryProvider.get().byKey(new Change.Key(r.getChangeId())));
+    RevCommit c = r.getCommit();
+    PatchSet.Id psId = cd.currentPatchSet().getId();
+    assertThat(psId.get()).isEqualTo(1);
+    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertSubmitApproval(psId);
+
+    assertThat(cd.patchSets()).hasSize(1);
+    assertThat(cd.patchSet(psId).getRevision().get()).isEqualTo(c.name());
   }
 
   @Test
@@ -202,6 +208,9 @@
     grant(Permission.PUSH, project, "refs/heads/master");
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
+    RevCommit c1 = r.getCommit();
+    PatchSet.Id psId1 = r.getPatchSetId();
+    assertThat(psId1.get()).isEqualTo(1);
 
     PushOneCommit push =
         pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
@@ -210,27 +219,85 @@
     r = push.to("refs/heads/master");
     r.assertOkStatus();
 
+    ChangeData cd = r.getChange();
+    RevCommit c2 = r.getCommit();
+    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    PatchSet.Id psId2 = cd.change().currentPatchSetId();
+    assertThat(psId2.get()).isEqualTo(2);
     assertCommit(project, "refs/heads/master");
-    assertThat(getSubmitter(r.getPatchSetId())).isNull();
-    Change c = db.changes().get(r.getPatchSetId().getParentKey());
-    assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED);
+    assertSubmitApproval(psId2);
+
+    assertThat(cd.patchSets()).hasSize(2);
+    assertThat(cd.patchSet(psId1).getRevision().get()).isEqualTo(c1.name());
+    assertThat(cd.patchSet(psId2).getRevision().get()).isEqualTo(c2.name());
+  }
+
+  @Test
+  public void mergeMultipleOnPushToBranchWithNewPatchset() throws Exception {
+    grant(Permission.PUSH, project, "refs/heads/master");
+
+    // Create 2 changes.
+    ObjectId initialHead = getRemoteHead();
+    PushOneCommit.Result r1 = createChange("Change 1", "a", "a");
+    r1.assertOkStatus();
+    PushOneCommit.Result r2 = createChange("Change 2", "b", "b");
+    r2.assertOkStatus();
+
+    RevCommit c1_1 = r1.getCommit();
+    RevCommit c2_1 = r2.getCommit();
+    PatchSet.Id psId1_1 = r1.getPatchSetId();
+    PatchSet.Id psId2_1 = r2.getPatchSetId();
+    assertThat(c1_1.getParent(0)).isEqualTo(initialHead);
+    assertThat(c2_1.getParent(0)).isEqualTo(c1_1);
+
+    // Amend both changes.
+    testRepo.reset(initialHead);
+    RevCommit c1_2 = testRepo.branch("HEAD").commit()
+        .message(c1_1.getShortMessage() + "v2")
+        .insertChangeId(r1.getChangeId().substring(1))
+        .create();
+    RevCommit c2_2 = testRepo.cherryPick(c2_1);
+
+    // Push directly to branch.
+    assertPushOk(
+        pushHead(testRepo, "refs/heads/master", false), "refs/heads/master");
+
+    ChangeData cd2 = r2.getChange();
+    assertThat(cd2.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    PatchSet.Id psId2_2 = cd2.change().currentPatchSetId();
+    assertThat(psId2_2.get()).isEqualTo(2);
+    assertThat(cd2.patchSet(psId2_1).getRevision().get())
+        .isEqualTo(c2_1.name());
+    assertThat(cd2.patchSet(psId2_2).getRevision().get())
+        .isEqualTo(c2_2.name());
+
+    ChangeData cd1 = r1.getChange();
+    assertThat(cd1.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    PatchSet.Id psId1_2 = cd1.change().currentPatchSetId();
+    assertThat(psId1_2.get()).isEqualTo(2);
+    assertThat(cd1.patchSet(psId1_1).getRevision().get())
+        .isEqualTo(c1_1.name());
+    assertThat(cd1.patchSet(psId1_2).getRevision().get())
+        .isEqualTo(c1_2.name());
   }
 
   private PatchSetApproval getSubmitter(PatchSet.Id patchSetId)
-      throws OrmException {
-    Change c = db.changes().get(patchSetId.getParentKey());
-    ChangeNotes notes = changeNotesFactory.create(c).load();
+      throws Exception {
+    ChangeNotes notes =
+        notesFactory.createChecked(db, project, patchSetId.getParentKey())
+            .load();
     return approvalsUtil.getSubmitter(db, notes, patchSetId);
   }
 
-  private void assertSubmitApproval(PatchSet.Id patchSetId) throws OrmException {
+  private void assertSubmitApproval(PatchSet.Id patchSetId) throws Exception {
     PatchSetApproval a = getSubmitter(patchSetId);
-    assertThat(a.isSubmit()).isTrue();
+    assertThat(a.isLegacySubmit()).isTrue();
     assertThat(a.getValue()).isEqualTo((short) 1);
     assertThat(a.getAccountId()).isEqualTo(admin.id);
   }
 
-  private void assertCommit(Project.NameKey project, String branch) throws IOException {
+  private void assertCommit(Project.NameKey project, String branch)
+      throws Exception {
     try (Repository r = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(r)) {
       RevCommit c = rw.parseCommit(r.exactRef(branch).getObjectId());
@@ -241,7 +308,7 @@
     }
   }
 
-  private void assertMergeCommit(String branch, String subject) throws IOException {
+  private void assertMergeCommit(String branch, String subject) throws Exception {
     try (Repository r = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(r)) {
       RevCommit c = rw.parseCommit(r.exactRef(branch).getObjectId());
@@ -254,7 +321,7 @@
   }
 
   private void assertTag(Project.NameKey project, String branch,
-      PushOneCommit.Tag tag) throws IOException {
+      PushOneCommit.Tag tag) throws Exception {
     try (Repository repo = repoManager.openRepository(project)) {
       Ref tagRef = repo.findRef(tag.name);
       assertThat(tagRef).isNotNull();
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
new file mode 100644
index 0000000..09e498f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java
@@ -0,0 +1,393 @@
+// 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.git;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
+import com.google.gerrit.server.util.SubmoduleSectionParser;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+import java.util.Set;
+
+public class SubmoduleSectionParserIT extends AbstractDaemonTest {
+  private static final String THIS_SERVER = "http://localhost/";
+
+  @Test
+  public void testFollowMasterBranch() throws Exception {
+    Project.NameKey p = createProject("a");
+    Config cfg = new Config();
+    cfg.fromText(""
+        + "[submodule \"a\"]\n"
+        + "path = localpath-to-a\n"
+        + "url = ssh://localhost/" + p.get() + "\n"
+        + "branch = master\n");
+    Branch.NameKey targetBranch = new Branch.NameKey(
+        new Project.NameKey("project"), "master");
+
+    Set<SubmoduleSubscription> res = new SubmoduleSectionParser(
+        cfg, THIS_SERVER, targetBranch).parseAllSections();
+
+    Set<SubmoduleSubscription> expected = Sets.newHashSet(
+        new SubmoduleSubscription(targetBranch, new Branch.NameKey(
+            p, "master"), "localpath-to-a"));
+
+    assertThat(res).containsExactlyElementsIn(expected);
+  }
+
+  @Test
+  public void testFollowMatchingBranch() throws Exception {
+    Project.NameKey p = createProject("a");
+    Config cfg = new Config();
+    cfg.fromText(""
+        + "[submodule \"a\"]\n"
+        + "path = a\n"
+        + "url = ssh://localhost/" + p.get() + "\n"
+        + "branch = .\n");
+
+    Branch.NameKey targetBranch1 = new Branch.NameKey(
+        new Project.NameKey("project"), "master");
+
+    Set<SubmoduleSubscription> res1 = new SubmoduleSectionParser(
+        cfg, THIS_SERVER, targetBranch1).parseAllSections();
+
+    Set<SubmoduleSubscription> expected1 = Sets.newHashSet(
+        new SubmoduleSubscription(targetBranch1, new Branch.NameKey(
+            p, "master"), "a"));
+
+    assertThat(res1).containsExactlyElementsIn(expected1);
+
+    Branch.NameKey targetBranch2 = new Branch.NameKey(
+        new Project.NameKey("project"), "somebranch");
+
+    Set<SubmoduleSubscription> res2 = new SubmoduleSectionParser(
+        cfg, THIS_SERVER, targetBranch2).parseAllSections();
+
+    Set<SubmoduleSubscription> expected2 = Sets.newHashSet(
+        new SubmoduleSubscription(targetBranch2, new Branch.NameKey(
+            p, "somebranch"), "a"));
+
+    assertThat(res2).containsExactlyElementsIn(expected2);
+  }
+
+  @Test
+  public void testFollowAnotherBranch() throws Exception {
+    Project.NameKey p = createProject("a");
+    Config cfg = new Config();
+    cfg.fromText(""
+        + "[submodule \"a\"]\n"
+        + "path = a\n"
+        + "url = ssh://localhost/" + p.get() + "\n"
+        + "branch = anotherbranch\n");
+
+    Branch.NameKey targetBranch = new Branch.NameKey(
+        new Project.NameKey("project"), "master");
+
+    Set<SubmoduleSubscription> res = new SubmoduleSectionParser(
+        cfg, THIS_SERVER, targetBranch).parseAllSections();
+
+    Set<SubmoduleSubscription> expected = Sets.newHashSet(
+        new SubmoduleSubscription(targetBranch, new Branch.NameKey(
+            p, "anotherbranch"), "a"));
+
+    assertThat(res).containsExactlyElementsIn(expected);
+  }
+
+  @Test
+  public void testWithAnotherURI() throws Exception {
+    Project.NameKey p = createProject("a");
+    Config cfg = new Config();
+    cfg.fromText(""
+        + "[submodule \"a\"]\n"
+        + "path = a\n"
+        + "url = http://localhost:80/" + p.get() + "\n"
+        + "branch = master\n");
+
+    Branch.NameKey targetBranch = new Branch.NameKey(
+        new Project.NameKey("project"), "master");
+
+    Set<SubmoduleSubscription> res =new SubmoduleSectionParser(
+        cfg, THIS_SERVER, targetBranch).parseAllSections();
+
+    Set<SubmoduleSubscription> expected = Sets.newHashSet(
+        new SubmoduleSubscription(targetBranch, new Branch.NameKey(
+            p, "master"), "a"));
+
+    assertThat(res).containsExactlyElementsIn(expected);
+  }
+
+  @Test
+  public void testWithSlashesInProjectName() throws Exception {
+    Project.NameKey p = createProject("project/with/slashes/a");
+    Config cfg = new Config();
+    cfg.fromText(""
+        + "[submodule \"project/with/slashes/a\"]\n"
+        + "path = a\n"
+        + "url = http://localhost:80/" + p.get() + "\n"
+        + "branch = master\n");
+
+    Branch.NameKey targetBranch = new Branch.NameKey(
+        new Project.NameKey("project"), "master");
+
+    Set<SubmoduleSubscription> res = new SubmoduleSectionParser(
+        cfg, THIS_SERVER, targetBranch).parseAllSections();
+
+    Set<SubmoduleSubscription> expected = Sets.newHashSet(
+        new SubmoduleSubscription(targetBranch, new Branch.NameKey(
+            p, "master"), "a"));
+
+    assertThat(res).containsExactlyElementsIn(expected);
+  }
+
+  @Test
+  public void testWithSlashesInPath() throws Exception {
+    Project.NameKey p = createProject("a");
+    Config cfg = new Config();
+    cfg.fromText(""
+        + "[submodule \"a\"]\n"
+        + "path = a/b/c/d/e\n"
+        + "url = http://localhost:80/" + p.get() + "\n"
+        + "branch = master\n");
+
+    Branch.NameKey targetBranch = new Branch.NameKey(
+        new Project.NameKey("project"), "master");
+
+    Set<SubmoduleSubscription> res = new SubmoduleSectionParser(
+        cfg, THIS_SERVER, targetBranch).parseAllSections();
+
+    Set<SubmoduleSubscription> expected = Sets.newHashSet(
+        new SubmoduleSubscription(targetBranch, new Branch.NameKey(
+            p, "master"), "a/b/c/d/e"));
+
+    assertThat(res).containsExactlyElementsIn(expected);
+  }
+
+  @Test
+  public void testWithMoreSections() throws Exception {
+    Project.NameKey p1 = createProject("a");
+    Project.NameKey p2 = createProject("b");
+    Config cfg = new Config();
+    cfg.fromText(""
+        + "[submodule \"a\"]\n"
+        + "     path = a\n"
+        + "     url = ssh://localhost/" + p1.get() + "\n"
+        + "     branch = .\n"
+        + "[submodule \"b\"]\n"
+        + "		path = b\n"
+        + "		url = http://localhost:80/" + p2.get() + "\n"
+        + "		branch = master\n");
+
+    Branch.NameKey targetBranch = new Branch.NameKey(
+        new Project.NameKey("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"),
+        new SubmoduleSubscription(targetBranch, new Branch.NameKey(
+            p2, "master"), "b"));
+
+    assertThat(res).containsExactlyElementsIn(expected);
+  }
+
+  @Test
+  public void testWithSubProjectFound() throws Exception {
+    Project.NameKey p1 = createProject("a/b");
+    Project.NameKey p2 = createProject("b");
+    Config cfg = new Config();
+    cfg.fromText("\n"
+        + "[submodule \"a/b\"]\n"
+        + "path = a/b\n"
+        + "url = ssh://localhost/" + p1.get() + "\n"
+        + "branch = .\n"
+        + "[submodule \"b\"]\n"
+        + "path = b\n"
+        + "url = http://localhost/" + p2.get() + "\n"
+        + "branch = .\n");
+
+    Branch.NameKey targetBranch = new Branch.NameKey(
+        new Project.NameKey("project"), "master");
+
+    Set<SubmoduleSubscription> res = new SubmoduleSectionParser(
+        cfg, THIS_SERVER, targetBranch).parseAllSections();
+
+    Set<SubmoduleSubscription> expected = Sets.newHashSet(
+        new SubmoduleSubscription(targetBranch, new Branch.NameKey(
+            p2, "master"), "b"),
+        new SubmoduleSubscription(targetBranch, new Branch.NameKey(
+            p1, "master"), "a/b"));
+
+    assertThat(res).containsExactlyElementsIn(expected);
+  }
+
+  @Test
+  public void testWithAnInvalidSection() throws Exception {
+    Project.NameKey p1 = createProject("a");
+    Project.NameKey p2 = createProject("b");
+    Project.NameKey p3 = createProject("d");
+    Project.NameKey p4 = createProject("e");
+    Config cfg = new Config();
+    cfg.fromText("\n"
+        + "[submodule \"a\"]\n"
+        + "    path = a\n"
+        + "    url = ssh://localhost/" + p1.get() + "\n"
+        + "    branch = .\n"
+        + "[submodule \"b\"]\n"
+            // path missing
+        + "    url = http://localhost:80/" + p2.get() + "\n"
+        + "    branch = master\n"
+        + "[submodule \"c\"]\n"
+        + "    path = c\n"
+            // url missing
+        + "    branch = .\n"
+        + "[submodule \"d\"]\n"
+        + "    path = d-parent/the-d-folder\n"
+        + "    url = ssh://localhost/" + p3.get() + "\n"
+            // branch missing
+        + "[submodule \"e\"]\n"
+        + "    path = e\n"
+        + "    url = ssh://localhost/" + p4.get() + "\n"
+        + "    branch = refs/heads/master\n");
+
+    Branch.NameKey targetBranch = new Branch.NameKey(
+        new Project.NameKey("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"),
+        new SubmoduleSubscription(targetBranch, new Branch.NameKey(
+            p4, "master"), "e"));
+
+    assertThat(res).containsExactlyElementsIn(expected);
+  }
+
+  @Test
+  public void testWithSectionOfNonexistingProject() throws Exception {
+    Config cfg = new Config();
+    cfg.fromText("\n"
+        + "[submodule \"a\"]\n"
+        + "path = a\n"
+        + "url = ssh://non-localhost/a\n"
+        // Project "a" doesn't exist
+        + "branch = .\\n");
+
+    Branch.NameKey targetBranch = new Branch.NameKey(
+        new Project.NameKey("project"), "master");
+
+    Set<SubmoduleSubscription> res = new SubmoduleSectionParser(
+        cfg, THIS_SERVER, targetBranch).parseAllSections();
+
+    assertThat(res).isEmpty();
+  }
+
+  @Test
+  public void testWithSectionToOtherServer() throws Exception {
+    Project.NameKey p1 = createProject("a");
+    Config cfg = new Config();
+    cfg.fromText(""
+        + "[submodule \"a\"]"
+        + "path = a"
+        + "url = ssh://non-localhost/" + p1.get() + "\n"
+        + "branch = .");
+
+    Branch.NameKey targetBranch = new Branch.NameKey(
+        new Project.NameKey("project"), "master");
+
+    Set<SubmoduleSubscription> res = new SubmoduleSectionParser(
+        cfg, THIS_SERVER, targetBranch).parseAllSections();
+
+    assertThat(res).isEmpty();
+  }
+
+  @Test
+  public void testWithRelativeURI() throws Exception {
+    Project.NameKey p1 = createProject("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("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);
+  }
+
+  @Test
+  public void testWithDeepRelativeURI() throws Exception {
+    Project.NameKey p1 = createProject("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);
+  }
+
+  @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 1216b00..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
@@ -16,17 +16,20 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.testutil.ConfigSuite;
 
 import org.eclipse.jgit.junit.TestRepository;
 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;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.junit.Test;
 
 @NoHttpd
@@ -37,12 +40,45 @@
   }
 
   @Test
-  public void testSubscriptionToEmptyRepo() throws Exception {
+  @GerritConfig(name = "submodule.enableSuperProjectSubscriptions", value = "false")
+  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");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
+    pushChangeTo(subRepo, "master");
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isFalse();
+  }
+
+  @Test
+  public void testSubscriptionToEmptyRepo() 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");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isTrue();
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEAD);
   }
@@ -51,74 +87,242 @@
   public void testSubscriptionToExistingRepo() 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");
+    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);
   }
 
   @Test
+  public void testSubscriptionWildcardACLForSingleBranch() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    // 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");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "branch",
+        "subscribed-to-project", "master");
+
+    ObjectId subHEAD = pushChangeTo(subRepo, "master");
+
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEAD);
+    assertThat(hasSubmodule(superRepo, "branch",
+        "subscribed-to-project")).isFalse();
+  }
+
+  @Test
+  public void testSubscriptionWildcardACLForMissingProject() throws Exception {
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+        "not-existing-super-project", "refs/heads/*");
+    pushChangeTo(subRepo, "master");
+  }
+
+  @Test
+  public void testSubscriptionWildcardACLForMissingBranch() throws Exception {
+    createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+        "super-project", "refs/heads/*");
+    pushChangeTo(subRepo, "foo");
+  }
+
+  @Test
+  public void testSubscriptionWildcardACLForMissingGitmodules() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+        "super-project", "refs/heads/*");
+    pushChangeTo(superRepo, "master");
+    pushChangeTo(subRepo, "master");
+  }
+
+  @Test
+  public void testSubscriptionWildcardACLOneOnOneMapping() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    // any branch is allowed to be subscribed to the same superprojects branch:
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+        "super-project", "refs/heads/*");
+
+    // create 'branch' in both repos:
+    pushChangeTo(superRepo, "branch");
+    pushChangeTo(subRepo, "branch");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "branch",
+        "subscribed-to-project", "branch");
+
+    ObjectId subHEAD1 = pushChangeTo(subRepo, "master");
+    ObjectId subHEAD2 = pushChangeTo(subRepo, "branch");
+
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEAD1);
+    expectToHaveSubmoduleState(superRepo, "branch",
+        "subscribed-to-project", subHEAD2);
+
+    // Now test that cross subscriptions do not work:
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "branch");
+    ObjectId subHEAD3 = pushChangeTo(subRepo, "branch");
+
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEAD1);
+    expectToHaveSubmoduleState(superRepo, "branch",
+        "subscribed-to-project", subHEAD3);
+  }
+
+  @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");
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
 
     // The first update doesn't include any commit messages
     ObjectId subRepoId = pushChangeTo(subRepo, "master");
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subRepoId);
     expectToHaveCommitMessage(superRepo, "master",
-        "Updated git submodules\n\n");
+        "Update git submodules\n\n");
 
     // Any following update also has a short message
     subRepoId = pushChangeTo(subRepo, "master");
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subRepoId);
     expectToHaveCommitMessage(superRepo, "master",
-        "Updated git submodules\n\n");
+        "Update git submodules\n\n");
   }
 
   @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");
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "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();
-    RevCommit subCommitMsg = rw.parseCommit(subHEAD);
     expectToHaveCommitMessage(superRepo, "master",
-        "Updated git submodules\n\n" +
-        "Project: " + name("subscribed-to-project")
-            + " master " + subHEAD.name() + "\n\n");
+        "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");
-    subCommitMsg = rw.parseCommit(subHEAD);
+    RevCommit subCommitMsg = rw.parseCommit(subHEAD);
     expectToHaveCommitMessage(superRepo, "master",
-        "Updated git submodules\n\n" +
-        "Project: " + name("subscribed-to-project")
-            + " master " + subHEAD.name() + "\n\n" +
-        subCommitMsg.getFullMessage() + "\n\n");
+        "Update git submodules\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");
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
 
     pushChangeTo(subRepo, "master");
     ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
@@ -136,12 +340,16 @@
   }
 
   @Test
-  public void testSubscriptionUnsubscribeByDeletingGitModules() throws Exception {
+  public void testSubscriptionUnsubscribeByDeletingGitModules()
+      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");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
 
     pushChangeTo(subRepo, "master");
     ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
@@ -162,8 +370,11 @@
   public void testSubscriptionToDifferentBranches() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/foo",
+        "super-project", "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "foo");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "foo");
     ObjectId subFoo = pushChangeTo(subRepo, "foo");
     pushChangeTo(subRepo, "master");
 
@@ -172,84 +383,164 @@
   }
 
   @Test
-  public void testCircularSubscriptionIsDetected() throws Exception {
+  public void testBranchCircularSubscription() 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/master",
+        "subscribed-to-project", "refs/heads/master");
+
+    pushChangeTo(subRepo, "master");
+    pushChangeTo(superRepo, "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
+    createSubmoduleSubscription(subRepo, "master", "super-project", "master");
+
+    pushChangeTo(subRepo, "master");
+    pushChangeTo(superRepo, "master");
+
+    assertThat(hasSubmodule(subRepo, "master",
+        "super-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master",
+        "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 {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(subRepo, "master", "super-project", "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
+    pushChangeTo(subRepo, "master");
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isFalse();
+  }
 
+  @Test
+  public void testSubscriptionFailOnWrongProjectACL() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "wrong-super-project", "refs/heads/master");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
+    pushChangeTo(subRepo, "master");
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isFalse();
+  }
+
+  @Test
+  public void testSubscriptionFailOnWrongBranchACL() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/wrong-branch");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
+    pushChangeTo(subRepo, "master");
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isFalse();
+  }
+
+  @Test
+  public void testSubscriptionInheritACL() throws Exception {
+    createProjectWithPush("config-repo");
+    createProjectWithPush("config-repo2",
+        new Project.NameKey(name("config-repo")));
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project",
+        new Project.NameKey(name("config-repo2")));
+    allowMatchingSubmoduleSubscription("config-repo", "refs/heads/*",
+        "super-project", "refs/heads/*");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
-    pushChangeTo(superRepo, "master");
-
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEAD);
-
-    assertThat(hasSubmodule(subRepo, "master", "super-project")).isFalse();
   }
 
-  private void deleteAllSubscriptions(TestRepository<?> repo, String branch)
-      throws Exception {
-    repo.git().fetch().setRemote("origin").call();
-    repo.reset("refs/remotes/origin/" + branch);
+  @Test
+  public void testAllowedButNotSubscribed() 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");
 
-    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();
+    pushChangeTo(subRepo, "master");
+    subRepo.branch("HEAD").commit().insertChangeId()
+        .message("some change")
+        .add("b.txt", "b contents for testing")
+        .create();
+    String refspec = "HEAD:refs/heads/master";
+    PushResult r = Iterables.getOnlyElement(subRepo.git().push()
+        .setRemote("origin")
+        .setRefSpecs(new RefSpec(refspec))
+        .call());
+    assertThat(r.getMessages()).doesNotContain("error");
+    assertThat(r.getRemoteUpdate("refs/heads/master").getStatus())
+    .isEqualTo(RemoteRefUpdate.Status.OK);
 
-    ObjectId actualId = repo.git().fetch().setRemote("origin").call()
-      .getAdvertisedRef("refs/heads/master").getObjectId();
-    assertThat(actualId).isEqualTo(expectedId);
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isFalse();
   }
 
-  private void deleteGitModulesFile(TestRepository<?> repo, String branch)
-      throws Exception {
-    repo.git().fetch().setRemote("origin").call();
-    repo.reset("refs/remotes/origin/" + branch);
+  @Test
+  public void testSubscriptionDeepRelative() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush(
+        "nested/subscribed-to-project");
+    // master is allowed to be subscribed to any superprojects branch:
+    allowMatchingSubmoduleSubscription("nested/subscribed-to-project",
+        "refs/heads/master", "super-project", null);
 
-    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();
+    pushChangeTo(subRepo, "master");
+    createRelativeSubmoduleSubscription(superRepo, "master",
+        "../", "nested/subscribed-to-project", "master");
 
-    ObjectId actualId = repo.git().fetch().setRemote("origin").call()
-      .getAdvertisedRef("refs/heads/master").getObjectId();
-    assertThat(actualId).isEqualTo(expectedId);
-  }
+    ObjectId subHEAD = pushChangeTo(subRepo, "master");
 
-  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);
+    expectToHaveSubmoduleState(superRepo, "master",
+        "nested/subscribed-to-project", subHEAD);
   }
 }
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 e4a054a..98405d7 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,14 +35,32 @@
   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");
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/master");
+
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
 
     ObjectId subHEAD = subRepo.branch("HEAD").commit().insertChangeId()
@@ -90,12 +110,84 @@
   }
 
   @Test
+  public void testSubscriptionUpdateIncludingChangeInSuperproject() 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");
+
+    ObjectId subHEAD = subRepo.branch("HEAD").commit().insertChangeId()
+        .message("some change")
+        .add("a.txt", "a contents ")
+        .create();
+    subRepo.git().push().setRemote("origin").setRefSpecs(
+          new RefSpec("HEAD:refs/heads/master")).call();
+
+    RevCommit c = subRepo.getRevWalk().parseCommit(subHEAD);
+
+    RevCommit c1 = subRepo.branch("HEAD").commit().insertChangeId()
+      .message("first change")
+      .add("asdf", "asdf\n")
+      .create();
+    subRepo.git().push().setRemote("origin")
+      .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo")))
+      .call();
+
+    subRepo.reset(c.getId());
+    RevCommit c2 = subRepo.branch("HEAD").commit().insertChangeId()
+      .message("qwerty")
+      .add("qwerty", "qwerty")
+      .create();
+
+    RevCommit c3 = subRepo.branch("HEAD").commit().insertChangeId()
+      .message("qwerty followup")
+      .add("qwerty", "qwerty\nqwerty\n")
+      .create();
+    subRepo.git().push().setRemote("origin")
+      .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo")))
+      .call();
+
+    RevCommit c4 = superRepo.branch("HEAD").commit().insertChangeId()
+      .message("new change on superproject")
+      .add("foo", "bar")
+      .create();
+    superRepo.git().push().setRemote("origin")
+      .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo")))
+      .call();
+
+    String id1 = getChangeId(subRepo, c1).get();
+    String id2 = getChangeId(subRepo, c2).get();
+    String id3 = getChangeId(subRepo, c3).get();
+    String id4 = getChangeId(superRepo, c4).get();
+    gApi.changes().id(id1).current().review(ReviewInput.approve());
+    gApi.changes().id(id2).current().review(ReviewInput.approve());
+    gApi.changes().id(id3).current().review(ReviewInput.approve());
+    gApi.changes().id(id4).current().review(ReviewInput.approve());
+
+    gApi.changes().id(id1).current().submit();
+    ObjectId subRepoId = subRepo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/master").getObjectId();
+
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subRepoId);
+  }
+
+  @Test
   public void testUpdateManySubmodules() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> sub1 = createProjectWithPush("sub1");
     TestRepository<?> sub2 = createProjectWithPush("sub2");
     TestRepository<?> sub3 = createProjectWithPush("sub3");
 
+    allowMatchingSubmoduleSubscription("sub1", "refs/heads/master",
+        "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription("sub2", "refs/heads/master",
+        "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription("sub3", "refs/heads/master",
+        "super-project", "refs/heads/master");
+
     Config config = new Config();
     prepareSubmoduleConfigEntry(config, "sub1", "master");
     prepareSubmoduleConfigEntry(config, "sub2", "master");
@@ -117,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();
@@ -129,4 +221,400 @@
         .that(superRepo.getRepository().resolve("origin/master^"))
         .isEqualTo(superPreviousId);
   }
+
+  @Test
+  public void testDoNotUseFastForward() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project", false);
+    TestRepository<?> sub = createProjectWithPush("sub", false);
+
+    allowMatchingSubmoduleSubscription("sub", "refs/heads/master",
+        "super-project", "refs/heads/master");
+
+    createSubmoduleSubscription(superRepo, "master", "sub", "master");
+
+    ObjectId subId =
+        pushChangeTo(sub, "refs/for/master", "some message", "same-topic");
+
+    ObjectId superId =
+        pushChangeTo(superRepo, "refs/for/master", "some message", "same-topic");
+
+    String subChangeId = getChangeId(sub, subId).get();
+    approve(subChangeId);
+    approve(getChangeId(superRepo, superId).get());
+
+    gApi.changes().id(subChangeId).current().submit();
+
+    expectToHaveSubmoduleState(superRepo, "master", "sub", sub, "master");
+    RevCommit superHead = getRemoteHead(name("super-project"), "master");
+    assertThat(superHead.getShortMessage()).contains("some message");
+    assertThat(superHead.getId()).isNotEqualTo(superId);
+  }
+
+  @Test
+  public void testUseFastForwardWhenNoSubmodule() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project", false);
+    TestRepository<?> sub = createProjectWithPush("sub", false);
+
+    ObjectId subId =
+        pushChangeTo(sub, "refs/for/master", "some message", "same-topic");
+
+    ObjectId superId =
+        pushChangeTo(superRepo, "refs/for/master", "some message", "same-topic");
+
+    String subChangeId = getChangeId(sub, subId).get();
+    approve(subChangeId);
+    approve(getChangeId(superRepo, superId).get());
+
+    gApi.changes().id(subChangeId).current().submit();
+
+    RevCommit superHead = getRemoteHead(name("super-project"), "master");
+    assertThat(superHead.getShortMessage()).isEqualTo("some message");
+    assertThat(superHead.getId()).isEqualTo(superId);
+  }
+
+  @Test
+  public void testSameProjectSameBranchDifferentPaths() 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 testSameProjectDifferentBranchDifferentPaths() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> sub = createProjectWithPush("sub");
+
+    allowMatchingSubmoduleSubscription("sub", "refs/heads/master",
+        "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription("sub", "refs/heads/dev",
+        "super-project", "refs/heads/master");
+
+    ObjectId devHead = pushChangeTo(sub, "dev");
+    Config config = new Config();
+    prepareSubmoduleConfigEntry(config, "sub", "sub-master", "master");
+    prepareSubmoduleConfigEntry(config, "sub", "sub-dev", "dev");
+    pushSubmoduleConfig(superRepo, "master", config);
+
+    ObjectId subMasterId =
+        pushChangeTo(sub, "refs/for/master", "some message", "b.txt",
+            "content b", "same-topic");
+
+    sub.reset(devHead);
+    ObjectId subDevId =
+        pushChangeTo(sub, "refs/for/dev", "some message in dev", "b.txt",
+            "content b", "same-topic");
+
+    approve(getChangeId(sub, subMasterId).get());
+    approve(getChangeId(sub, subDevId).get());
+
+    ObjectId superPreviousId = pushChangeTo(superRepo, "master");
+
+    gApi.changes().id(getChangeId(sub, subMasterId).get()).current().submit();
+
+    expectToHaveSubmoduleState(superRepo, "master", "sub-master", sub, "master");
+    expectToHaveSubmoduleState(superRepo, "master", "sub-dev", sub, "dev");
+
+    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();
+  }
+
+  @Test
+  public void testProjectNoSubscriptionWholeTopic() throws Exception {
+    TestRepository<?> repoA = createProjectWithPush("project-a");
+    TestRepository<?> repoB = createProjectWithPush("project-b");
+    // bootstrap the dev branch
+    ObjectId a0 = pushChangeTo(repoA, "dev");
+
+    // bootstrap the dev branch
+    ObjectId b0 = pushChangeTo(repoB, "dev");
+
+    // create a change for master branch in repo a
+    ObjectId aHead =
+        pushChangeTo(repoA, "refs/for/master", "master.txt", "content master A",
+            "some message in a master.txt", "same-topic");
+
+    // create a change for master branch in repo b
+    ObjectId bHead =
+        pushChangeTo(repoB, "refs/for/master", "master.txt", "content master B",
+            "some message in b master.txt", "same-topic");
+
+    // create a change for dev branch in repo a
+    repoA.reset(a0);
+    ObjectId aDevHead =
+        pushChangeTo(repoA, "refs/for/dev", "dev.txt", "content dev A",
+            "some message in a dev.txt", "same-topic");
+
+    // create a change for dev branch in repo b
+    repoB.reset(b0);
+    ObjectId bDevHead =
+        pushChangeTo(repoB, "refs/for/dev", "dev.txt", "content dev B",
+            "some message in b dev.txt", "same-topic");
+
+    approve(getChangeId(repoA, aHead).get());
+    approve(getChangeId(repoB, bHead).get());
+    approve(getChangeId(repoA, aDevHead).get());
+    approve(getChangeId(repoB, bDevHead).get());
+
+    gApi.changes().id(getChangeId(repoA, aDevHead).get()).current().submit();
+    assertThat(
+        getRemoteHead(name("project-a"), "refs/heads/master").getShortMessage())
+        .contains("some message in a master.txt");
+    assertThat(
+        getRemoteHead(name("project-a"), "refs/heads/dev").getShortMessage())
+        .contains("some message in a dev.txt");
+    assertThat(
+        getRemoteHead(name("project-b"), "refs/heads/master").getShortMessage())
+        .contains("some message in b master.txt");
+    assertThat(
+        getRemoteHead(name("project-b"), "refs/heads/dev").getShortMessage())
+        .contains("some message in b dev.txt");
+  }
+
+  @Test
+  public void testTwoProjectsMultipleBranchesWholeTopic() throws Exception {
+    TestRepository<?> repoA = createProjectWithPush("project-a");
+    TestRepository<?> repoB = createProjectWithPush("project-b");
+    // bootstrap the dev branch
+    pushChangeTo(repoA, "dev");
+
+    // bootstrap the dev branch
+    ObjectId b0 = pushChangeTo(repoB, "dev");
+
+    allowMatchingSubmoduleSubscription("project-b",
+        "refs/heads/master", "project-a", "refs/heads/master");
+    allowMatchingSubmoduleSubscription("project-b", "refs/heads/dev",
+        "project-a", "refs/heads/dev");
+
+    createSubmoduleSubscription(repoA, "master", "project-b", "master");
+    createSubmoduleSubscription(repoA, "dev", "project-b", "dev");
+
+
+    // create a change for master branch in repo b
+    ObjectId bHead =
+        pushChangeTo(repoB, "refs/for/master", "master.txt", "content master B",
+            "some message in b master.txt", "same-topic");
+
+    // create a change for dev branch in repo b
+    repoB.reset(b0);
+    ObjectId bDevHead =
+        pushChangeTo(repoB, "refs/for/dev", "dev.txt", "content dev B",
+            "some message in b dev.txt", "same-topic");
+
+    approve(getChangeId(repoB, bHead).get());
+    approve(getChangeId(repoB, bDevHead).get());
+    gApi.changes().id(getChangeId(repoB, bHead).get()).current().submit();
+
+    expectToHaveSubmoduleState(repoA, "master", "project-b", repoB, "master");
+    expectToHaveSubmoduleState(repoA, "dev", "project-b", repoB, "dev");
+  }
 }
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 1ded088..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,15 +15,14 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
-import com.google.common.base.CharMatcher;
-import com.google.common.base.Splitter;
-import com.google.common.collect.Ordering;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+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.Permission;
@@ -31,13 +30,23 @@
 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;
+import com.google.gerrit.server.git.SearchingChangeCacheImpl;
+import com.google.gerrit.server.git.TagCache;
+import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.Util;
+import com.google.gerrit.testutil.DisabledReviewDb;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 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.junit.Before;
@@ -45,12 +54,26 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 @NoHttpd
 public class VisibleRefFilterIT extends AbstractDaemonTest {
   @Inject
   private ChangeEditModifier editModifier;
 
+  @Inject
+  private ProjectControl.GenericFactory projectControlFactory;
+
+  @Inject
+  @Nullable
+  private SearchingChangeCacheImpl changeCache;
+
+  @Inject
+  private TagCache tagCache;
+
+  @Inject
+  private Provider<CurrentUser> userProvider;
+
   private AccountGroup.UUID admins;
 
   private Change.Id c1;
@@ -119,10 +142,11 @@
   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);
     assertRefs(
         "HEAD",
         r1 + "1",
@@ -138,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",
@@ -148,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");
   }
@@ -158,6 +182,7 @@
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
     deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
+    setApiUser(user);
     assertRefs(
         "HEAD",
         r1 + "1",
@@ -171,6 +196,7 @@
     deny(Permission.READ, REGISTERED_USERS, "refs/heads/master");
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
+    setApiUser(user);
     assertRefs(
         r2 + "1",
         r2 + "meta",
@@ -186,16 +212,16 @@
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
     deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
-    Change change1 = db.changes().get(c1);
-    PatchSet ps1 = db.patchSets().get(new PatchSet.Id(c1, 1));
+    Change c = notesFactory.createChecked(db, project, c1).getChange();
+    PatchSet ps1 = getPatchSet(new PatchSet.Id(c1, 1));
 
     // Admin's edit is not visible.
     setApiUser(admin);
-    editModifier.createEdit(change1, ps1);
+    editModifier.createEdit(c, ps1);
 
     // User's edit is visible.
     setApiUser(user);
-    editModifier.createEdit(change1, ps1);
+    editModifier.createEdit(c, ps1);
 
     assertRefs(
         "HEAD",
@@ -213,12 +239,11 @@
       deny(Permission.READ, REGISTERED_USERS, "refs/heads/master");
       allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
-      Change change1 = db.changes().get(c1);
-      PatchSet ps1 = db.patchSets().get(new PatchSet.Id(c1, 1));
+      Change c = notesFactory.createChecked(db, project, c1).getChange();
+      PatchSet ps1 = getPatchSet(new PatchSet.Id(c1, 1));
       setApiUser(admin);
-      editModifier.createEdit(change1, ps1);
+      editModifier.createEdit(c, ps1);
       setApiUser(user);
-      editModifier.createEdit(change1, ps1);
 
       assertRefs(
           // Change 1 is visible due to accessDatabase capability, even though
@@ -232,37 +257,149 @@
           // 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);
     }
   }
 
+  @Test
+  public void draftRefs() throws Exception {
+    allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
+
+    PushOneCommit.Result br = pushFactory.create(db, admin.getIdent(), testRepo)
+        .to("refs/drafts/master");
+    br.assertOkStatus();
+    Change.Id c3 = br.getChange().getId();
+    String r3 = changeRefPrefix(c3);
+
+    // Only admin can see admin's draft change.
+    setApiUser(admin);
+    assertRefs(
+        "HEAD",
+        r1 + "1",
+        r1 + "meta",
+        r2 + "1",
+        r2 + "meta",
+        r3 + "1",
+        r3 + "meta",
+        "refs/heads/branch",
+        "refs/heads/master",
+        RefNames.REFS_CONFIG,
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag");
+
+    // user can't.
+    setApiUser(user);
+    assertRefs(
+        "HEAD",
+        r1 + "1",
+        r1 + "meta",
+        r2 + "1",
+        r2 + "meta",
+        "refs/heads/branch",
+        "refs/heads/master",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag");
+  }
+
+  @Test
+  public void noSearchingChangeCacheImpl() throws Exception {
+    allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
+
+    setApiUser(user);
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertRefs(
+          repo,
+          new VisibleRefFilter(tagCache, notesFactory, null, repo,
+              projectControl(), db, true),
+          // Can't use stored values from the index so DB must be enabled.
+          false,
+          "HEAD",
+          r1 + "1",
+          r1 + "meta",
+          r2 + "1",
+          r2 + "meta",
+          "refs/heads/branch",
+          "refs/heads/master",
+          "refs/tags/branch-tag",
+          "refs/tags/master-tag");
+    }
+  }
+
+  @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.
    *
-   * @param expected expected refs, in order. If notedb is disabled by the
-   *     configuration, any notedb refs (i.e. ending in "/meta") are removed
+   * @param expectedWithMeta expected refs, in order. If NoteDb is disabled by
+   *     the configuration, any NoteDb refs (i.e. ending in "/meta") are removed
    *     from the expected list before comparing to the actual results.
    * @throws Exception
    */
-  private void assertRefs(String... expected) throws Exception {
-    String out = sshSession.exec(String.format(
-        "gerrit ls-user-refs -p %s -u %s",
-        project.get(), user.getId().get()));
-    assert_().withFailureMessage(sshSession.getError())
-      .that(sshSession.hasError()).isFalse();
+  private void assertRefs(String... expectedWithMeta) throws Exception {
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertRefs(
+          repo,
+          new VisibleRefFilter(tagCache, notesFactory, changeCache, repo,
+              projectControl(), new DisabledReviewDb(), true),
+          true,
+          expectedWithMeta);
+    }
+  }
 
-    List<String> filtered = new ArrayList<>(expected.length);
-    for (String r : expected) {
+  private void assertRefs(Repository repo, VisibleRefFilter filter,
+      boolean disableDb, String... expectedWithMeta) throws Exception {
+    List<String> expected = new ArrayList<>(expectedWithMeta.length);
+    for (String r : expectedWithMeta) {
       if (notesMigration.writeChanges() || !r.endsWith(RefNames.META_SUFFIX)) {
-        filtered.add(r);
+        expected.add(r);
       }
     }
 
-    Splitter s = Splitter.on(CharMatcher.whitespace()).omitEmptyStrings();
-    assertThat(filtered).containsExactlyElementsIn(
-        Ordering.natural().sortedCopy(s.split(out))).inOrder();
+    AcceptanceTestRequestScope.Context ctx = null;
+    if (disableDb) {
+      ctx = disableDb();
+    }
+    try {
+      Map<String, Ref> all = repo.getAllRefs();
+      assertThat(filter.filter(all, false).keySet())
+          .containsExactlyElementsIn(expected);
+    } finally {
+      if (disableDb) {
+        enableDb(ctx);
+      }
+    }
+  }
+
+  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/pgm/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD
new file mode 100644
index 0000000..806acd2
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD
@@ -0,0 +1,8 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'pgm',
+  srcs = glob(['*IT.java']),
+  source_under_test = ['//gerrit-pgm:pgm'],
+  labels = ['pgm'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNoteDbIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNoteDbIT.java
new file mode 100644
index 0000000..66e0c73
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNoteDbIT.java
@@ -0,0 +1,64 @@
+// 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.pgm;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.io.Files;
+import com.google.gerrit.launcher.GerritLauncher;
+import com.google.gerrit.server.notedb.ConfigNotesMigration;
+import com.google.gerrit.testutil.TempFileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+
+public class RebuildNoteDbIT {
+  private File sitePath;
+
+  @Before
+  public void createTempDirectory() throws Exception {
+    sitePath = TempFileUtil.createTempDirectory();
+  }
+
+  @After
+  public void destroySite() throws Exception {
+    if (sitePath != null) {
+      TempFileUtil.cleanup();
+    }
+  }
+
+  @Test
+  public void rebuildEmptySite() throws Exception {
+    initSite();
+    Files.append(ConfigNotesMigration.allEnabledConfig().toText(),
+        new File(sitePath.toString(), "etc/gerrit.config"),
+        UTF_8);
+    runGerrit("RebuildNoteDb", "-d", sitePath.toString(),
+        "--show-stack-trace");
+  }
+
+  private void initSite() throws Exception {
+    runGerrit("init", "-d", sitePath.getPath(),
+        "--batch", "--no-auto-start", "--skip-plugins", "--show-stack-trace");
+  }
+
+  private static void runGerrit(String... args) throws Exception {
+    assertThat(GerritLauncher.mainImpl(args)).isEqualTo(0);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java
deleted file mode 100644
index 5d0e7df..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java
+++ /dev/null
@@ -1,64 +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.pgm;
-
-import static com.google.common.truth.Truth.assertThat;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.io.Files;
-import com.google.gerrit.launcher.GerritLauncher;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.testutil.TempFileUtil;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.File;
-
-public class RebuildNotedbIT {
-  private File sitePath;
-
-  @Before
-  public void createTempDirectory() throws Exception {
-    sitePath = TempFileUtil.createTempDirectory();
-  }
-
-  @After
-  public void destroySite() throws Exception {
-    if (sitePath != null) {
-      TempFileUtil.cleanup();
-    }
-  }
-
-  @Test
-  public void rebuildEmptySite() throws Exception {
-    initSite();
-    Files.append(NotesMigration.allEnabledConfig().toText(),
-        new File(sitePath.toString(), "etc/gerrit.config"),
-        UTF_8);
-    runGerrit("RebuildNotedb", "-d", sitePath.toString(),
-        "--show-stack-trace");
-  }
-
-  private void initSite() throws Exception {
-    runGerrit("init", "-d", sitePath.getPath(),
-        "--batch", "--no-auto-start", "--skip-plugins", "--show-stack-trace");
-  }
-
-  private static void runGerrit(String... args) throws Exception {
-    assertThat(GerritLauncher.mainImpl(args)).isEqualTo(0);
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
index b7c1819..76c918b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
@@ -1,10 +1,10 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  group = 'rest-account',
+  group = 'rest_account',
   srcs = glob(['*IT.java']),
   deps = [':util'],
-  labels = ['rest']
+  labels = ['rest'],
 )
 
 java_library(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUILD
new file mode 100644
index 0000000..558d0a9
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUILD
@@ -0,0 +1,23 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'rest_account',
+  srcs = glob(['*IT.java']),
+  deps = [':util'],
+  labels = ['rest']
+)
+
+java_library(
+  name = 'util',
+  srcs = [
+    'AccountAssert.java',
+    'CapabilityInfo.java',
+  ],
+  deps = [
+    '//gerrit-acceptance-tests:lib',
+    '//gerrit-reviewdb:server',
+    '//lib:gwtorm',
+    '//lib:junit',
+  ],
+  visibility = ['//visibility:public'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
index 3e7c2bf..ce82270 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
@@ -34,7 +34,6 @@
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 public class CapabilitiesIT extends AbstractDaemonTest {
@@ -52,8 +51,8 @@
     allowGlobalCapabilities(REGISTERED_USERS, all);
     try {
       RestResponse r =
-          userSession.get("/accounts/self/capabilities");
-      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+          userRestSession.get("/accounts/self/capabilities");
+      r.assertOK();
       CapabilityInfo info = (new Gson()).fromJson(r.getReader(),
           new TypeToken<CapabilityInfo>() {}.getType());
       for (String c : GlobalCapability.getAllNames()) {
@@ -80,8 +79,8 @@
   @Test
   public void testCapabilitiesAdmin() throws Exception {
     RestResponse r =
-        adminSession.get("/accounts/self/capabilities");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+        adminRestSession.get("/accounts/self/capabilities");
+    r.assertOK();
     CapabilityInfo info = (new Gson()).fromJson(r.getReader(),
         new TypeToken<CapabilityInfo>() {}.getType());
     for (String c : GlobalCapability.getAllNames()) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java
deleted file mode 100644
index 9dbb1ff..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java
+++ /dev/null
@@ -1,160 +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.acceptance.rest.account;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertEquals;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.extensions.client.Theme;
-
-import org.apache.http.HttpStatus;
-import org.junit.Test;
-
-public class DiffPreferencesIT extends AbstractDaemonTest {
-  @Test
-  public void getDiffPreferencesOfNonExistingAccount_NotFound()
-      throws Exception {
-    assertEquals(HttpStatus.SC_NOT_FOUND,
-        adminSession.get("/accounts/non-existing/preferences.diff")
-        .getStatusCode());
-  }
-
-  @Test
-  public void getDiffPreferences() throws Exception {
-    RestResponse r = adminSession.get("/accounts/" + admin.email
-        + "/preferences.diff");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    DiffPreferencesInfo d = DiffPreferencesInfo.defaults();
-    DiffPreferencesInfo o =
-        newGson().fromJson(r.getReader(), DiffPreferencesInfo.class);
-
-    assertThat(o.context).isEqualTo(d.context);
-    assertThat(o.tabSize).isEqualTo(d.tabSize);
-    assertThat(o.lineLength).isEqualTo(d.lineLength);
-    assertThat(o.cursorBlinkRate).isEqualTo(d.cursorBlinkRate);
-    assertThat(o.expandAllComments).isNull();
-    assertThat(o.intralineDifference).isEqualTo(d.intralineDifference);
-    assertThat(o.manualReview).isNull();
-    assertThat(o.retainHeader).isNull();
-    assertThat(o.showLineEndings).isEqualTo(d.showLineEndings);
-    assertThat(o.showTabs).isEqualTo(d.showTabs);
-    assertThat(o.showWhitespaceErrors).isEqualTo(d.showWhitespaceErrors);
-    assertThat(o.skipDeleted).isNull();
-    assertThat(o.skipUncommented).isNull();
-    assertThat(o.syntaxHighlighting).isEqualTo(d.syntaxHighlighting);
-    assertThat(o.hideTopMenu).isNull();
-    assertThat(o.autoHideDiffTableHeader).isEqualTo(d.autoHideDiffTableHeader);
-    assertThat(o.hideLineNumbers).isNull();
-    assertThat(o.renderEntireFile).isNull();
-    assertThat(o.hideEmptyPane).isNull();
-    assertThat(o.matchBrackets).isNull();
-    assertThat(o.lineWrapping).isNull();
-    assertThat(o.ignoreWhitespace).isEqualTo(d.ignoreWhitespace);
-    assertThat(o.theme).isEqualTo(d.theme);
-  }
-
-  @Test
-  public void setDiffPreferences() throws Exception {
-    DiffPreferencesInfo i = DiffPreferencesInfo.defaults();
-
-    // change all default values
-    i.context *= -1;
-    i.tabSize *= -1;
-    i.lineLength *= -1;
-    i.cursorBlinkRate = 500;
-    i.theme = Theme.MIDNIGHT;
-    i.ignoreWhitespace = Whitespace.IGNORE_ALL;
-    i.expandAllComments ^= true;
-    i.intralineDifference ^= true;
-    i.manualReview ^= true;
-    i.retainHeader ^= true;
-    i.showLineEndings ^= true;
-    i.showTabs ^= true;
-    i.showWhitespaceErrors ^= true;
-    i.skipDeleted ^= true;
-    i.skipUncommented ^= true;
-    i.syntaxHighlighting ^= true;
-    i.hideTopMenu ^= true;
-    i.autoHideDiffTableHeader ^= true;
-    i.hideLineNumbers ^= true;
-    i.renderEntireFile ^= true;
-    i.hideEmptyPane ^= true;
-    i.matchBrackets ^= true;
-    i.lineWrapping ^= true;
-
-    RestResponse r = adminSession.put("/accounts/" + admin.email
-        + "/preferences.diff", i);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    DiffPreferencesInfo o = newGson().fromJson(r.getReader(),
-        DiffPreferencesInfo.class);
-
-    assertThat(o.context).isEqualTo(i.context);
-    assertThat(o.tabSize).isEqualTo(i.tabSize);
-    assertThat(o.lineLength).isEqualTo(i.lineLength);
-    assertThat(o.cursorBlinkRate).isEqualTo(i.cursorBlinkRate);
-    assertThat(o.expandAllComments).isEqualTo(i.expandAllComments);
-    assertThat(o.intralineDifference).isNull();
-    assertThat(o.manualReview).isEqualTo(i.manualReview);
-    assertThat(o.retainHeader).isEqualTo(i.retainHeader);
-    assertThat(o.showLineEndings).isNull();
-    assertThat(o.showTabs).isNull();
-    assertThat(o.showWhitespaceErrors).isNull();
-    assertThat(o.skipDeleted).isEqualTo(i.skipDeleted);
-    assertThat(o.skipUncommented).isEqualTo(i.skipUncommented);
-    assertThat(o.syntaxHighlighting).isNull();
-    assertThat(o.hideTopMenu).isEqualTo(i.hideTopMenu);
-    assertThat(o.autoHideDiffTableHeader).isNull();
-    assertThat(o.hideLineNumbers).isEqualTo(i.hideLineNumbers);
-    assertThat(o.renderEntireFile).isEqualTo(i.renderEntireFile);
-    assertThat(o.hideEmptyPane).isEqualTo(i.hideEmptyPane);
-    assertThat(o.matchBrackets).isEqualTo(i.matchBrackets);
-    assertThat(o.lineWrapping).isEqualTo(i.lineWrapping);
-    assertThat(o.ignoreWhitespace).isEqualTo(i.ignoreWhitespace);
-    assertThat(o.theme).isEqualTo(i.theme);
-
-    // Partially fill input record
-    i = new DiffPreferencesInfo();
-    i.tabSize = 42;
-    r = adminSession.put("/accounts/" + admin.email
-        + "/preferences.diff", i);
-    DiffPreferencesInfo a = newGson().fromJson(r.getReader(),
-        DiffPreferencesInfo.class);
-
-    assertThat(a.context).isEqualTo(o.context);
-    assertThat(a.tabSize).isEqualTo(42);
-    assertThat(a.lineLength).isEqualTo(o.lineLength);
-    assertThat(a.expandAllComments).isEqualTo(o.expandAllComments);
-    assertThat(a.intralineDifference).isNull();
-    assertThat(a.manualReview).isEqualTo(o.manualReview);
-    assertThat(a.retainHeader).isEqualTo(o.retainHeader);
-    assertThat(a.showLineEndings).isNull();
-    assertThat(a.showTabs).isNull();
-    assertThat(a.showWhitespaceErrors).isNull();
-    assertThat(a.skipDeleted).isEqualTo(o.skipDeleted);
-    assertThat(a.skipUncommented).isEqualTo(o.skipUncommented);
-    assertThat(a.syntaxHighlighting).isNull();
-    assertThat(a.hideTopMenu).isEqualTo(o.hideTopMenu);
-    assertThat(a.autoHideDiffTableHeader).isNull();
-    assertThat(a.hideLineNumbers).isEqualTo(o.hideLineNumbers);
-    assertThat(a.renderEntireFile).isEqualTo(o.renderEntireFile);
-    assertThat(a.hideEmptyPane).isEqualTo(o.hideEmptyPane);
-    assertThat(a.ignoreWhitespace).isEqualTo(o.ignoreWhitespace);
-    assertThat(a.theme).isEqualTo(o.theme);
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
deleted file mode 100644
index b97219c..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
+++ /dev/null
@@ -1,110 +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.rest.account;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.extensions.client.EditPreferencesInfo;
-import com.google.gerrit.extensions.client.KeyMapType;
-import com.google.gerrit.extensions.client.Theme;
-
-import org.apache.http.HttpStatus;
-import org.junit.Test;
-
-import java.io.IOException;
-
-public class EditPreferencesIT extends AbstractDaemonTest {
-  @Test
-  public void getSetEditPreferences() throws Exception {
-    String endPoint = "/accounts/" + admin.email + "/preferences.edit";
-    RestResponse r = adminSession.get(endPoint);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    EditPreferencesInfo out = getEditPrefInfo(r);
-
-    assertThat(out.lineLength).isEqualTo(100);
-    assertThat(out.tabSize).isEqualTo(8);
-    assertThat(out.cursorBlinkRate).isEqualTo(0);
-    assertThat(out.hideTopMenu).isNull();
-    assertThat(out.showTabs).isTrue();
-    assertThat(out.showWhitespaceErrors).isNull();
-    assertThat(out.syntaxHighlighting).isTrue();
-    assertThat(out.hideLineNumbers).isNull();
-    assertThat(out.matchBrackets).isTrue();
-    assertThat(out.lineWrapping).isNull();
-    assertThat(out.autoCloseBrackets).isNull();
-    assertThat(out.theme).isEqualTo(Theme.DEFAULT);
-    assertThat(out.keyMapType).isEqualTo(KeyMapType.DEFAULT);
-
-    // change some default values
-    out.lineLength = 80;
-    out.tabSize = 4;
-    out.cursorBlinkRate = 500;
-    out.hideTopMenu = true;
-    out.showTabs = false;
-    out.showWhitespaceErrors = true;
-    out.syntaxHighlighting = false;
-    out.hideLineNumbers = true;
-    out.matchBrackets = false;
-    out.lineWrapping = true;
-    out.autoCloseBrackets = true;
-    out.theme = Theme.TWILIGHT;
-    out.keyMapType = KeyMapType.EMACS;
-
-    r = adminSession.put(endPoint, out);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-
-    r = adminSession.get(endPoint);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    EditPreferencesInfo info = getEditPrefInfo(r);
-    assertEditPreferences(info, out);
-
-    // Partially filled input record
-    EditPreferencesInfo in = new EditPreferencesInfo();
-    in.tabSize = 42;
-    r = adminSession.put(endPoint, in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-
-    r = adminSession.get(endPoint);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    info = getEditPrefInfo(r);
-    out.tabSize = in.tabSize;
-    assertEditPreferences(info, out);
-  }
-
-  private EditPreferencesInfo getEditPrefInfo(RestResponse r)
-      throws IOException {
-    return newGson().fromJson(r.getReader(),
-        EditPreferencesInfo.class);
-  }
-
-  private void assertEditPreferences(EditPreferencesInfo out,
-      EditPreferencesInfo in) {
-    assertThat(out.lineLength).isEqualTo(in.lineLength);
-    assertThat(out.tabSize).isEqualTo(in.tabSize);
-    assertThat(out.cursorBlinkRate).isEqualTo(in.cursorBlinkRate);
-    assertThat(out.hideTopMenu).isEqualTo(in.hideTopMenu);
-    assertThat(out.showTabs).isNull();
-    assertThat(out.showWhitespaceErrors).isEqualTo(in.showWhitespaceErrors);
-    assertThat(out.syntaxHighlighting).isNull();
-    assertThat(out.hideLineNumbers).isEqualTo(in.hideLineNumbers);
-    assertThat(out.matchBrackets).isNull();
-    assertThat(out.lineWrapping).isEqualTo(in.lineWrapping);
-    assertThat(out.autoCloseBrackets).isEqualTo(in.autoCloseBrackets);
-    assertThat(out.theme).isEqualTo(in.theme);
-    assertThat(out.keyMapType).isEqualTo(in.keyMapType);
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
index da1d3ec..f48f9fa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
@@ -27,7 +27,7 @@
 public class GetAccountDetailIT extends AbstractDaemonTest {
   @Test
   public void getDetail() throws Exception {
-    RestResponse r = adminSession.get("/accounts/" + admin.username + "/detail/");
+    RestResponse r = adminRestSession.get("/accounts/" + admin.username + "/detail/");
     AccountDetailInfo info = newGson().fromJson(r.getReader(), AccountDetailInfo.class);
     assertAccountInfo(admin, info);
     Account account = accountCache.get(admin.getId()).getAccount();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
index 2543095..3297c60 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
@@ -22,11 +22,9 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.PutUsername;
-import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 import java.util.Collections;
@@ -40,8 +38,8 @@
     PutUsername.Input in = new PutUsername.Input();
     in.username = "myUsername";
     RestResponse r =
-        adminSession.put("/accounts/" + createUser().get() + "/username", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+        adminRestSession.put("/accounts/" + createUser().get() + "/username", in);
+    r.assertOK();
     assertThat(newGson().fromJson(r.getReader(), String.class)).isEqualTo(
         in.username);
   }
@@ -50,28 +48,28 @@
   public void setExisting_Conflict() throws Exception {
     PutUsername.Input in = new PutUsername.Input();
     in.username = admin.username;
-    RestResponse r =
-        adminSession.put("/accounts/" + createUser().get() + "/username", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+    adminRestSession
+        .put("/accounts/" + createUser().get() + "/username", in)
+        .assertConflict();
   }
 
   @Test
   public void setNew_MethodNotAllowed() throws Exception {
     PutUsername.Input in = new PutUsername.Input();
     in.username = "newUsername";
-    RestResponse r =
-        adminSession.put("/accounts/" + admin.username + "/username", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_METHOD_NOT_ALLOWED);
+    adminRestSession
+        .put("/accounts/" + admin.username + "/username", in)
+        .assertMethodNotAllowed();
   }
 
   @Test
   public void delete_MethodNotAllowed() throws Exception {
-    RestResponse r =
-        adminSession.put("/accounts/" + admin.username + "/username");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_METHOD_NOT_ALLOWED);
+    adminRestSession
+        .put("/accounts/" + admin.username + "/username")
+        .assertMethodNotAllowed();
   }
 
-  private Account.Id createUser() throws OrmException {
+  private Account.Id createUser() throws Exception {
     try (ReviewDb db = reviewDbProvider.open()) {
       Account.Id id = new Account.Id(db.nextAccountId());
       Account a = new Account(id, TimeUtil.nowTs());
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
new file mode 100644
index 0000000..32cfc9b
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
@@ -0,0 +1,247 @@
+// 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.account;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class WatchedProjectsIT extends AbstractDaemonTest {
+
+  private static final String NEW_PROJECT_NAME = "newProjectAccess";
+
+  @Test
+  public void setAndGetWatchedProjects() throws Exception {
+    String projectName1 = createProject(NEW_PROJECT_NAME).get();
+    String projectName2 = createProject(NEW_PROJECT_NAME + "2").get();
+
+    List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(2);
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName1;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    pwi = new ProjectWatchInfo();
+    pwi.project = projectName2;
+    pwi.filter = "branch:master";
+    pwi.notifySubmittedChanges = true;
+    pwi.notifyNewPatchSets = true;
+    projectsToWatch.add(pwi);
+
+    List<ProjectWatchInfo> persistedWatchedProjects =
+        gApi.accounts().self().setWatchedProjects(projectsToWatch);
+    assertThat(persistedWatchedProjects)
+        .containsAllIn(projectsToWatch).inOrder();
+  }
+
+  @Test
+  public void setAndDeleteWatchedProjects() throws Exception {
+    String projectName1 = createProject(NEW_PROJECT_NAME).get();
+    String projectName2 = createProject(NEW_PROJECT_NAME + "2").get();
+
+    List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName1;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    pwi = new ProjectWatchInfo();
+    pwi.project = projectName2;
+    pwi.filter = "branch:master";
+    pwi.notifySubmittedChanges = true;
+    pwi.notifyNewPatchSets = true;
+    projectsToWatch.add(pwi);
+
+    // Persist watched projects
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    List<ProjectWatchInfo> d = Lists.newArrayList(pwi);
+    gApi.accounts().self().deleteWatchedProjects(d);
+    projectsToWatch.remove(pwi);
+
+    List<ProjectWatchInfo> persistedWatchedProjects =
+        gApi.accounts().self().getWatchedProjects();
+
+    assertThat(persistedWatchedProjects).doesNotContain(pwi);
+    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
+  }
+
+  @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";
+
+    List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(2);
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    exception.expect(UnprocessableEntityException.class);
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+  }
+
+  @Test
+  public void deleteNonExistingProjectWatch() throws Exception {
+    String projectName = project.get();
+
+    // Let another user watch a project
+    setApiUser(admin);
+    List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    // Try to delete a watched project using a different user
+    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);
+    gApi.accounts().self().deleteWatchedProjects(d);
+  }
+
+  @Test
+  public void modifyProjectWatchUsingOmittedValues() throws Exception {
+    String projectName = project.get();
+
+    // Let another user watch a project
+    setApiUser(admin);
+    List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    // Persist a defined state
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    // Omit previously set value - will set it to false on the server
+    // The response will not carry this field then as we omit sending
+    // false values in JSON
+    pwi.notifyNewChanges = null;
+
+    // Perform update
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    List<ProjectWatchInfo> watchedProjects =
+        gApi.accounts().self().getWatchedProjects();
+
+    assertThat(watchedProjects).containsAllIn(projectsToWatch);
+  }
+
+  @Test
+  public void setAndDeleteWatchedProjectsWithDifferentFilter()
+      throws Exception {
+    String projectName = project.get();
+
+    List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.filter = "branch:stable";
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.filter = "branch:master";
+    pwi.notifySubmittedChanges = true;
+    pwi.notifyNewPatchSets = true;
+    projectsToWatch.add(pwi);
+
+    // Persist watched projects
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    List<ProjectWatchInfo> d = Lists.newArrayList(pwi);
+    gApi.accounts().self().deleteWatchedProjects(d);
+    projectsToWatch.remove(pwi);
+
+    List<ProjectWatchInfo> persistedWatchedProjects =
+        gApi.accounts().self().getWatchedProjects();
+
+    assertThat(persistedWatchedProjects).doesNotContain(pwi);
+    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index e7950a4..c0e6306 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -14,31 +14,25 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.fail;
 
 import com.google.common.base.Function;
-import com.google.common.base.Strings;
-import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
 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.Sandboxed;
+import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
-import com.google.gerrit.common.EventListener;
-import com.google.gerrit.common.EventSource;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
-import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -46,28 +40,27 @@
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.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.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.data.RefUpdateAttribute;
-import com.google.gerrit.server.events.ChangeMergedEvent;
-import com.google.gerrit.server.events.Event;
-import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.testutil.ConfigSuite;
-import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.testutil.TestTimeUtil;
 import com.google.inject.Inject;
 
-import org.apache.http.HttpStatus;
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -82,54 +75,33 @@
 import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 
-@Sandboxed
+@NoHttpd
 public abstract class AbstractSubmit extends AbstractDaemonTest {
   @ConfigSuite.Config
   public static Config submitWholeTopicEnabled() {
     return submitWholeTopicEnabledConfig();
   }
 
-  private Map<String, String> mergeResults;
-  protected Multimap<String, RefUpdateAttribute> refUpdatedEvents;
-
-  @Inject
-  private ChangeNotes.Factory notesFactory;
-
   @Inject
   private ApprovalsUtil approvalsUtil;
 
   @Inject
-  private IdentifiedUser.GenericFactory factory;
+  private Submit submitHandler;
 
-  @Inject
-  EventSource source;
+  private String systemTimeZone;
 
   @Before
-  public void setUp() throws Exception {
-    mergeResults = Maps.newHashMap();
-    refUpdatedEvents = HashMultimap.create();
-    CurrentUser listenerUser = factory.create(user.id);
-    source.addEventListener(new EventListener() {
+  public void setTimeForTesting() {
+    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
+  }
 
-      @Override
-      public void onEvent(Event event) {
-        if (event instanceof ChangeMergedEvent) {
-          ChangeMergedEvent changeMergedEvent = (ChangeMergedEvent) event;
-          mergeResults.put(changeMergedEvent.change.number,
-              changeMergedEvent.newRev);
-        } else if (event instanceof RefUpdatedEvent) {
-          RefUpdatedEvent e = (RefUpdatedEvent) event;
-          RefUpdateAttribute r = e.refUpdate;
-          refUpdatedEvents.put(r.project + "-" + r.refName, r);
-        }
-      }
-
-    }, listenerUser);
+  @After
+  public void resetTime() {
+    TestTimeUtil.useSystemTime();
+    System.setProperty("user.timezone", systemTimeZone);
   }
 
   @After
@@ -144,7 +116,7 @@
   public void submitToEmptyRepo() throws Exception {
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    assertThat(getRemoteHead().getId()).isEqualTo(change.getCommitId());
+    assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
   }
 
   @Test
@@ -241,10 +213,11 @@
     approve(change2.getChangeId());
     approve(change3.getChangeId());
     submit(change3.getChangeId());
+    String expectedTopic = name(topic);
+    change1.assertChange(Change.Status.MERGED, expectedTopic, admin);
+    change2.assertChange(Change.Status.MERGED, expectedTopic, admin);
+    change3.assertChange(Change.Status.MERGED, expectedTopic, admin);
 
-    change1.assertChange(Change.Status.MERGED, topic, admin);
-    change2.assertChange(Change.Status.MERGED, topic, admin);
-    change3.assertChange(Change.Status.MERGED, topic, admin);
     // Check for the exact change to have the correct submitter.
     assertSubmitter(change3);
     // Also check submitters for changes submitted via the topic relationship.
@@ -269,11 +242,49 @@
         "Initial empty repository", "Change 1", "Change 2", "Change 3");
     if (getSubmitType() == SubmitType.MERGE_ALWAYS) {
       assertThat(commitsInRepo).contains(
-          "Merge changes from topic '" + topic + "'");
+          "Merge changes from topic '" + expectedTopic + "'");
     }
   }
 
   @Test
+  public void submitDraftChange() throws Exception {
+    PushOneCommit.Result draft = createDraftChange();
+    Change.Id num = draft.getChange().getId();
+    submitWithConflict(draft.getChangeId(),
+        "Failed to submit 1 change due to the following problems:\n"
+        + "Change " + num + ": Change " + num + " is draft");
+  }
+
+  @Test
+  public void submitDraftPatchSet() throws Exception {
+    PushOneCommit.Result change = createChange();
+    PushOneCommit.Result draft = amendChangeAsDraft(change.getChangeId());
+    Change.Id num = draft.getChange().getId();
+
+    submitWithConflict(draft.getChangeId(),
+        "Failed to submit 1 change due to the following problems:\n"
+        + "Change " + num + ": submit rule error: "
+        + "Cannot submit draft patch sets");
+  }
+
+  @Test
+  public void submitWithHiddenBranchInSameTopic() throws Exception {
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+    PushOneCommit.Result visible =
+        createChange("refs/for/master/" + name("topic"));
+    Change.Id num = visible.getChange().getId();
+
+    createBranch(new Branch.NameKey(project, "hidden"));
+    PushOneCommit.Result hidden =
+        createChange("refs/for/hidden/" + name("topic"));
+    approve(hidden.getChangeId());
+    blockRead("refs/heads/hidden");
+
+    submit(visible.getChangeId(), new SubmitInput(), AuthException.class,
+        "A change to be submitted with " + num + " is not visible");
+  }
+
+  @Test
   public void submitChangeWhenParentOfOtherBranchTip() throws Exception {
     // Chain of two commits
     // Push both to topic-branch
@@ -378,12 +389,20 @@
   private void assertSubmitter(PushOneCommit.Result change) throws Exception {
     ChangeInfo info = get(change.getChangeId(), ListChangesOption.MESSAGES);
     assertThat(info.messages).isNotNull();
-    assertThat(info.messages).hasSize(3);
+    Iterable<String> messages = Iterables.transform(info.messages,
+        new Function<ChangeMessageInfo, String>() {
+          @Override
+          public String apply(ChangeMessageInfo in) {
+            return in.message;
+          }
+        });
+    assertThat(messages).hasSize(3);
+    String last = Iterables.getLast(messages);
     if (getSubmitType() == SubmitType.CHERRY_PICK) {
-      assertThat(Iterables.getLast(info.messages).message).startsWith(
+      assertThat(last).startsWith(
           "Change has been successfully cherry-picked as ");
     } else {
-      assertThat(Iterables.getLast(info.messages).message).isEqualTo(
+      assertThat(last).isEqualTo(
           "Change has been successfully merged by Administrator");
     }
   }
@@ -396,77 +415,72 @@
     }
   }
 
-  protected PushOneCommit.Result createChange(String subject,
-      String fileName, String content) throws Exception {
-    PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, subject, fileName, content);
-    return push.to("refs/for/master");
-  }
-
-  protected PushOneCommit.Result createChange(String subject,
-      String fileName, String content, String topic)
-          throws Exception {
-    PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, subject, fileName, content);
-    return push.to("refs/for/master/" + topic);
-  }
-
-  protected PushOneCommit.Result createChange(TestRepository<?> repo,
-      String branch, String subject, String fileName, String content,
-      String topic) throws Exception {
-    PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), repo, subject, fileName, content);
-    return push.to("refs/for/" + branch + "/" + name(topic));
-  }
-
   protected void submit(String changeId) throws Exception {
-    submit(changeId, HttpStatus.SC_OK, null);
+    submit(changeId, new SubmitInput(), null, null);
+  }
+
+  protected void submit(String changeId, SubmitInput input) throws Exception {
+    submit(changeId, input, null, null);
   }
 
   protected void submitWithConflict(String changeId,
       String expectedError) throws Exception {
-    submit(changeId, HttpStatus.SC_CONFLICT, expectedError);
+    submit(changeId, new SubmitInput(), ResourceConflictException.class,
+        expectedError);
   }
 
-  private void submit(String changeId, int expectedStatus, String msg)
-      throws Exception {
+  protected void submit(String changeId, SubmitInput input,
+      Class<? extends RestApiException> expectedExceptionType,
+      String expectedExceptionMsg) throws Exception {
     approve(changeId);
-    SubmitInput subm = new SubmitInput();
-    RestResponse r =
-        adminSession.post("/changes/" + changeId + "/submit", subm);
-    assertThat(r.getStatusCode()).isEqualTo(expectedStatus);
-    if (expectedStatus == HttpStatus.SC_OK) {
-      checkArgument(msg == null, "msg must be null for successful submits");
-      ChangeInfo change =
-          newGson().fromJson(r.getReader(),
-              new TypeToken<ChangeInfo>() {}.getType());
-      assertThat(change.status).isEqualTo(ChangeStatus.MERGED);
-
-      checkMergeResult(change);
-    } else {
-      checkArgument(!Strings.isNullOrEmpty(msg), "msg must be a valid string " +
-          "containing an error message for unsuccessful submits");
-      assertThat(r.getEntityContent()).isEqualTo(msg);
+    if (expectedExceptionType == null) {
+      assertSubmittable(changeId);
     }
-    r.consume();
+    try {
+      gApi.changes().id(changeId).current().submit(input);
+      if (expectedExceptionType != null) {
+        fail("Expected exception of type "
+            + expectedExceptionType.getSimpleName());
+      }
+    } catch (RestApiException e) {
+      if (expectedExceptionType == null) {
+        throw e;
+      }
+      // More verbose than using assertThat and/or ExpectedException, but gives
+      // us the stack trace.
+      if (!expectedExceptionType.isAssignableFrom(e.getClass())
+          || !e.getMessage().equals(expectedExceptionMsg)) {
+        throw new AssertionError("Expected exception of type "
+            + expectedExceptionType.getSimpleName() + " with message: \""
+            + expectedExceptionMsg + "\" but got exception of type "
+            + e.getClass().getSimpleName() + " with message \""
+            + e.getMessage() + "\"", e);
+      }
+      return;
+    }
+    ChangeInfo change = gApi.changes().id(changeId).info();
+    assertMerged(change.changeId);
   }
 
-  private void checkMergeResult(ChangeInfo change) throws IOException {
-    // Get the revision of the branch after the submit to compare with the
-    // newRev of the ChangeMergedEvent.
-    RestResponse b =
-        adminSession.get("/projects/" + change.project + "/branches/"
-            + change.branch);
-    if (b.getStatusCode() == HttpStatus.SC_OK) {
-      BranchInfo branch =
-          newGson().fromJson(b.getReader(),
-              new TypeToken<BranchInfo>() {}.getType());
-      assertThat(mergeResults).isNotEmpty();
-      String newRev = mergeResults.get(Integer.toString(change._number));
-      assertThat(newRev).isNotNull();
-      assertThat(branch.revision).isEqualTo(newRev);
-    }
-    b.consume();
+  protected void assertSubmittable(String changeId) throws Exception {
+    assertThat(gApi.changes().id(changeId).info().submittable)
+        .named("submit bit on ChangeInfo")
+        .isEqualTo(true);
+    RevisionResource rsrc = parseCurrentRevisionResource(changeId);
+    UiAction.Description desc = submitHandler.getDescription(rsrc);
+    assertThat(desc.isVisible()).named("visible bit on submit action").isTrue();
+    assertThat(desc.isEnabled()).named("enabled bit on submit action").isTrue();
+  }
+
+  protected void assertChangeMergedEvents(String... expected) throws Exception {
+    eventRecorder.assertChangeMergedEvents(
+        project.get(), "refs/heads/master", expected);
+  }
+
+  protected void assertRefUpdatedEvents(RevCommit... expected)
+      throws Exception {
+    eventRecorder.assertRefUpdatedEvents(
+        project.get(), "refs/heads/master", expected);
   }
 
   protected void assertCurrentRevision(String changeId, int expectedNum,
@@ -489,16 +503,20 @@
   }
 
   protected void assertApproved(String changeId) throws Exception {
+    assertApproved(changeId, admin);
+  }
+
+  protected void assertApproved(String changeId, TestAccount user)
+      throws Exception {
     ChangeInfo c = get(changeId, DETAILED_LABELS);
     LabelInfo cr = c.labels.get("Code-Review");
     assertThat(cr.all).hasSize(1);
     assertThat(cr.all.get(0).value).isEqualTo(2);
-    assertThat(new Account.Id(cr.all.get(0)._accountId)).isEqualTo(admin.getId());
+    assertThat(new Account.Id(cr.all.get(0)._accountId))
+        .isEqualTo(user.getId());
   }
 
-  protected void assertMerged(PushOneCommit.Result change)
-      throws RestApiException {
-    String changeId = change.getChangeId();
+  protected void assertMerged(String changeId) throws RestApiException {
     ChangeStatus status = gApi.changes().id(changeId).info().status;
     assertThat(status).isEqualTo(ChangeStatus.MERGED);
   }
@@ -514,26 +532,34 @@
   }
 
   protected void assertSubmitter(String changeId, int psId)
-      throws OrmException {
-    ChangeNotes cn = notesFactory.create(
-        getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change());
-    PatchSetApproval submitter = approvalsUtil.getSubmitter(
-        db, cn, new PatchSet.Id(cn.getChangeId(), psId));
-    assertThat(submitter.isSubmit()).isTrue();
-    assertThat(submitter.getAccountId()).isEqualTo(admin.getId());
+      throws Exception {
+    assertSubmitter(changeId, psId, admin);
+  }
+
+  protected void assertSubmitter(String changeId, int psId, TestAccount user)
+      throws Exception {
+    Change c =
+        getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change();
+    ChangeNotes cn = notesFactory.createChecked(db, c);
+    PatchSetApproval submitter = approvalsUtil.getSubmitter(db, cn,
+        new PatchSet.Id(cn.getChangeId(), psId));
+    assertThat(submitter).isNotNull();
+    assertThat(submitter.isLegacySubmit()).isTrue();
+    assertThat(submitter.getAccountId()).isEqualTo(user.getId());
   }
 
   protected void assertNoSubmitter(String changeId, int psId)
-      throws OrmException {
-    ChangeNotes cn = notesFactory.create(
-        getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change());
+      throws Exception {
+    Change c =
+        getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change();
+    ChangeNotes cn = notesFactory.createChecked(db, c);
     PatchSetApproval submitter = approvalsUtil.getSubmitter(
         db, cn, new PatchSet.Id(cn.getChangeId(), psId));
     assertThat(submitter).isNull();
   }
 
   protected void assertCherryPick(TestRepository<?> testRepo,
-      boolean contentMerge) throws IOException {
+      boolean contentMerge) throws Exception {
     assertRebase(testRepo, contentMerge);
     RevCommit remoteHead = getRemoteHead();
     assertThat(remoteHead.getFooterLines("Reviewed-On")).isNotEmpty();
@@ -541,7 +567,7 @@
   }
 
   protected void assertRebase(TestRepository<?> testRepo, boolean contentMerge)
-      throws IOException {
+      throws Exception {
     Repository repo = testRepo.getRepository();
     RevCommit localHead = getHead(repo);
     RevCommit remoteHead = getRemoteHead();
@@ -555,25 +581,8 @@
     assertThat(remoteHead.getShortMessage()).isEqualTo(localHead.getShortMessage());
   }
 
-  private RevCommit getHead(Repository repo) throws IOException {
-    return getHead(repo, "HEAD");
-  }
-
-  protected RevCommit getRemoteHead(Project.NameKey project, String branch)
-      throws IOException {
-    try (Repository repo = repoManager.openRepository(project)) {
-      return getHead(repo, "refs/heads/" + branch);
-    }
-  }
-
-  protected RevCommit getRemoteHead()
-      throws IOException {
-    return getRemoteHead(project, "master");
-  }
-
-
   protected List<RevCommit> getRemoteLog(Project.NameKey project, String branch)
-      throws IOException {
+      throws Exception {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       rw.markStart(rw.parseCommit(
@@ -582,29 +591,17 @@
     }
   }
 
-  protected List<RevCommit> getRemoteLog() throws IOException {
+  protected List<RevCommit> getRemoteLog() throws Exception {
     return getRemoteLog(project, "master");
   }
 
-  protected RefUpdateAttribute getOneRefUpdate(String key) {
-    Collection<RefUpdateAttribute> refUpdates = refUpdatedEvents.get(key);
-    assertThat(refUpdates).hasSize(1);
-    return refUpdates.iterator().next();
-  }
-
-  private RevCommit getHead(Repository repo, String name) throws IOException {
-    try (RevWalk rw = new RevWalk(repo)) {
-      return rw.parseCommit(repo.exactRef(name).getObjectId());
-    }
-  }
-
-  private String getLatestDiff(Repository repo) throws IOException {
+  private String getLatestDiff(Repository repo) throws Exception {
     ObjectId oldTreeId = repo.resolve("HEAD~1^{tree}");
     ObjectId newTreeId = repo.resolve("HEAD^{tree}");
     return getLatestDiff(repo, oldTreeId, newTreeId);
   }
 
-  private String getLatestRemoteDiff() throws IOException {
+  private String getLatestRemoteDiff() throws Exception {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       ObjectId oldTreeId = repo.resolve("refs/heads/master~1^{tree}");
@@ -614,7 +611,7 @@
   }
 
   private String getLatestDiff(Repository repo, ObjectId oldTreeId,
-      ObjectId newTreeId) throws IOException {
+      ObjectId newTreeId) throws Exception {
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     try (DiffFormatter fmt = new DiffFormatter(out)) {
       fmt.setRepository(repo);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
index 2b3d035..741864a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -15,12 +15,24 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.change.Submit.TestSubmitInput;
 
+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.Test;
 
 public abstract class AbstractSubmitByMerge extends AbstractSubmit {
@@ -40,7 +52,7 @@
     RevCommit head = getRemoteHead();
     assertThat(head.getParentCount()).isEqualTo(2);
     assertThat(head.getParent(0)).isEqualTo(oldHead);
-    assertThat(head.getParent(1)).isEqualTo(change2.getCommitId());
+    assertThat(head.getParent(1)).isEqualTo(change2.getCommit());
   }
 
   @Test
@@ -54,14 +66,14 @@
     submit(change2.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    testRepo.reset(change.getCommitId());
+    testRepo.reset(change.getCommit());
     PushOneCommit.Result change3 =
         createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
     RevCommit head = getRemoteHead();
     assertThat(head.getParentCount()).isEqualTo(2);
     assertThat(head.getParent(0)).isEqualTo(oldHead);
-    assertThat(head.getParent(1)).isEqualTo(change3.getCommitId());
+    assertThat(head.getParent(1)).isEqualTo(change3.getCommit());
   }
 
   @Test
@@ -77,10 +89,95 @@
     PushOneCommit.Result change2 =
         createChange("Change 2", "a.txt", "other content");
     submitWithConflict(change2.getChangeId(),
-        "Cannot merge " + change2.getCommit().name() + "\n" +
-        "Change could not be merged due to a path conflict.\n\n" +
+        "Failed to submit 1 change due to the following problems:\n" +
+        "Change " + change2.getChange().getId() + ": " +
+        "Change could not be merged due to a path conflict. " +
         "Please rebase the change locally " +
         "and upload the rebased commit for review.");
     assertThat(getRemoteHead()).isEqualTo(oldHead);
   }
+
+  @Test
+  @TestProjectInput(createEmptyCommit = false)
+  public void submitMultipleCommitsToEmptyRepoAsFastForward() throws Exception {
+    PushOneCommit.Result change1 = createChange();
+    PushOneCommit.Result change2 = createChange();
+    approve(change1.getChangeId());
+    submit(change2.getChangeId());
+    assertThat(getRemoteHead().getId()).isEqualTo(change2.getCommit());
+  }
+
+  @Test
+  @TestProjectInput(createEmptyCommit = false)
+  public void submitMultipleCommitsToEmptyRepoWithOneMerge() throws Exception {
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+    PushOneCommit.Result change1 = pushFactory.create(
+          db, admin.getIdent(), testRepo, "Change 1", "a", "a")
+        .to("refs/for/master/" + name("topic"));
+
+    PushOneCommit push2 = pushFactory.create(
+          db, admin.getIdent(), testRepo, "Change 2", "b", "b");
+    push2.noParents();
+    PushOneCommit.Result change2 = push2.to("refs/for/master/" + name("topic"));
+    change2.assertOkStatus();
+
+    approve(change1.getChangeId());
+    submit(change2.getChangeId());
+
+    RevCommit head = getRemoteHead();
+    assertThat(head.getParents()).hasLength(2);
+    assertThat(head.getParent(0)).isEqualTo(change1.getCommit());
+    assertThat(head.getParent(1)).isEqualTo(change2.getCommit());
+  }
+
+  @Test
+  public void repairChangeStateAfterFailure() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange("Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+    RevCommit afterChange1Head = getRemoteHead();
+
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change2 =
+        createChange("Change 2", "b.txt", "other content");
+    Change.Id id2 = change2.getChange().getId();
+    SubmitInput failAfterRefUpdates =
+        new TestSubmitInput(new SubmitInput(), true);
+    submit(change2.getChangeId(), failAfterRefUpdates,
+        ResourceConflictException.class, "Failing after ref updates");
+
+    // Bad: ref advanced but change wasn't updated.
+    PatchSet.Id psId1 = new PatchSet.Id(id2, 1);
+    ChangeInfo info = gApi.changes().id(id2.get()).get();
+    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
+
+    RevCommit tip;
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      ObjectId rev1 = repo.exactRef(psId1.toRefName()).getObjectId();
+      assertThat(rev1).isNotNull();
+
+      tip = rw.parseCommit(repo.exactRef("refs/heads/master").getObjectId());
+      assertThat(tip.getParentCount()).isEqualTo(2);
+      assertThat(tip.getParent(0)).isEqualTo(afterChange1Head);
+      assertThat(tip.getParent(1)).isEqualTo(change2.getCommit());
+    }
+
+    submit(change2.getChangeId(), new SubmitInput(), null, null);
+
+    // Change status and patch set entities were updated, and branch tip stayed
+    // the same.
+    info = gApi.changes().id(id2.get()).get();
+    assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
+    assertThat(Iterables.getLast(info.messages).message)
+        .isEqualTo("Change has been successfully merged by Administrator");
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.exactRef("refs/heads/master").getObjectId())
+          .isEqualTo(tip);
+    }
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index ba98963..880fe89 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -16,11 +16,16 @@
 
 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.PushOneCommit;
+import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.server.change.GetRevisionActions;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.inject.Inject;
 
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -35,6 +40,9 @@
     return submitWholeTopicEnabledConfig();
   }
 
+  @Inject
+  private GetRevisionActions getRevisionActions;
+
   @Test
   public void revisionActionsOneChangePerTopicUnapproved() throws Exception {
     String changeId = createChangeWithTopic().getChangeId();
@@ -79,6 +87,107 @@
   }
 
   @Test
+  public void revisionActionsETag() throws Exception {
+    String parent = createChange().getChangeId();
+    String change = createChangeWithTopic().getChangeId();
+    approve(change);
+    String etag1 = getRevisionActions.getETag(parseCurrentRevisionResource(change));
+
+    approve(parent);
+    String etag2 = getRevisionActions.getETag(parseCurrentRevisionResource(change));
+
+    String changeWithSameTopic = createChangeWithTopic().getChangeId();
+    String etag3 = getRevisionActions.getETag(parseCurrentRevisionResource(change));
+
+    approve(changeWithSameTopic);
+    String etag4 = getRevisionActions.getETag(parseCurrentRevisionResource(change));
+
+    if (isSubmitWholeTopicEnabled()) {
+      assertThat(ImmutableList.of(etag1, etag2, etag3, etag4)).containsNoDuplicates();
+    } else {
+      assertThat(etag2).isNotEqualTo(etag1);
+      assertThat(etag3).isEqualTo(etag2);
+      assertThat(etag4).isEqualTo(etag2);
+    }
+  }
+
+  @Test
+  public void revisionActionsETagWithHiddenDraftInTopic() throws Exception {
+    String change = createChangeWithTopic().getChangeId();
+    approve(change);
+
+    setApiUser(user);
+    String etag1 = getRevisionActions.getETag(parseCurrentRevisionResource(change));
+
+    setApiUser(admin);
+    String draft = createDraftWithTopic().getChangeId();
+    approve(draft);
+
+    setApiUser(user);
+    String etag2 = getRevisionActions.getETag(parseCurrentRevisionResource(change));
+
+    if (isSubmitWholeTopicEnabled()) {
+      assertThat(etag2).isNotEqualTo(etag1);
+    } else {
+      assertThat(etag2).isEqualTo(etag1);
+    }
+  }
+
+  @Test
+  public void revisionActionsAnonymousETag() throws Exception {
+    String parent = createChange().getChangeId();
+    String change = createChangeWithTopic().getChangeId();
+    approve(change);
+
+    setApiUserAnonymous();
+    String etag1 = getRevisionActions.getETag(parseCurrentRevisionResource(change));
+
+    setApiUser(admin);
+    approve(parent);
+
+    setApiUserAnonymous();
+    String etag2 = getRevisionActions.getETag(parseCurrentRevisionResource(change));
+
+    setApiUser(admin);
+    String changeWithSameTopic = createChangeWithTopic().getChangeId();
+
+    setApiUserAnonymous();
+    String etag3 = getRevisionActions.getETag(parseCurrentRevisionResource(change));
+
+    setApiUser(admin);
+    approve(changeWithSameTopic);
+
+    setApiUserAnonymous();
+    String etag4 = getRevisionActions.getETag(parseCurrentRevisionResource(change));
+
+    if (isSubmitWholeTopicEnabled()) {
+      assertThat(ImmutableList.of(etag1, etag2, etag3, etag4)).containsNoDuplicates();
+    } else {
+      assertThat(etag2).isNotEqualTo(etag1);
+      assertThat(etag3).isEqualTo(etag2);
+      assertThat(etag4).isEqualTo(etag2);
+    }
+  }
+
+  @Test
+  @TestProjectInput(submitType = SubmitType.CHERRY_PICK)
+  public void revisionActionsAnonymousETagCherryPickStrategy() throws Exception {
+    String parent = createChange().getChangeId();
+    String change = createChange().getChangeId();
+    approve(change);
+
+    setApiUserAnonymous();
+    String etag1 = getRevisionActions.getETag(parseCurrentRevisionResource(change));
+
+    setApiUser(admin);
+    approve(parent);
+
+    setApiUserAnonymous();
+    String etag2 = getRevisionActions.getETag(parseCurrentRevisionResource(change));
+    assertThat(etag2).isEqualTo(etag1);
+  }
+
+  @Test
   public void revisionActionsTwoChangesInTopic_conflicting() throws Exception {
     String changeId = createChangeWithTopic().getChangeId();
     approve(changeId);
@@ -175,13 +284,20 @@
     assertThat(actions).containsKey("rebase");
   }
 
+  private PushOneCommit.Result createCommitAndPush(
+      TestRepository<InMemoryRepository> repo, String ref,
+      String commitMsg, String fileName, String content) throws Exception {
+    return pushFactory
+        .create(db, admin.getIdent(), repo, commitMsg, fileName, content)
+        .to(ref);
+  }
+
   private PushOneCommit.Result createChangeWithTopic(
       TestRepository<InMemoryRepository> repo, String topic,
       String commitMsg, String fileName, String content) throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(),
-        repo, commitMsg, fileName, content);
     assertThat(topic).isNotEmpty();
-    return push.to("refs/for/master/" + name(topic));
+    return createCommitAndPush(repo, "refs/for/master/" + name(topic),
+        commitMsg, fileName, content);
   }
 
   private PushOneCommit.Result createChangeWithTopic()
@@ -189,4 +305,10 @@
     return createChangeWithTopic(testRepo, "foo2",
         "a message", "a.txt", "content\n");
   }
+
+  private PushOneCommit.Result createDraftWithTopic()
+      throws Exception {
+    return createCommitAndPush(testRepo, "refs/drafts/master/" + name("foo2"),
+        "a message", "a.txt", "content\n");
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
index 1a8e151..04e71eb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
@@ -9,17 +9,19 @@
 OTHER_TESTS = glob(['*IT.java'], excludes = SUBMIT_TESTS)
 
 acceptance_tests(
-  group = 'rest-change-other',
+  group = 'rest_change_other',
   srcs = OTHER_TESTS,
   deps = [
     ':submit_util',
+    '//gerrit-server:server',
+    '//lib/guice:guice',
     '//lib/joda:joda-time',
   ],
   labels = ['rest'],
 )
- 
+
 acceptance_tests(
-  group = 'rest-change-submit',
+  group = 'rest_change_submit',
   srcs = SUBMIT_TESTS,
   deps = [
     ':submit_util',
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUILD
new file mode 100644
index 0000000..c06f02f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUILD
@@ -0,0 +1,36 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+SUBMIT_UTIL_SRCS = [
+  'AbstractSubmit.java',
+  'AbstractSubmitByMerge.java',
+]
+
+SUBMIT_TESTS = glob(['Submit*IT.java'])
+OTHER_TESTS = glob(['*IT.java'], exclude = SUBMIT_TESTS)
+
+acceptance_tests(
+  group = 'rest_change_other',
+  srcs = OTHER_TESTS,
+  deps = [
+    ':submit_util',
+    '//lib/joda:joda-time',
+  ],
+  labels = ['rest'],
+)
+
+acceptance_tests(
+  group = 'rest_change_submit',
+  srcs = SUBMIT_TESTS,
+  deps = [
+    ':submit_util',
+  ],
+  labels = ['rest'],
+)
+
+java_library(
+  name = 'submit_util',
+  srcs = SUBMIT_UTIL_SRCS,
+  deps = [
+    '//gerrit-acceptance-tests:lib',
+  ],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index 91f2962..40b0391 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -81,13 +81,34 @@
     assertMessage(secondMessage, it.next().message);
   }
 
+  @Test
+  public void postMessageWithTag() throws Exception {
+    String changeId = createChange().getChangeId();
+    String tag = "jenkins";
+    String msg = "Message with tag.";
+    postMessage(changeId, msg, tag);
+    ChangeInfo c = get(changeId);
+    assertThat(c.messages).isNotNull();
+    assertThat(c.messages).hasSize(2);
+    Iterator<ChangeMessageInfo> it = c.messages.iterator();
+    assertThat(it.next().message).isEqualTo("Uploaded patch set 1.");
+    ChangeMessageInfo actual = it.next();
+    assertMessage(msg, actual.message);
+    assertThat(actual.tag).isEqualTo(tag);
+  }
+
   private void assertMessage(String expected, String actual) {
     assertThat(actual).isEqualTo("Patch Set 1:\n\n" + expected);
   }
 
   private void postMessage(String changeId, String msg) throws Exception {
+    postMessage(changeId, msg, null);
+  }
+
+  private void postMessage(String changeId, String msg, String tag) throws Exception {
     ReviewInput in = new ReviewInput();
     in.message = msg;
+    in.tag = tag;
     gApi.changes().id(changeId).current().review(in);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index 2229577..875725f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -30,12 +30,9 @@
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
 
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class ChangeOwnerIT extends AbstractDaemonTest {
 
   private TestAccount user2;
@@ -76,27 +73,24 @@
   }
 
   private void assertApproveFails(TestAccount a, String changeId) throws Exception {
-    try {
-      approve(a, changeId);
-    } catch (AuthException expected) {
-      // Expected.
-    }
+    exception.expect(AuthException.class);
+    approve(a, changeId);
   }
 
-  private void grantApproveToChangeOwner() throws IOException,
-      ConfigInvalidException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    md.setMessage(String.format("Grant approve to change owner"));
-    ProjectConfig config = ProjectConfig.read(md);
-    AccessSection s = config.getAccessSection("refs/heads/*", true);
-    Permission p = s.getPermission(LABEL + "Code-Review", true);
-    PermissionRule rule = new PermissionRule(config
-        .resolve(SystemGroupBackend.getGroup(SystemGroupBackend.CHANGE_OWNER)));
-    rule.setMin(-2);
-    rule.setMax(+2);
-    p.add(rule);
-    config.commit(md);
-    projectCache.evict(config.getProject());
+  private void grantApproveToChangeOwner() throws Exception {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      md.setMessage(String.format("Grant approve to change owner"));
+      ProjectConfig config = ProjectConfig.read(md);
+      AccessSection s = config.getAccessSection("refs/heads/*", true);
+      Permission p = s.getPermission(LABEL + "Code-Review", true);
+      PermissionRule rule = new PermissionRule(config
+          .resolve(SystemGroupBackend.getGroup(SystemGroupBackend.CHANGE_OWNER)));
+      rule.setMin(-2);
+      rule.setMax(+2);
+      p.add(rule);
+      config.commit(md);
+      projectCache.evict(config.getProject());
+    }
   }
 
   private String createMyChange() throws Exception {
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..a42f5cd
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -0,0 +1,619 @@
+// 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, notesMigration.readChanges() ? REVIEWER : CC,
+        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, CC, 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();
+    assertReviewers(c, CC, firstUsers);
+
+    // 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, CC, 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()) {
+      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();
+    assertReviewers(c, REVIEWER);
+    assertReviewers(c, CC, user);
+
+    in.state = REVIEWER;
+    addReviewer(changeId, in);
+    c = gApi.changes().id(r.getChangeId()).get();
+    if (notesMigration.readChanges()) {
+      assertReviewers(c, REVIEWER, user);
+      assertReviewers(c, CC);
+    } else {
+      // If NoteDb not enabled, should have had no effect.
+      assertReviewers(c, REVIEWER);
+      assertReviewers(c, CC, user);
+    }
+  }
+
+  @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, change owner should be the only reviewer.
+      assertReviewers(c, REVIEWER, admin);
+      assertReviewers(c, CC, user, observer);
+    }
+
+    // Verify emails were sent to added reviewers.
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(2);
+
+    Message m = messages.get(0);
+    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");
+
+    m = messages.get(1);
+    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.");
+  }
+
+  @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 user is returned with the CC group.
+      assertReviewers(c, REVIEWER, admin);
+      List<TestAccount> expectedCC = users.subList(0, mediumGroupSize);
+      expectedCC.add(user);
+      assertReviewers(c, CC, expectedCC);
+    }
+  }
+
+  @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 c78231b..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
@@ -26,7 +26,9 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 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;
 
@@ -46,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);
@@ -56,6 +58,18 @@
   @Test
   @TestProjectInput(cloneAs = "user")
   public void updateProjectConfig() throws Exception {
+    String id = testUpdateProjectConfig();
+    assertThat(gApi.changes().id(id).get().revisions).hasSize(1);
+  }
+
+  @Test
+  @TestProjectInput(cloneAs = "user", submitType = SubmitType.CHERRY_PICK)
+  public void updateProjectConfigWithCherryPick() throws Exception {
+    String id = testUpdateProjectConfig();
+    assertThat(gApi.changes().id(id).get().revisions).hasSize(2);
+  }
+
+  private String testUpdateProjectConfig() throws Exception {
     Config cfg = readProjectConfig();
     assertThat(cfg.getString("project", null, "description")).isNull();
     String desc = "new project description";
@@ -74,6 +88,11 @@
     fetchRefsMetaConfig();
     assertThat(readProjectConfig().getString("project", null, "description"))
         .isEqualTo(desc);
+    String changeRev = gApi.changes().id(id).get().currentRevision;
+    String branchRev = gApi.projects().name(project.get())
+        .branch(RefNames.REFS_CONFIG).get().revision;
+    assertThat(changeRev).isEqualTo(branchRev);
+    return id;
   }
 
   @Test
@@ -99,10 +118,11 @@
       gApi.changes().id(id).current().submit();
       fail("expected submit to fail");
     } catch (ResourceConflictException e) {
+      int n = gApi.changes().id(id).info()._number;
       assertThat(e).hasMessage(
-          "Cannot merge " + r.getCommit().name() + "\n"
-          + "Change contains a project configuration that changes the parent"
-          + " project.\n"
+          "Failed to submit 1 change due to the following problems:\n"
+          + "Change " + n + ": Change contains a project configuration that"
+          + " changes the parent project.\n"
           + "The change must be submitted by a Gerrit administrator.");
     }
 
@@ -126,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 9edf9b2..993f3f5 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
@@ -16,25 +16,46 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 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.NoHttpd;
+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.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.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;
 
-@NoHttpd
 public class CreateChangeIT extends AbstractDaemonTest {
   @ConfigSuite.Config
   public static Config allowDraftsDisabled() {
@@ -51,10 +72,9 @@
     TestTimeUtil.useSystemTime();
   }
 
-
   @Test
   public void createEmptyChange_MissingBranch() throws Exception {
-    ChangeInfo ci = new ChangeInfo();
+    ChangeInput ci = new ChangeInput();
     ci.project = project.get();
     assertCreateFails(ci, BadRequestException.class,
         "branch must be non-empty");
@@ -62,7 +82,7 @@
 
   @Test
   public void createEmptyChange_MissingMessage() throws Exception {
-    ChangeInfo ci = new ChangeInfo();
+    ChangeInput ci = new ChangeInput();
     ci.project = project.get();
     ci.branch = "master";
     assertCreateFails(ci, BadRequestException.class,
@@ -71,32 +91,211 @@
 
   @Test
   public void createEmptyChange_InvalidStatus() throws Exception {
-    ChangeInfo ci = newChangeInfo(ChangeStatus.MERGED);
+    ChangeInput ci = newChangeInput(ChangeStatus.MERGED);
     assertCreateFails(ci, BadRequestException.class,
         "unsupported change status");
   }
 
   @Test
+  public void createEmptyChange_InvalidChangeId() throws Exception {
+   ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+   ci.subject = "Subject\n\nChange-Id: I0000000000000000000000000000000000000000";
+   assertCreateFails(ci, ResourceConflictException.class,
+       "invalid Change-Id line format in commit message footer");
+  }
+
+  @Test
+  public void createEmptyChange_InvalidSubject() throws Exception {
+   ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+   ci.subject = "Change-Id: I1234000000000000000000000000000000000000";
+   assertCreateFails(ci, ResourceConflictException.class,
+       "missing subject; Change-Id must be in commit message footer");
+  }
+
+  @Test
   public void createNewChange() throws Exception {
-    assertCreateSucceeds(newChangeInfo(ChangeStatus.NEW));
+    ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
+    assertThat(info.revisions.get(info.currentRevision).commit.message)
+        .contains("Change-Id: " + info.changeId);
   }
 
   @Test
-  public void createDraftChange() throws Exception {
+  public void createNewChangeWithChangeId() throws Exception {
+    ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+    String changeId = "I1234000000000000000000000000000000000000";
+    String changeIdLine = "Change-Id: " + changeId;
+    ci.subject = "Subject\n\n" + changeIdLine;
+    ChangeInfo info = assertCreateSucceeds(ci);
+    assertThat(info.changeId).isEqualTo(changeId);
+    assertThat(info.revisions.get(info.currentRevision).commit.message).contains(changeIdLine);
+  }
+
+  @Test
+  public void createNewChangeSignedOffByFooter() throws Exception {
+    setSignedOffByFooter(true);
+    try {
+      ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
+      String message = info.revisions.get(info.currentRevision).commit.message;
+      assertThat(message).contains(
+          String.format("%sAdministrator <%s>", SIGNED_OFF_BY_TAG,
+              admin.getIdent().getEmailAddress()));
+    } finally {
+      setSignedOffByFooter(false);
+    }
+  }
+
+  @Test
+  public void createNewChangeSignedOffByFooterWithChangeId() throws Exception {
+    setSignedOffByFooter(true);
+    try {
+      ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+      String changeId = "I1234000000000000000000000000000000000000";
+      String changeIdLine = "Change-Id: " + changeId;
+      ci.subject = "Subject\n\n" + changeIdLine;
+      ChangeInfo info = assertCreateSucceeds(ci);
+      assertThat(info.changeId).isEqualTo(changeId);
+      String message = info.revisions.get(info.currentRevision).commit.message;
+      assertThat(message).contains(changeIdLine);
+      assertThat(message).contains(
+          String.format("%sAdministrator <%s>", SIGNED_OFF_BY_TAG,
+              admin.getIdent().getEmailAddress()));
+    } finally {
+      setSignedOffByFooter(false);
+    }
+  }
+
+  @Test
+  public void createNewDraftChange() throws Exception {
     assume().that(isAllowDrafts()).isTrue();
-    assertCreateSucceeds(newChangeInfo(ChangeStatus.DRAFT));
+    assertCreateSucceeds(newChangeInput(ChangeStatus.DRAFT));
   }
 
   @Test
-  public void createDraftChangeNotAllowed() throws Exception {
+  public void createNewDraftChangeNotAllowed() throws Exception {
     assume().that(isAllowDrafts()).isFalse();
-    ChangeInfo ci = newChangeInfo(ChangeStatus.DRAFT);
+    ChangeInput ci = newChangeInput(ChangeStatus.DRAFT);
     assertCreateFails(ci, MethodNotAllowedException.class,
         "draft workflow is disabled");
   }
 
-  private ChangeInfo newChangeInfo(ChangeStatus status) {
-    ChangeInfo in = new ChangeInfo();
+  @Test
+  public void noteDbCommit() throws Exception {
+    assume().that(notesMigration.readChanges()).isTrue();
+
+    ChangeInfo c = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      RevCommit commit = rw.parseCommit(
+          repo.exactRef(changeMetaRef(new Change.Id(c._number))).getObjectId());
+
+      assertThat(commit.getShortMessage()).isEqualTo("Create change");
+
+      PersonIdent expectedAuthor = changeNoteUtil.newIdent(
+          accountCache.get(admin.id).getAccount(), c.created, serverIdent.get(),
+          AnonymousCowardNameProvider.DEFAULT);
+      assertThat(commit.getAuthorIdent()).isEqualTo(expectedAuthor);
+
+      assertThat(commit.getCommitterIdent())
+          .isEqualTo(new PersonIdent(serverIdent.get(), c.created));
+      assertThat(commit.getParentCount()).isEqualTo(0);
+    }
+  }
+
+  @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();
     in.branch = "master";
     in.subject = "Empty change";
@@ -105,18 +304,20 @@
     return in;
   }
 
-  private void assertCreateSucceeds(ChangeInfo in) throws Exception {
+  private ChangeInfo assertCreateSucceeds(ChangeInput in) throws Exception {
     ChangeInfo out = gApi.changes().create(in).get();
     assertThat(out.branch).isEqualTo(in.branch);
-    assertThat(out.subject).isEqualTo(in.subject);
+    assertThat(out.subject).isEqualTo(in.subject.split("\n")[0]);
     assertThat(out.topic).isEqualTo(in.topic);
     assertThat(out.status).isEqualTo(in.status);
     assertThat(out.revisions).hasSize(1);
+    assertThat(out.submitted).isNull();
     Boolean draft = Iterables.getOnlyElement(out.revisions.values()).draft;
     assertThat(booleanToDraftStatus(draft)).isEqualTo(in.status);
+    return out;
   }
 
-  private void assertCreateFails(ChangeInfo in,
+  private void assertCreateFails(ChangeInput in,
       Class<? extends RestApiException> errType, String errSubstring)
       throws Exception {
     exception.expect(errType);
@@ -130,4 +331,69 @@
     }
     return draft ? ChangeStatus.DRAFT : ChangeStatus.NEW;
   }
+
+  // TODO(davido): Expose setting of account preferences in the API
+  private void setSignedOffByFooter(boolean value) throws Exception {
+    RestResponse r = adminRestSession.get("/accounts/" + admin.email
+        + "/preferences");
+    r.assertOK();
+    GeneralPreferencesInfo i =
+        newGson().fromJson(r.getReader(), GeneralPreferencesInfo.class);
+    i.signedOffBy = value;
+
+    r = adminRestSession.put("/accounts/" + admin.email + "/preferences", i);
+    r.assertOK();
+    GeneralPreferencesInfo o = newGson().fromJson(r.getReader(),
+        GeneralPreferencesInfo.class);
+
+    if (value) {
+      assertThat(o.signedOffBy).isTrue();
+    } else {
+      assertThat(o.signedOffBy).isNull();
+    }
+  }
+
+  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 47d071f..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
@@ -16,36 +16,57 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableList;
 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.PushOneCommit.Result;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 
-import org.apache.http.HttpStatus;
+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;
 
-import java.io.IOException;
+import java.util.HashMap;
 
+@NoHttpd
 public class DeleteDraftPatchSetIT extends AbstractDaemonTest {
 
+  @Inject
+  private AllUsersName allUsers;
+
   @Test
-  public void deletePatchSet() throws Exception {
+  public void deletePatchSetNotDraft() throws Exception {
     String changeId = createChange().getChangeId();
     PatchSet ps = getCurrentPatchSet(changeId);
     String triplet = project.get() + "~master~" + changeId;
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.NEW);
-    RestResponse r = deletePatchSet(changeId, ps, adminSession);
-    assertThat(r.getEntityContent()).isEqualTo("Patch set is not a draft");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Patch set is not a draft");
+    setApiUser(admin);
+    deletePatchSet(changeId, ps);
   }
 
   @Test
@@ -56,26 +77,188 @@
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
-    RestResponse r = deletePatchSet(changeId, ps, userSession);
-    assertThat(r.getEntityContent()).isEqualTo("Not found: " + changeId);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+
+    exception.expect(ResourceNotFoundException.class);
+    exception.expectMessage("Not found: " + changeId);
+    setApiUser(user);
+    deletePatchSet(changeId, ps);
   }
 
   @Test
   public void deleteDraftPatchSetAndChange() throws Exception {
     String changeId = createDraftChangeWith2PS();
     PatchSet ps = getCurrentPatchSet(changeId);
-    String triplet = project.get() + "~master~" + changeId;
-    ChangeInfo c = get(triplet);
-    assertThat(c.id).isEqualTo(triplet);
-    assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
-    RestResponse r = deletePatchSet(changeId, ps, adminSession);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    assertThat(getChange(changeId).patchSets()).hasSize(1);
+    Change.Id id = ps.getId().getParentKey();
+
+    DraftInput din = new DraftInput();
+    din.path = "a.txt";
+    din.message = "comment on a.txt";
+    gApi.changes().id(changeId).current().createDraft(din);
+
+    if (notesMigration.writeChanges()) {
+      assertThat(getDraftRef(admin, id)).isNotNull();
+    }
+
+    ChangeData cd = getChange(changeId);
+    assertThat(cd.patchSets()).hasSize(2);
+    assertThat(cd.change().currentPatchSetId().get()).isEqualTo(2);
+    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.DRAFT);
+    deletePatchSet(changeId, ps);
+
+    cd = getChange(changeId);
+    assertThat(cd.patchSets()).hasSize(1);
+    assertThat(cd.change().currentPatchSetId().get()).isEqualTo(1);
+
     ps = getCurrentPatchSet(changeId);
-    r = deletePatchSet(changeId, ps, adminSession);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    deletePatchSet(changeId, ps);
     assertThat(queryProvider.get().byKeyPrefix(changeId)).isEmpty();
+
+    if (notesMigration.writeChanges()) {
+      assertThat(getDraftRef(admin, id)).isNull();
+      assertThat(getMetaRef(id)).isNull();
+    }
+
+    exception.expect(ResourceNotFoundException.class);
+    gApi.changes().id(id.get());
+  }
+
+  @Test
+  public void deleteDraftPS1() throws Exception {
+    String changeId = createDraftChangeWith2PS();
+
+    ReviewInput rin = new ReviewInput();
+    rin.message = "Change message";
+    CommentInput cin = new CommentInput();
+    cin.line = 1;
+    cin.patchSet = 1;
+    cin.path = PushOneCommit.FILE_NAME;
+    cin.side = Side.REVISION;
+    cin.message = "Inline comment";
+    rin.comments = new HashMap<>();
+    rin.comments.put(cin.path, ImmutableList.of(cin));
+    gApi.changes().id(changeId).revision(1).review(rin);
+
+    ChangeData cd = getChange(changeId);
+    PatchSet.Id delPsId = new PatchSet.Id(cd.getId(), 1);
+    PatchSet ps = cd.patchSet(delPsId);
+    deletePatchSet(changeId, ps);
+
+    cd = getChange(changeId);
+    assertThat(cd.patchSets()).hasSize(1);
+    assertThat(Iterables.getOnlyElement(cd.patchSets()).getId().get())
+        .isEqualTo(2);
+
+    // Other entities based on deleted patch sets are also deleted.
+    for (ChangeMessage m : cd.messages()) {
+      assertThat(m.getPatchSetId()).named(m.toString()).isNotEqualTo(delPsId);
+    }
+    for (PatchLineComment c : cd.publishedComments()) {
+      assertThat(c.getPatchSetId()).named(c.toString()).isNotEqualTo(delPsId);
+    }
+  }
+
+  @Test
+  public void deleteDraftPS2() throws Exception {
+    String changeId = createDraftChangeWith2PS();
+
+    ReviewInput rin = new ReviewInput();
+    rin.message = "Change message";
+    CommentInput cin = new CommentInput();
+    cin.line = 1;
+    cin.patchSet = 1;
+    cin.path = PushOneCommit.FILE_NAME;
+    cin.side = Side.REVISION;
+    cin.message = "Inline comment";
+    rin.comments = new HashMap<>();
+    rin.comments.put(cin.path, ImmutableList.of(cin));
+    gApi.changes().id(changeId).revision(1).review(rin);
+
+    ChangeData cd = getChange(changeId);
+    PatchSet.Id delPsId = new PatchSet.Id(cd.getId(), 2);
+    PatchSet ps = cd.patchSet(delPsId);
+    deletePatchSet(changeId, ps);
+
+    cd = getChange(changeId);
+    assertThat(cd.patchSets()).hasSize(1);
+    assertThat(Iterables.getOnlyElement(cd.patchSets()).getId().get())
+        .isEqualTo(1);
+
+    // Other entities based on deleted patch sets are also deleted.
+    for (ChangeMessage m : cd.messages()) {
+      assertThat(m.getPatchSetId()).named(m.toString()).isNotEqualTo(delPsId);
+    }
+    for (PatchLineComment c : cd.publishedComments()) {
+      assertThat(c.getPatchSetId()).named(c.toString()).isNotEqualTo(delPsId);
+    }
+  }
+
+  @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)) {
+      return repo.exactRef(RefNames.refsDraftComments(changeId, account.id));
+    }
+  }
+
+  private Ref getMetaRef(Change.Id changeId) throws Exception {
+    try (Repository repo = repoManager.openRepository(project)) {
+      return repo.exactRef(RefNames.changeMetaRef(changeId));
+    }
   }
 
   private String createDraftChangeWith2PS() throws Exception {
@@ -86,19 +269,15 @@
     return push.to("refs/drafts/master").getChangeId();
   }
 
-  private PatchSet getCurrentPatchSet(String changeId) throws OrmException {
+  private PatchSet getCurrentPatchSet(String changeId) throws Exception {
     return getChange(changeId).currentPatchSet();
   }
 
-  private ChangeData getChange(String changeId) throws OrmException {
+  private ChangeData getChange(String changeId) throws Exception {
     return Iterables.getOnlyElement(queryProvider.get().byKeyPrefix(changeId));
   }
 
-  private static RestResponse deletePatchSet(String changeId,
-      PatchSet ps, RestSession s) throws IOException {
-    return s.delete("/changes/"
-        + changeId
-        + "/revisions/"
-        + ps.getRevision().get());
+  private void deletePatchSet(String changeId, PatchSet ps) throws Exception {
+    gApi.changes().id(changeId).revision(ps.getId().get()).delete();
   }
 }
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 2707507..3228a22a 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
@@ -22,18 +22,21 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
+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.LabelInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.testutil.ConfigSuite;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.testutil.NoteDbMode;
 
-import org.apache.http.HttpStatus;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
-import java.io.IOException;
+import java.util.Collection;
 
 public class DraftChangeIT extends AbstractDaemonTest {
   @ConfigSuite.Config
@@ -50,9 +53,10 @@
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.NEW);
-    RestResponse response = deleteChange(changeId, adminSession);
-    assertThat(response.getEntityContent()).isEqualTo("Change is not a draft");
-    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+    RestResponse response = deleteChange(changeId, adminRestSession);
+    assertThat(response.getEntityContent())
+        .isEqualTo("Change is not a draft: " + c._number);
+    response.assertConflict();
   }
 
   @Test
@@ -65,8 +69,7 @@
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
-    RestResponse response = deleteChange(changeId, adminSession);
-    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    deleteChange(changeId, adminRestSession).assertNoContent();
 
     exception.expect(ResourceNotFoundException.class);
     get(triplet);
@@ -83,8 +86,7 @@
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
     assertThat(c.revisions.get(c.currentRevision).draft).isTrue();
-    RestResponse response = publishChange(changeId);
-    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    publishChange(changeId).assertNoContent();
     c = get(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.NEW);
     assertThat(c.revisions.get(c.currentRevision).draft).isNull();
@@ -100,8 +102,7 @@
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
-    RestResponse response = publishPatchSet(changeId);
-    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    publishPatchSet(changeId).assertNoContent();
     assertThat(get(triplet).status).isEqualTo(ChangeStatus.NEW);
   }
 
@@ -112,24 +113,51 @@
     r.assertErrorStatus("draft workflow is disabled");
   }
 
-  private PushOneCommit.Result createDraftChange() throws Exception {
-    return pushTo("refs/drafts/master");
+  @Test
+  public void listApprovalsOnDraftChange() throws Exception {
+    assume().that(isAllowDrafts()).isTrue();
+    PushOneCommit.Result result = createDraftChange();
+    result.assertOkStatus();
+    String changeId = result.getChangeId();
+    String triplet = project.get() + "~master~" + changeId;
+
+    gApi.changes().id(triplet).addReviewer(user.fullName);
+
+    ChangeInfo info = get(triplet);
+    LabelInfo label = info.labels.get("Code-Review");
+    assertThat(label.all).hasSize(1);
+    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);
+    assertThat(ccs).hasSize(1);
+    assertThat(ccs.iterator().next()._accountId).isEqualTo(user.id.get());
+
+    setApiUser(user);
+    gApi.changes().id(triplet).current().review(ReviewInput.recommend());
+    setApiUser(admin);
+
+    label = get(triplet).labels.get("Code-Review");
+    assertThat(label.all).hasSize(1);
+    assertThat(label.all.get(0)._accountId).isEqualTo(user.id.get());
+    assertThat(label.all.get(0).value).isEqualTo(1);
   }
 
   private static RestResponse deleteChange(String changeId,
-      RestSession s) throws IOException {
+      RestSession s) throws Exception {
     return s.delete("/changes/" + changeId);
   }
 
-  private RestResponse publishChange(String changeId) throws IOException {
-    return adminSession.post("/changes/" + changeId + "/publish");
+  private RestResponse publishChange(String changeId) throws Exception {
+    return adminRestSession.post("/changes/" + changeId + "/publish");
   }
 
-  private RestResponse publishPatchSet(String changeId) throws IOException,
-      OrmException {
+  private RestResponse publishPatchSet(String changeId) throws Exception {
     PatchSet patchSet = Iterables.getOnlyElement(
         queryProvider.get().byKeyPrefix(changeId)).currentPatchSet();
-    return adminSession.post("/changes/"
+    return adminRestSession.post("/changes/"
         + changeId
         + "/revisions/"
         + patchSet.getRevision().get()
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index aa7305a..a044772 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -14,24 +14,41 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+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 java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.common.truth.IterableSubject;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.testutil.TestTimeUtil;
 
+import org.junit.AfterClass;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 @NoHttpd
 public class HashtagsIT extends AbstractDaemonTest {
   @Before
   public void before() {
-    assume().that(notesMigration.enabled()).isTrue();
+    assume().that(notesMigration.readChanges()).isTrue();
+  }
+
+  @BeforeClass
+  public static void setTimeForTesting() {
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
+  }
+
+  @AfterClass
+  public static void restoreTime() {
+    TestTimeUtil.useSystemTime();
   }
 
   @Test
@@ -48,11 +65,13 @@
     // Adding a single hashtag returns a single hashtag.
     addHashtags(r, "tag2");
     assertThatGet(r).containsExactly("tag2");
+    assertMessage(r, "Hashtag added: tag2");
 
     // Adding another single hashtag to change that already has one hashtag
     // returns a sorted list of hashtags with existing and new.
     addHashtags(r, "tag1");
     assertThatGet(r).containsExactly("tag1", "tag2").inOrder();
+    assertMessage(r, "Hashtag added: tag1");
   }
 
   @Test
@@ -62,11 +81,13 @@
     // Adding multiple hashtags returns a sorted list of hashtags.
     addHashtags(r, "tag3", "tag1");
     assertThatGet(r).containsExactly("tag1", "tag3").inOrder();
+    assertMessage(r, "Hashtags added: tag1, tag3");
 
     // Adding multiple hashtags to change that already has hashtags returns a
     // sorted list of hashtags with existing and new.
     addHashtags(r, "tag2", "tag4");
     assertThatGet(r).containsExactly("tag1", "tag2", "tag3", "tag4").inOrder();
+    assertMessage(r, "Hashtags added: tag2, tag4");
   }
 
   @Test
@@ -76,10 +97,16 @@
     PushOneCommit.Result r = createChange();
     addHashtags(r, "tag2");
     assertThatGet(r).containsExactly("tag2");
+    assertMessage(r, "Hashtag added: tag2");
+    ChangeMessageInfo last = getLastMessage(r);
+
     addHashtags(r, "tag2");
     assertThatGet(r).containsExactly("tag2");
+    assertNoNewMessageSince(r, last);
+
     addHashtags(r, "tag1", "tag2");
     assertThatGet(r).containsExactly("tag1", "tag2").inOrder();
+    assertMessage(r, "Hashtag added: tag1");
   }
 
   @Test
@@ -89,30 +116,37 @@
     // Leading # is stripped from added tag.
     addHashtags(r, "#tag1");
     assertThatGet(r).containsExactly("tag1");
+    assertMessage(r, "Hashtag added: tag1");
 
     // Leading # is stripped from multiple added tags.
     addHashtags(r, "#tag2", "#tag3");
     assertThatGet(r).containsExactly("tag1", "tag2", "tag3").inOrder();
+    assertMessage(r, "Hashtags added: tag2, tag3");
 
     // Leading # is stripped from removed tag.
     removeHashtags(r, "#tag2");
     assertThatGet(r).containsExactly("tag1", "tag3").inOrder();
+    assertMessage(r, "Hashtag removed: tag2");
 
     // Leading # is stripped from multiple removed tags.
     removeHashtags(r, "#tag1", "#tag3");
     assertThatGet(r).isEmpty();
+    assertMessage(r, "Hashtags removed: tag1, tag3");
 
     // Leading # and space are stripped from added tag.
     addHashtags(r, "# tag1");
     assertThatGet(r).containsExactly("tag1");
+    assertMessage(r, "Hashtag added: tag1");
 
     // Multiple leading # are stripped from added tag.
     addHashtags(r, "##tag2");
     assertThatGet(r).containsExactly("tag1", "tag2").inOrder();
+    assertMessage(r, "Hashtag added: tag2");
 
     // Multiple leading spaces and # are stripped from added tag.
     addHashtags(r, "# # tag3");
     assertThatGet(r).containsExactly("tag1", "tag2", "tag3").inOrder();
+    assertMessage(r, "Hashtag added: tag3");
   }
 
   @Test
@@ -124,12 +158,14 @@
     assertThatGet(r).containsExactly("tag1");
     removeHashtags(r, "tag1");
     assertThatGet(r).isEmpty();
+    assertMessage(r, "Hashtag removed: tag1");
 
     // Removing a single tag from a change that has multiple tags returns a
     // sorted list of remaining tags.
     addHashtags(r, "tag1", "tag2", "tag3");
     removeHashtags(r, "tag2");
     assertThatGet(r).containsExactly("tag1", "tag3").inOrder();
+    assertMessage(r, "Hashtag removed: tag2");
   }
 
   @Test
@@ -141,6 +177,7 @@
     assertThatGet(r).containsExactly("tag1", "tag2").inOrder();
     removeHashtags(r, "tag1", "tag2");
     assertThatGet(r).isEmpty();
+    assertMessage(r, "Hashtags removed: tag1, tag2");
 
     // Removing multiple tags from a change that has multiple tags returns a
     // sorted list of remaining tags.
@@ -148,6 +185,7 @@
     assertThatGet(r).containsExactly("tag1", "tag2", "tag3", "tag4").inOrder();
     removeHashtags(r, "tag2", "tag4");
     assertThatGet(r).containsExactly("tag1", "tag3").inOrder();
+    assertMessage(r, "Hashtags removed: tag2, tag4");
   }
 
   @Test
@@ -155,20 +193,26 @@
     // Removing a single hashtag from change that has no hashtags returns an
     // empty list.
     PushOneCommit.Result r = createChange();
+    ChangeMessageInfo last = getLastMessage(r);
     removeHashtags(r, "tag1");
     assertThatGet(r).isEmpty();
+    assertNoNewMessageSince(r, last);
 
     // Removing a single non-existing tag from a change that only has one other
     // tag returns a list of only one tag.
     addHashtags(r, "tag1");
+    last = getLastMessage(r);
     removeHashtags(r, "tag4");
     assertThatGet(r).containsExactly("tag1");
+    assertNoNewMessageSince(r, last);
 
     // Removing a single non-existing tag from a change that has multiple tags
     // returns a sorted list of tags without any deleted.
     addHashtags(r, "tag1", "tag2", "tag3");
+    last = getLastMessage(r);
     removeHashtags(r, "tag4");
     assertThatGet(r).containsExactly("tag1", "tag2", "tag3").inOrder();
+    assertNoNewMessageSince(r, last);
   }
 
   @Test
@@ -181,6 +225,7 @@
     input.remove = Sets.newHashSet("tag1");
     gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
     assertThatGet(r).containsExactly("tag2", "tag3", "tag4");
+    assertMessage(r, "Hashtags added: tag3, tag4\nHashtag removed: tag1");
 
     // Adding and removing the same hashtag actually removes it.
     addHashtags(r, "tag1", "tag2");
@@ -189,6 +234,15 @@
     input.remove = Sets.newHashSet("tag3");
     gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
     assertThatGet(r).containsExactly("tag1", "tag2", "tag4");
+    assertMessage(r, "Hashtag removed: tag3");
+  }
+
+  @Test
+  public void testHashtagWithMixedCase() throws Exception {
+    PushOneCommit.Result r = createChange();
+    addHashtags(r, "MyHashtag");
+    assertThatGet(r).containsExactly("MyHashtag");
+    assertMessage(r, "Hashtag added: MyHashtag");
   }
 
   private IterableSubject<
@@ -217,4 +271,25 @@
         .id(r.getChange().getId().get())
         .setHashtags(input);
   }
+
+  private void assertMessage(PushOneCommit.Result r, String expectedMessage)
+      throws Exception {
+    assertThat(getLastMessage(r).message).isEqualTo(expectedMessage);
+  }
+
+  private void assertNoNewMessageSince(PushOneCommit.Result r,
+      ChangeMessageInfo expected) throws Exception {
+    checkNotNull(expected);
+    ChangeMessageInfo last = getLastMessage(r);
+    assertThat(last.message).isEqualTo(expected.message);
+    assertThat(last.id).isEqualTo(expected.id);
+  }
+
+  private ChangeMessageInfo getLastMessage(PushOneCommit.Result r)
+      throws Exception {
+    ChangeMessageInfo lastMessage = Iterables.getLast(
+        gApi.changes().id(r.getChange().getId().get()).get().messages, null);
+    assertThat(lastMessage).named(lastMessage.message).isNotNull();
+    return lastMessage;
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index 405f8e5..9dba788b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -14,27 +14,25 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 public class IndexChangeIT extends AbstractDaemonTest {
   @Test
   public void indexChange() throws Exception {
     String changeId = createChange().getChangeId();
-    RestResponse r = adminSession.post("/changes/" + changeId + "/index/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    adminRestSession
+        .post("/changes/" + changeId + "/index/")
+        .assertNoContent();
   }
 
   @Test
   public void indexChangeOnNonVisibleBranch() throws Exception {
     String changeId = createChange().getChangeId();
-    blockRead(project, "refs/heads/master");
-    RestResponse r = userSession.post("/changes/" + changeId + "/index/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    blockRead("refs/heads/master");
+    userRestSession
+        .post("/changes/" + changeId + "/index/")
+        .assertNotFound();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
index 3728a51..b0d34f0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
@@ -20,7 +20,6 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -29,6 +28,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @NoHttpd
@@ -39,7 +39,7 @@
 
   @Before
   public void setUp() throws Exception {
-    results = Lists.newArrayList();
+    results = new ArrayList<>();
     results.add(push("file contents", null));
     changeId = results.get(0).getChangeId();
     results.add(push("new contents 1", changeId));
@@ -95,6 +95,6 @@
   }
 
   private String commitId(int i) {
-    return results.get(i).getCommitId().name();
+    return results.get(i).getCommit().name();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
new file mode 100644
index 0000000..b66358f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -0,0 +1,269 @@
+// 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.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+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.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.MoveInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
+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.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+@NoHttpd
+public class MoveChangeIT extends AbstractDaemonTest {
+  @Test
+  public void moveChangeWithShortRef() throws Exception {
+    // Move change to a different branch using short ref name
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    move(r.getChangeId(), newBranch.getShortName());
+    assertThat(r.getChange().change().getDest()).isEqualTo(newBranch);
+  }
+
+  @Test
+  public void moveChangeWithFullRef() throws Exception {
+    // Move change to a different branch using full ref name
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    move(r.getChangeId(), newBranch.get());
+    assertThat(r.getChange().change().getDest()).isEqualTo(newBranch);
+  }
+
+  @Test
+  public void moveChangeWithMessage() throws Exception {
+    // Provide a message using --message flag
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    String moveMessage = "Moving for the move test";
+    move(r.getChangeId(), newBranch.get(), moveMessage);
+    assertThat(r.getChange().change().getDest()).isEqualTo(newBranch);
+    StringBuilder expectedMessage = new StringBuilder();
+    expectedMessage.append("Change destination moved from master to moveTest");
+    expectedMessage.append("\n\n");
+    expectedMessage.append(moveMessage);
+    assertThat(r.getChange().messages().get(1).getMessage())
+        .isEqualTo(expectedMessage.toString());
+  }
+
+  @Test
+  public void moveChangeToSameRefAsCurrent() throws Exception {
+    // Move change to the branch same as change's destination
+    PushOneCommit.Result r = createChange();
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Change is already destined for the specified branch");
+    move(r.getChangeId(), r.getChange().change().getDest().get());
+  }
+
+  @Test
+  public void moveChangeToSameChangeId() throws Exception {
+    // Move change to a branch with existing change with same change ID
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    int changeNum = r.getChange().change().getChangeId();
+    createChange(newBranch.get(), r.getChangeId());
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Destination " + newBranch.getShortName()
+        + " has a different change with same change key " + r.getChangeId());
+    move(changeNum, newBranch.get());
+  }
+
+  @Test
+  public void moveChangeToNonExistentRef() throws Exception {
+    // Move change to a non-existing branch
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch = new Branch.NameKey(
+        r.getChange().change().getProject(), "does_not_exist");
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Destination " + newBranch.get()
+        + " not found in the project");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  @Test
+  public void moveClosedChange() throws Exception {
+    // Move a change which is not open
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    merge(r);
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Change is merged");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  @Test
+  public void moveMergeCommitChange() throws Exception {
+    // Move a change which has a merge commit as the current PS
+    // Create a merge commit and push for review
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = createChange();
+    TestRepository<?>.CommitBuilder commitBuilder =
+        testRepo.branch("HEAD").commit().insertChangeId();
+    commitBuilder
+      .parent(r1.getCommit())
+      .parent(r2.getCommit())
+      .message("Move change Merge Commit")
+      .author(admin.getIdent())
+      .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()));
+    RevCommit c = commitBuilder.create();
+    pushHead(testRepo, "refs/for/master", false, false);
+
+    // Try to move the merge commit to another branch
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r1.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Merge commit cannot be moved");
+    move(GitUtil.getChangeId(testRepo, c).get(), newBranch.get());
+  }
+
+  @Test
+  public void moveChangeToBranchWithoutUploadPerms() throws Exception {
+    // Move change to a destination where user doesn't have upload permissions
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "blocked_branch");
+    createBranch(newBranch);
+    block(Permission.PUSH,
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(),
+        "refs/for/" + newBranch.get());
+    exception.expect(AuthException.class);
+    exception.expectMessage("Move not permitted");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  @Test
+  public void moveChangeFromBranchWithoutAbandonPerms() throws Exception {
+    // Move change for which user does not have abandon permissions
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    block(Permission.ABANDON,
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(),
+        r.getChange().change().getDest().get());
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage("Move not permitted");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  @Test
+  public void moveChangeToBranchThatContainsCurrentCommit() throws Exception {
+    // Move change to a branch for which current PS revision is reachable from
+    // tip
+
+    // Create a change
+    PushOneCommit.Result r = createChange();
+    int changeNum = r.getChange().change().getChangeId();
+
+    // Create a branch with that same commit
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    BranchInput bi = new BranchInput();
+    bi.revision = r.getCommit().name();
+    gApi.projects()
+      .name(newBranch.getParentKey().get())
+      .branch(newBranch.get())
+      .create(bi);
+
+    // Try to move the change to the branch with the same commit
+    exception.expect(ResourceConflictException.class);
+    exception
+        .expectMessage("Current patchset revision is reachable from tip of "
+            + newBranch.get());
+    move(changeNum, newBranch.get());
+  }
+
+  @Test
+  public void moveChangeWithCurrentPatchSetLocked() throws Exception {
+    // Move change that is locked
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    LabelType patchSetLock = Util.patchSetLock();
+    cfg.getLabelSections().put(patchSetLock.getName(), patchSetLock);
+    AccountGroup.UUID registeredUsers =
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+    Util.allow(cfg, Permission.forLabel(patchSetLock.getName()), 0, 1, registeredUsers,
+        "refs/heads/*");
+    saveProjectConfig(cfg);
+    grant(Permission.LABEL + "Patch-Set-Lock", project, "refs/heads/*");
+    revision(r).review(new ReviewInput().label("Patch-Set-Lock", 1));
+
+    exception.expect(AuthException.class);
+    exception.expectMessage("Move not permitted");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  private void move(int changeNum, String destination)
+      throws RestApiException {
+    gApi.changes().id(changeNum).move(destination);
+  }
+
+  private void move(String changeId, String destination)
+      throws RestApiException {
+    gApi.changes().id(changeId).move(destination);
+  }
+
+  private void move(String changeId, String destination, String message)
+      throws RestApiException {
+    MoveInput in = new MoveInput();
+    in.destinationBranch = destination;
+    in.message = message;
+    gApi.changes().id(changeId).move(in);
+  }
+
+  private PushOneCommit.Result createChange(String branch, String changeId)
+      throws Exception {
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, changeId);
+    PushOneCommit.Result result = push.to("refs/for/" + branch);
+    result.assertOkStatus();
+    return result;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index efaa1df..af43373 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -19,13 +19,22 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 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.ChangeInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.change.Submit.TestSubmitInput;
+import com.google.gerrit.server.git.strategy.CommitMergeStatus;
 
+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.Test;
 
 import java.util.List;
@@ -39,11 +48,16 @@
 
   @Test
   public void submitWithCherryPickIfFastForwardPossible() throws Exception {
+    RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
     assertCherryPick(testRepo, false);
-    assertThat(getRemoteHead().getParent(0))
+    RevCommit newHead = getRemoteHead();
+    assertThat(newHead.getParent(0))
       .isEqualTo(change.getCommit().getParent(0));
+
+    assertRefUpdatedEvents(initialHead, newHead);
+    assertChangeMergedEvents(change.getChangeId(), newHead.name());
   }
 
   @Test
@@ -53,7 +67,7 @@
         createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit headAfterFirstSubmit = getRemoteHead();
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 =
         createChange("Change 2", "b.txt", "other content");
@@ -61,36 +75,51 @@
     assertCherryPick(testRepo, false);
     RevCommit newHead = getRemoteHead();
     assertThat(newHead.getParentCount()).isEqualTo(1);
-    assertThat(newHead.getParent(0)).isEqualTo(oldHead);
+    assertThat(newHead.getParent(0)).isEqualTo(headAfterFirstSubmit);
     assertCurrentRevision(change2.getChangeId(), 2, newHead);
     assertSubmitter(change2.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 2);
     assertPersonEquals(admin.getIdent(), newHead.getAuthorIdent());
     assertPersonEquals(admin.getIdent(), newHead.getCommitterIdent());
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
+        headAfterFirstSubmit, newHead);
+    assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(),
+        change2.getChangeId(), newHead.name());
   }
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge() throws Exception {
+    RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change =
         createChange("Change 1", "a.txt", "aaa\nbbb\nccc\n");
     submit(change.getChangeId());
+    RevCommit headAfterFirstSubmit = getRemoteHead();
     PushOneCommit.Result change2 =
         createChange("Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
     submit(change2.getChangeId());
+    RevCommit headAfterSecondSubmit = getRemoteHead();
 
-    RevCommit oldHead = getRemoteHead();
-    testRepo.reset(change.getCommitId());
+    testRepo.reset(change.getCommit());
     PushOneCommit.Result change3 =
         createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
     assertCherryPick(testRepo, true);
-    RevCommit newHead = getRemoteHead();
-    assertThat(newHead.getParent(0)).isEqualTo(oldHead);
+    RevCommit headAfterThirdSubmit = getRemoteHead();
+    assertThat(headAfterThirdSubmit.getParent(0))
+        .isEqualTo(headAfterSecondSubmit);
     assertApproved(change3.getChangeId());
-    assertCurrentRevision(change3.getChangeId(), 2, newHead);
+    assertCurrentRevision(change3.getChangeId(), 2, headAfterThirdSubmit);
     assertSubmitter(change2.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 2);
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
+        headAfterFirstSubmit, headAfterSecondSubmit,
+        headAfterSecondSubmit, headAfterThirdSubmit);
+    assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(),
+        change2.getChangeId(), headAfterSecondSubmit.name(),
+        change3.getChangeId(), headAfterThirdSubmit.name());
   }
 
   @Test
@@ -101,19 +130,22 @@
         createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit newHead = getRemoteHead();
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 =
         createChange("Change 2", "a.txt", "other content");
     submitWithConflict(change2.getChangeId(),
-        "Cannot merge " + change2.getCommitId().name() + "\n" +
-        "Change could not be merged due to a path conflict.\n\n" +
-        "Please rebase the change locally and " +
+        "Failed to submit 1 change due to the following problems:\n" +
+        "Change " + change2.getChange().getId() + ": Change could not be " +
+        "merged due to a path conflict. Please rebase the change locally and " +
         "upload the rebased commit for review.");
 
-    assertThat(getRemoteHead()).isEqualTo(oldHead);
-    assertCurrentRevision(change2.getChangeId(), 1, change2.getCommitId());
+    assertThat(getRemoteHead()).isEqualTo(newHead);
+    assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit());
     assertNoSubmitter(change2.getChangeId(), 1);
+
+    assertRefUpdatedEvents(initialHead, newHead);
+    assertChangeMergedEvents(change.getChangeId(), newHead.name());
   }
 
   @Test
@@ -123,19 +155,25 @@
         createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit headAfterFirstSubmit = getRemoteHead();
     testRepo.reset(initialHead);
     createChange("Change 2", "b.txt", "other content");
     PushOneCommit.Result change3 =
         createChange("Change 3", "c.txt", "different content");
     submit(change3.getChangeId());
     assertCherryPick(testRepo, false);
-    RevCommit newHead = getRemoteHead();
-    assertThat(newHead.getParent(0)).isEqualTo(oldHead);
+    RevCommit headAfterSecondSubmit = getRemoteHead();
+    assertThat(headAfterSecondSubmit.getParent(0))
+        .isEqualTo(headAfterFirstSubmit);
     assertApproved(change3.getChangeId());
-    assertCurrentRevision(change3.getChangeId(), 2, newHead);
+    assertCurrentRevision(change3.getChangeId(), 2, headAfterSecondSubmit);
     assertSubmitter(change3.getChangeId(), 1);
     assertSubmitter(change3.getChangeId(), 2);
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
+        headAfterFirstSubmit, headAfterSecondSubmit);
+    assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(),
+        change3.getChangeId(), headAfterSecondSubmit.name());
   }
 
   @Test
@@ -145,20 +183,23 @@
         createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit newHead = getRemoteHead();
     testRepo.reset(initialHead);
     createChange("Change 2", "b.txt", "other content");
     PushOneCommit.Result change3 =
         createChange("Change 3", "b.txt", "different content");
     submitWithConflict(change3.getChangeId(),
-        "Cannot merge " + change3.getCommitId().name() + "\n" +
-        "Change could not be merged due to a path conflict.\n\n" +
-        "Please rebase the change locally and " +
+        "Failed to submit 1 change due to the following problems:\n" +
+        "Change " + change3.getChange().getId() + ": Change could not be " +
+        "merged due to a path conflict. Please rebase the change locally and " +
         "upload the rebased commit for review.");
 
-    assertThat(getRemoteHead()).isEqualTo(oldHead);
-    assertCurrentRevision(change3.getChangeId(), 1, change3.getCommitId());
+    assertThat(getRemoteHead()).isEqualTo(newHead);
+    assertCurrentRevision(change3.getChangeId(), 1, change3.getCommit());
     assertNoSubmitter(change3.getChangeId(), 1);
+
+    assertRefUpdatedEvents(initialHead, newHead);
+    assertChangeMergedEvents(change.getChangeId(), newHead.name());
   }
 
   @Test
@@ -166,25 +207,28 @@
     RevCommit initialHead = getRemoteHead();
 
     testRepo.reset(initialHead);
-    PushOneCommit.Result change2 = createChange("Change 2", "b", "b");
+    PushOneCommit.Result change = createChange("Change 1", "b", "b");
 
     testRepo.reset(initialHead);
-    PushOneCommit.Result change3 = createChange("Change 3", "c", "c");
+    PushOneCommit.Result change2 = createChange("Change 2", "c", "c");
 
     testRepo.reset(initialHead);
-    PushOneCommit.Result change4 = createChange("Change 4", "d", "d");
+    PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
 
+    approve(change.getChangeId());
     approve(change2.getChangeId());
-    approve(change3.getChangeId());
-    submit(change4.getChangeId());
+    submit(change3.getChangeId());
 
     List<RevCommit> log = getRemoteLog();
     assertThat(log.get(0).getShortMessage()).isEqualTo(
-        change4.getCommit().getShortMessage());
+        change3.getCommit().getShortMessage());
     assertThat(log.get(1).getId()).isEqualTo(initialHead.getId());
 
+    assertNew(change.getChangeId());
     assertNew(change2.getChangeId());
-    assertNew(change3.getChangeId());
+
+    assertRefUpdatedEvents(initialHead, log.get(0));
+    assertChangeMergedEvents(change3.getChangeId(), log.get(0).name());
   }
 
   @Test
@@ -192,27 +236,34 @@
     RevCommit initialHead = getRemoteHead();
 
     testRepo.reset(initialHead);
-    PushOneCommit.Result change2 = createChange("Change 2", "b", "b");
-    PushOneCommit.Result change3 = createChange("Change 3", "c", "c");
-    assertThat(change3.getCommit().getParent(0)).isEqualTo(change2.getCommit());
+    PushOneCommit.Result change = createChange("Change 1", "b", "b");
+    PushOneCommit.Result change2 = createChange("Change 2", "c", "c");
+    assertThat(change2.getCommit().getParent(0)).isEqualTo(change.getCommit());
 
-    // Submit succeeds; change3 is successfully cherry-picked onto head.
-    submit(change3.getChangeId());
-    // Submit succeeds; change2 is successfully cherry-picked onto head
-    // (which was change3's cherry-pick).
+    // Submit succeeds; change2 is successfully cherry-picked onto head.
     submit(change2.getChangeId());
+    RevCommit headAfterFirstSubmit = getRemoteHead();
+    // Submit succeeds; change is successfully cherry-picked onto head
+    // (which was change2's cherry-pick).
+    submit(change.getChangeId());
+    RevCommit headAfterSecondSubmit = getRemoteHead();
 
-    // change2 is the new tip.
+    // change is the new tip.
     List<RevCommit> log = getRemoteLog();
     assertThat(log.get(0).getShortMessage()).isEqualTo(
-        change2.getCommit().getShortMessage());
+        change.getCommit().getShortMessage());
     assertThat(log.get(0).getParent(0)).isEqualTo(log.get(1));
 
     assertThat(log.get(1).getShortMessage()).isEqualTo(
-        change3.getCommit().getShortMessage());
+        change2.getCommit().getShortMessage());
     assertThat(log.get(1).getParent(0)).isEqualTo(log.get(2));
 
     assertThat(log.get(2).getId()).isEqualTo(initialHead.getId());
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
+        headAfterFirstSubmit, headAfterSecondSubmit);
+    assertChangeMergedEvents(change2.getChangeId(), headAfterFirstSubmit.name(),
+        change.getChangeId(), headAfterSecondSubmit.name());
   }
 
   @Test
@@ -220,27 +271,28 @@
     RevCommit initialHead = getRemoteHead();
 
     testRepo.reset(initialHead);
-    PushOneCommit.Result change2 = createChange("Change 2", "b", "b1");
-    PushOneCommit.Result change3 = createChange("Change 3", "b", "b2");
-    assertThat(change3.getCommit().getParent(0)).isEqualTo(change2.getCommit());
+    PushOneCommit.Result change = createChange("Change 1", "b", "b1");
+    PushOneCommit.Result change2 = createChange("Change 2", "b", "b2");
+    assertThat(change2.getCommit().getParent(0)).isEqualTo(change.getCommit());
 
-    // Submit fails; change3 contains the delta "b1" -> "b2", which cannot be
+    // Submit fails; change2 contains the delta "b1" -> "b2", which cannot be
     // applied against tip.
-    submitWithConflict(change3.getChangeId(),
-        "Cannot merge " + change3.getCommitId().name() + "\n" +
-        "Change could not be merged due to a path conflict.\n\n" +
-        "Please rebase the change locally and " +
+    submitWithConflict(change2.getChangeId(),
+        "Failed to submit 1 change due to the following problems:\n" +
+        "Change " + change2.getChange().getId() + ": Change could not be " +
+        "merged due to a path conflict. Please rebase the change locally and " +
         "upload the rebased commit for review.");
 
-    ChangeInfo info3 = get(change3.getChangeId(), ListChangesOption.MESSAGES);
+    ChangeInfo info3 = get(change2.getChangeId(), ListChangesOption.MESSAGES);
     assertThat(info3.status).isEqualTo(ChangeStatus.NEW);
-    assertThat(Iterables.getLast(info3.messages).message.toLowerCase())
-        .contains("path conflict");
 
     // Tip has not changed.
     List<RevCommit> log = getRemoteLog();
     assertThat(log.get(0)).isEqualTo(initialHead.getId());
-    assertNoSubmitter(change3.getChangeId(), 1);
+    assertNoSubmitter(change2.getChangeId(), 1);
+
+    assertRefUpdatedEvents();
+    assertChangeMergedEvents();
   }
 
   @Test
@@ -248,16 +300,116 @@
     RevCommit initialHead = getRemoteHead();
 
     testRepo.reset(initialHead);
-    PushOneCommit.Result change2 = createChange("Change 2", "b", "b");
-    PushOneCommit.Result change3 = createChange("Change 3", "c", "c");
-    PushOneCommit.Result change4 = createChange("Change 5", "e", "e");
+    PushOneCommit.Result change = createChange("Change 1", "b", "b");
+    PushOneCommit.Result change2 = createChange("Change 2", "c", "c");
+    PushOneCommit.Result change3 = createChange("Change 3", "e", "e");
 
-    // Out of the above, only submit 4. 2,3 are not related to 4
-    // by topic or ancestor (due to cherrypicking!)
-    approve(change3.getChangeId());
-    submit(change4.getChangeId());
+    // Out of the above, only submit change 3. Changes 1 and 2 are not
+    // related to change 3 by topic or ancestor (due to cherrypicking!)
+    approve(change2.getChangeId());
+    submit(change3.getChangeId());
+    RevCommit newHead = getRemoteHead();
 
+    assertNew(change.getChangeId());
     assertNew(change2.getChangeId());
-    assertNew(change3.getChangeId());
+
+    assertRefUpdatedEvents(initialHead, newHead);
+    assertChangeMergedEvents(change3.getChangeId(), newHead.name());
+  }
+
+  @Test
+  @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
+  public void submitIdenticalTree() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+
+    PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "a");
+
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "a");
+
+    submit(change1.getChangeId());
+    RevCommit headAfterFirstSubmit = getRemoteHead();
+    assertThat(headAfterFirstSubmit.getShortMessage()).isEqualTo("Change 1");
+
+    submit(change2.getChangeId(), new SubmitInput(), null, null);
+
+    assertThat(getRemoteHead()).isEqualTo(headAfterFirstSubmit);
+
+    ChangeInfo info2 = get(change2.getChangeId());
+    assertThat(info2.status).isEqualTo(ChangeStatus.MERGED);
+    assertThat(Iterables.getLast(info2.messages).message)
+        .isEqualTo(CommitMergeStatus.SKIPPED_IDENTICAL_TREE.getMessage());
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
+    assertChangeMergedEvents(change1.getChangeId(), headAfterFirstSubmit.name(),
+        change2.getChangeId(), headAfterFirstSubmit.name());
+  }
+
+  @Test
+  public void repairChangeStateAfterFailure() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange("Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit headAfterFirstSubmit = getRemoteHead();
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change2 =
+        createChange("Change 2", "b.txt", "other content");
+    Change.Id id2 = change2.getChange().getId();
+    SubmitInput failAfterRefUpdates =
+        new TestSubmitInput(new SubmitInput(), true);
+    submit(change2.getChangeId(), failAfterRefUpdates,
+        ResourceConflictException.class, "Failing after ref updates");
+    RevCommit headAfterFailedSubmit = getRemoteHead();
+
+    // Bad: ref advanced but change wasn't updated.
+    PatchSet.Id psId1 = new PatchSet.Id(id2, 1);
+    PatchSet.Id psId2 = new PatchSet.Id(id2, 2);
+    ChangeInfo info = gApi.changes().id(id2.get()).get();
+    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
+    assertThat(getPatchSet(psId2)).isNull();
+
+    ObjectId rev2;
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      ObjectId rev1 = repo.exactRef(psId1.toRefName()).getObjectId();
+      assertThat(rev1).isNotNull();
+
+      rev2 = repo.exactRef(psId2.toRefName()).getObjectId();
+      assertThat(rev2).isNotNull();
+      assertThat(rev2).isNotEqualTo(rev1);
+      assertThat(rw.parseCommit(rev2).getParent(0))
+          .isEqualTo(headAfterFirstSubmit);
+
+      assertThat(repo.exactRef("refs/heads/master").getObjectId())
+          .isEqualTo(rev2);
+    }
+
+    submit(change2.getChangeId());
+
+    // Change status and patch set entities were updated, and branch tip stayed
+    // the same.
+    RevCommit headAfterSecondSubmit = getRemoteHead();
+    assertThat(headAfterSecondSubmit).isEqualTo(headAfterFailedSubmit);
+    info = gApi.changes().id(id2.get()).get();
+    assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(2);
+    PatchSet ps2 = getPatchSet(psId2);
+    assertThat(ps2).isNotNull();
+    assertThat(ps2.getRevision().get()).isEqualTo(rev2.name());
+    assertThat(Iterables.getLast(info.messages).message)
+        .isEqualTo("Change has been successfully cherry-picked as "
+            + rev2.name() + " by Administrator");
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.exactRef("refs/heads/master").getObjectId())
+          .isEqualTo(rev2);
+    }
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
+    assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(),
+        change2.getChangeId(), headAfterSecondSubmit.name());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index 374fb13..65f3fc8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -15,14 +15,27 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
 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.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.change.Submit.TestSubmitInput;
 
+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.eclipse.jgit.transport.PushResult;
 import org.junit.Test;
 
 import java.util.Map;
@@ -36,58 +49,66 @@
 
   @Test
   public void submitWithFastForward() throws Exception {
-    RevCommit oldHead = getRemoteHead();
+    RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit head = getRemoteHead();
-    assertThat(head.getId()).isEqualTo(change.getCommitId());
-    assertThat(head.getParent(0)).isEqualTo(oldHead);
+    RevCommit updatedHead = getRemoteHead();
+    assertThat(updatedHead.getId()).isEqualTo(change.getCommit());
+    assertThat(updatedHead.getParent(0)).isEqualTo(initialHead);
     assertSubmitter(change.getChangeId(), 1);
+
+    assertRefUpdatedEvents(initialHead, updatedHead);
+    assertChangeMergedEvents(change.getChangeId(), updatedHead.name());
   }
 
   @Test
-  public void submitTwoChangesWithFastForward() throws Exception {
-    RevCommit originalHead = getRemoteHead();
+  public void submitMultipleChangesWithFastForward() throws Exception {
+    RevCommit initialHead = getRemoteHead();
 
     PushOneCommit.Result change = createChange();
     PushOneCommit.Result change2 = createChange();
+    PushOneCommit.Result change3 = createChange();
 
     String id1 = change.getChangeId();
     String id2 = change2.getChangeId();
+    String id3 = change3.getChangeId();
     approve(id1);
-    submit(id2);
+    approve(id2);
+    submit(id3);
 
     RevCommit updatedHead = getRemoteHead();
-    assertThat(updatedHead.getId()).isEqualTo(change2.getCommit());
-    assertThat(updatedHead.getParent(0).getId()).isEqualTo(change.getCommit());
+    assertThat(updatedHead.getId()).isEqualTo(change3.getCommit());
+    assertThat(updatedHead.getParent(0).getId()).isEqualTo(change2.getCommit());
     assertSubmitter(change.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 1);
+    assertSubmitter(change3.getChangeId(), 1);
     assertPersonEquals(admin.getIdent(), updatedHead.getAuthorIdent());
     assertPersonEquals(admin.getIdent(), updatedHead.getCommitterIdent());
-    assertSubmittedTogether(id1, id2, id1);
-    assertSubmittedTogether(id2, id2, id1);
+    assertSubmittedTogether(id1, id3, id2, id1);
+    assertSubmittedTogether(id2, id3, id2, id1);
+    assertSubmittedTogether(id3, id3, id2, id1);
 
-    RefUpdateAttribute refUpdate = getOneRefUpdate(
-        project.get() + "-refs/heads/master");
-    assertThat(refUpdate).isNotNull();
-    assertThat(refUpdate.oldRev).isEqualTo(originalHead.name());
-    assertThat(refUpdate.newRev).isEqualTo(updatedHead.name());
+    assertRefUpdatedEvents(initialHead, updatedHead);
+    assertChangeMergedEvents(id1, updatedHead.name(),
+        id2, updatedHead.name(),
+        id3, updatedHead.name());
   }
 
   @Test
   public void submitTwoChangesWithFastForward_missingDependency() throws Exception {
-    RevCommit oldHead = getRemoteHead();
+    RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change1 = createChange();
     PushOneCommit.Result change2 = createChange();
 
     Change.Id id1 = change1.getPatchSetId().getParentKey();
     submitWithConflict(change2.getChangeId(),
-        "The change could not be submitted because it depends on change(s) [" +
-        id1 + "], which could not be submitted because:\n" +
-        id1 + ": needs Code-Review;");
+        "Failed to submit 2 changes due to the following problems:\n"
+        + "Change " + id1 + ": needs Code-Review");
 
-    RevCommit head = getRemoteHead();
-    assertThat(head.getId()).isEqualTo(oldHead.getId());
+    RevCommit updatedHead = getRemoteHead();
+    assertThat(updatedHead.getId()).isEqualTo(initialHead.getId());
+    assertRefUpdatedEvents();
+    assertChangeMergedEvents();
   }
 
   @Test
@@ -97,7 +118,7 @@
         createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit headAfterFirstSubmit = getRemoteHead();
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 =
         createChange("Change 2", "b.txt", "other content");
@@ -110,10 +131,87 @@
     assertThat(info.enabled).isNull();
 
     submitWithConflict(change2.getChangeId(),
-        "Cannot merge " + change2.getCommitId().name() + "\n" +
-        "Project policy requires all submissions to be a fast-forward.\n\n" +
-        "Please rebase the change locally and upload again for review.");
-    assertThat(getRemoteHead()).isEqualTo(oldHead);
+        "Failed to submit 1 change due to the following problems:\n" +
+        "Change " + change2.getChange().getId() + ": Project policy requires " +
+        "all submissions to be a fast-forward. Please rebase the change " +
+        "locally and upload again for review.");
+    assertThat(getRemoteHead()).isEqualTo(headAfterFirstSubmit);
     assertSubmitter(change.getChangeId(), 1);
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
+    assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name());
+  }
+
+  @Test
+  public void repairChangeStateAfterFailure() throws Exception {
+    PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
+    Change.Id id = change.getChange().getId();
+    SubmitInput failAfterRefUpdates =
+        new TestSubmitInput(new SubmitInput(), true);
+    submit(change.getChangeId(), failAfterRefUpdates,
+        ResourceConflictException.class, "Failing after ref updates");
+
+    // Bad: ref advanced but change wasn't updated.
+    PatchSet.Id psId = new PatchSet.Id(id, 1);
+    ChangeInfo info = gApi.changes().id(id.get()).get();
+    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
+
+    ObjectId rev;
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      rev = repo.exactRef(psId.toRefName()).getObjectId();
+      assertThat(rev).isNotNull();
+      assertThat(repo.exactRef("refs/heads/master").getObjectId())
+          .isEqualTo(rev);
+    }
+
+    submit(change.getChangeId());
+
+    // Change status was updated, and branch tip stayed the same.
+    info = gApi.changes().id(id.get()).get();
+    assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
+    assertThat(Iterables.getLast(info.messages).message)
+        .isEqualTo("Change has been successfully merged by Administrator");
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.exactRef("refs/heads/master").getObjectId())
+          .isEqualTo(rev);
+    }
+
+    assertRefUpdatedEvents();
+    assertChangeMergedEvents(change.getChangeId(), getRemoteHead().name());
+  }
+
+  @Test
+  public void submitSameCommitsAsInExperimentalBranch() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+
+    grant(Permission.CREATE, project, "refs/heads/*");
+    grant(Permission.PUSH, project, "refs/heads/experimental");
+
+    RevCommit c1 = commitBuilder()
+        .add("b.txt", "1")
+        .message("commit at tip")
+        .create();
+    String id1 = GitUtil.getChangeId(testRepo, c1).get();
+
+    PushResult r1 = pushHead(testRepo, "refs/for/master", false);
+    assertThat(r1.getRemoteUpdate("refs/for/master").getNewObjectId())
+        .isEqualTo(c1.getId());
+
+    PushResult r2 = pushHead(testRepo, "refs/heads/experimental", false);
+    assertThat(r2.getRemoteUpdate("refs/heads/experimental").getNewObjectId())
+        .isEqualTo(c1.getId());
+
+    submit(id1);
+    RevCommit headAfterSubmit = getRemoteHead();
+
+    assertThat(getRemoteHead().getId()).isEqualTo(c1.getId());
+    assertSubmitter(id1, 1);
+
+    assertRefUpdatedEvents(initialHead, headAfterSubmit);
+    assertChangeMergedEvents(id1, headAfterSubmit.name());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
index eb1d16b..315971f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
@@ -22,8 +22,6 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
-import java.util.List;
-
 public class SubmitByMergeAlwaysIT extends AbstractSubmitByMerge {
 
   @Override
@@ -33,47 +31,69 @@
 
   @Test
   public void submitWithMergeIfFastForwardPossible() throws Exception {
-    RevCommit oldHead = getRemoteHead();
+    RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit head = getRemoteHead();
-    assertThat(head.getParentCount()).isEqualTo(2);
-    assertThat(head.getParent(0)).isEqualTo(oldHead);
-    assertThat(head.getParent(1)).isEqualTo(change.getCommitId());
+    RevCommit headAfterSubmit = getRemoteHead();
+    assertThat(headAfterSubmit.getParentCount()).isEqualTo(2);
+    assertThat(headAfterSubmit.getParent(0)).isEqualTo(initialHead);
+    assertThat(headAfterSubmit.getParent(1)).isEqualTo(change.getCommit());
     assertSubmitter(change.getChangeId(), 1);
-    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
-    assertPersonEquals(serverIdent.get(), head.getCommitterIdent());
+    assertPersonEquals(admin.getIdent(), headAfterSubmit.getAuthorIdent());
+    assertPersonEquals(serverIdent.get(), headAfterSubmit.getCommitterIdent());
+
+    assertRefUpdatedEvents(initialHead, headAfterSubmit);
+    assertChangeMergedEvents(change.getChangeId(), headAfterSubmit.name());
   }
 
   @Test
   public void submitMultipleChanges() throws Exception {
     RevCommit initialHead = getRemoteHead();
 
-    testRepo.reset(initialHead);
-    PushOneCommit.Result change2 = createChange("Change 2", "b", "b");
+    // Submit a change so that the remote head advances
+    PushOneCommit.Result change = createChange("Change 1", "b", "b");
+    submit(change.getChangeId());
 
-    testRepo.reset(initialHead);
-    PushOneCommit.Result change3 = createChange("Change 3", "c", "c");
+    // The remote head should now be a merge of the previous head
+    // and "Change 1"
+    RevCommit headAfterFirstSubmit = getRemoteLog().get(0);
+    assertThat(headAfterFirstSubmit.getParent(1).getShortMessage()).isEqualTo(
+        change.getCommit().getShortMessage());
+    assertThat(headAfterFirstSubmit.getParent(0).getShortMessage()).isEqualTo(
+        initialHead.getShortMessage());
+    assertThat(headAfterFirstSubmit.getParent(0).getId()).isEqualTo(
+        initialHead.getId());
 
-    testRepo.reset(initialHead);
-    PushOneCommit.Result change4 = createChange("Change 4", "d", "d");
-
+    // Submit three changes at the same time
+    PushOneCommit.Result change2 = createChange("Change 2", "c", "c");
+    PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
+    PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
     approve(change2.getChangeId());
     approve(change3.getChangeId());
     submit(change4.getChangeId());
 
-    List<RevCommit> log = getRemoteLog();
-    RevCommit tip = log.get(0);
-    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
+    // Submitting change 4 should result in changes 2 and 3 also being submitted
+    assertMerged(change2.getChangeId());
+    assertMerged(change3.getChangeId());
+
+    // The remote head should now be a merge of the new head after
+    // the previous submit, and "Change 4".
+    RevCommit headAfterSecondSubmit = getRemoteLog().get(0);
+    assertThat(headAfterSecondSubmit.getParent(1).getShortMessage()).isEqualTo(
         change4.getCommit().getShortMessage());
-    assertThat(tip.getParent(0).getShortMessage()).isEqualTo(
-        initialHead.getShortMessage());
-    assertThat(tip.getParent(0).getId()).isEqualTo(initialHead.getId());
+    assertThat(headAfterSecondSubmit.getParent(0).getShortMessage()).isEqualTo(
+        headAfterFirstSubmit.getShortMessage());
+    assertThat(headAfterSecondSubmit.getParent(0).getId()).isEqualTo(
+        headAfterFirstSubmit.getId());
+    assertPersonEquals(admin.getIdent(), headAfterSecondSubmit.getAuthorIdent());
+    assertPersonEquals(serverIdent.get(),
+        headAfterSecondSubmit.getCommitterIdent());
 
-    assertPersonEquals(admin.getIdent(), tip.getAuthorIdent());
-    assertPersonEquals(serverIdent.get(), tip.getCommitterIdent());
-
-    assertNew(change2.getChangeId());
-    assertNew(change3.getChangeId());
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
+        headAfterFirstSubmit, headAfterSecondSubmit);
+    assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(),
+        change2.getChangeId(), headAfterSecondSubmit.name(),
+        change3.getChangeId(), headAfterSecondSubmit.name(),
+        change4.getChangeId(), headAfterSecondSubmit.name());
   }
 }
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 04baacd..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
@@ -1,14 +1,34 @@
+// Copyright (C) 2013 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 com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestProjectInput;
+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.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Project;
 
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Test;
 
 import java.util.List;
@@ -22,15 +42,18 @@
 
   @Test
   public void submitWithFastForward() throws Exception {
-    RevCommit oldHead = getRemoteHead();
+    RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit head = getRemoteHead();
-    assertThat(head.getId()).isEqualTo(change.getCommitId());
-    assertThat(head.getParent(0)).isEqualTo(oldHead);
+    RevCommit updatedHead = getRemoteHead();
+    assertThat(updatedHead.getId()).isEqualTo(change.getCommit());
+    assertThat(updatedHead.getParent(0)).isEqualTo(initialHead);
     assertSubmitter(change.getChangeId(), 1);
-    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
+    assertPersonEquals(admin.getIdent(), updatedHead.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), updatedHead.getCommitterIdent());
+
+    assertRefUpdatedEvents(initialHead, updatedHead);
+    assertChangeMergedEvents(change.getChangeId(), updatedHead.name());
   }
 
   @Test
@@ -38,40 +61,52 @@
     RevCommit initialHead = getRemoteHead();
 
     testRepo.reset(initialHead);
-    PushOneCommit.Result change2 = createChange("Change 2", "b", "b");
+    PushOneCommit.Result change = createChange("Change 1", "b", "b");
 
     testRepo.reset(initialHead);
-    PushOneCommit.Result change3 = createChange("Change 3", "c", "c");
+    PushOneCommit.Result change2 = createChange("Change 2", "c", "c");
 
     testRepo.reset(initialHead);
-    PushOneCommit.Result change4 = createChange("Change 4", "d", "d");
+    PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
+    PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
+    PushOneCommit.Result change5 = createChange("Change 5", "f", "f");
 
-    // Change 2 stays untouched.
-    approve(change2.getChangeId());
-    // Change 3 is a fast-forward, no need to merge.
-    submit(change3.getChangeId());
+    // Change 2 is a fast-forward, no need to merge.
+    submit(change2.getChangeId());
 
-    RevCommit tip = getRemoteLog().get(0);
-    assertThat(tip.getShortMessage()).isEqualTo(
-        change3.getCommit().getShortMessage());
-    assertThat(tip.getParent(0).getId()).isEqualTo(
+    RevCommit headAfterFirstSubmit = getRemoteLog().get(0);
+    assertThat(headAfterFirstSubmit.getShortMessage()).isEqualTo(
+        change2.getCommit().getShortMessage());
+    assertThat(headAfterFirstSubmit.getParent(0).getId()).isEqualTo(
         initialHead.getId());
-    assertPersonEquals(admin.getIdent(), tip.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), tip.getCommitterIdent());
+    assertPersonEquals(admin.getIdent(), headAfterFirstSubmit.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), headAfterFirstSubmit.getCommitterIdent());
 
-    // We need to merge change 4.
-    submit(change4.getChangeId());
+    // We need to merge changes 3, 4 and 5.
+    approve(change3.getChangeId());
+    approve(change4.getChangeId());
+    submit(change5.getChangeId());
 
-    tip = getRemoteLog().get(0);
-    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
-        change4.getCommit().getShortMessage());
-    assertThat(tip.getParent(0).getShortMessage()).isEqualTo(
-        change3.getCommit().getShortMessage());
+    RevCommit headAfterSecondSubmit = getRemoteLog().get(0);
+    assertThat(headAfterSecondSubmit.getParent(1).getShortMessage()).isEqualTo(
+        change5.getCommit().getShortMessage());
+    assertThat(headAfterSecondSubmit.getParent(0).getShortMessage()).isEqualTo(
+        change2.getCommit().getShortMessage());
 
-    assertPersonEquals(admin.getIdent(), tip.getAuthorIdent());
-    assertPersonEquals(serverIdent.get(), tip.getCommitterIdent());
+    assertPersonEquals(admin.getIdent(), headAfterSecondSubmit.getAuthorIdent());
+    assertPersonEquals(serverIdent.get(), headAfterSecondSubmit.getCommitterIdent());
 
-    assertNew(change2.getChangeId());
+    // First change stays untouched.
+    assertNew(change.getChangeId());
+
+    // The two submit operations should have resulted in two ref-update events
+    // and three change-merged events.
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
+        headAfterFirstSubmit, headAfterSecondSubmit);
+    assertChangeMergedEvents(change2.getChangeId(), headAfterFirstSubmit.name(),
+        change3.getChangeId(), headAfterSecondSubmit.name(),
+        change4.getChangeId(), headAfterSecondSubmit.name(),
+        change5.getChangeId(), headAfterSecondSubmit.name());
   }
 
   @Test
@@ -181,9 +216,9 @@
 
     if (isSubmitWholeTopicEnabled()) {
       submitWithConflict(change1b.getChangeId(),
-          "Cannot merge " + change3.getCommit().name() + "\n" +
-          "Change could not be merged due to a path conflict.\n\n" +
-          "Please rebase the change locally " +
+          "Failed to submit 5 changes due to the following problems:\n" +
+          "Change " + change3.getChange().getId() + ": Change could not be " +
+          "merged due to a path conflict. Please rebase the change locally " +
           "and upload the rebased commit for review.");
     } else {
       submit(change1b.getChangeId());
@@ -218,10 +253,13 @@
 
   @Test
   public void submitWithMergedAncestorsOnOtherBranch() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+
     PushOneCommit.Result change1 = createChange(testRepo,  "master",
         "base commit",
         "a.txt", "1", "");
     submit(change1.getChangeId());
+    RevCommit headAfterFirstSubmit = getRemoteHead();
 
     gApi.projects()
         .name(project.get())
@@ -234,8 +272,8 @@
 
     submit(change2.getChangeId());
 
-    RevCommit tip1 = getRemoteLog(project, "master").get(0);
-    assertThat(tip1.getShortMessage()).isEqualTo(
+    RevCommit headAfterSecondSubmit = getRemoteLog(project, "master").get(0);
+    assertThat(headAfterSecondSubmit.getShortMessage()).isEqualTo(
         change2.getCommit().getShortMessage());
 
     RevCommit tip2 = getRemoteLog(project, "branch").get(0);
@@ -254,21 +292,28 @@
         change3.getCommit().getShortMessage());
     assertThat(log3.get(1).getShortMessage()).isEqualTo(
         change2.getCommit().getShortMessage());
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
+        headAfterFirstSubmit, headAfterSecondSubmit);
+    assertChangeMergedEvents(change1.getChangeId(), headAfterFirstSubmit.name(),
+        change2.getChangeId(), headAfterSecondSubmit.name());
   }
 
   @Test
   public void submitWithOpenAncestorsOnOtherBranch() throws Exception {
-    PushOneCommit.Result change1 = createChange(testRepo,  "master",
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change1 = createChange(testRepo, "master",
         "base commit",
         "a.txt", "1", "");
     submit(change1.getChangeId());
+    RevCommit headAfterFirstSubmit = getRemoteHead();
 
     gApi.projects()
         .name(project.get())
         .branch("branch")
         .create(new BranchInput());
 
-    PushOneCommit.Result change2 = createChange(testRepo,  "master",
+    PushOneCommit.Result change2 = createChange(testRepo, "master",
         "We want to commit this to master first",
         "a.txt", "2", "");
 
@@ -282,23 +327,25 @@
     assertThat(tip2.getShortMessage()).isEqualTo(
         change1.getCommit().getShortMessage());
 
-    PushOneCommit.Result change3a = createChange(testRepo,  "branch",
+    PushOneCommit.Result change3a = createChange(testRepo, "branch",
         "This commit is based on change2 pending for master, "
         + "but is targeted itself at branch, which doesn't include it.",
         "a.txt", "3", "a-topic-here");
 
     Project.NameKey p3 = createProject("project-related-to-change3");
     TestRepository<?> repo3 = cloneProject(p3);
-    RevCommit initialHead = getRemoteHead(p3, "master");
+    RevCommit repo3Head = getRemoteHead(p3, "master");
     PushOneCommit.Result change3b = createChange(repo3, "master",
         "some accompanying changes for change3a in another repo "
         + "tied together via topic",
         "a.txt", "1", "a-topic-here");
     approve(change3b.getChangeId());
 
-    submitWithConflict(change3a.getChangeId(), "Cannot merge " +
-        change3a.getCommit().name() +
-        "\nMissing dependency");
+    String cnt = isSubmitWholeTopicEnabled() ? "2 changes" : "1 change";
+    submitWithConflict(change3a.getChangeId(),
+        "Failed to submit " + cnt + " due to the following problems:\n"
+        + "Change " + change3a.getChange().getId() + ": depends on change that"
+        + " was not submitted");
 
     RevCommit tipbranch = getRemoteLog(project, "branch").get(0);
     assertThat(tipbranch.getShortMessage()).isEqualTo(
@@ -306,6 +353,143 @@
 
     RevCommit tipmaster = getRemoteLog(p3, "master").get(0);
     assertThat(tipmaster.getShortMessage()).isEqualTo(
-        initialHead.getShortMessage());
+        repo3Head.getShortMessage());
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
+    assertChangeMergedEvents(change1.getChangeId(), headAfterFirstSubmit.name());
+  }
+
+  @Test
+  public void testGerritWorkflow() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+
+    // We'll setup a master and a stable branch.
+    // Then we create a change to be applied to master, which is
+    // then cherry picked back to stable. The stable branch will
+    // be merged up into master again.
+    gApi.projects()
+        .name(project.get())
+        .branch("stable")
+        .create(new BranchInput());
+
+    // Push a change to master
+    PushOneCommit push =
+        pushFactory.create(db, user.getIdent(), testRepo,
+            "small fix", "a.txt", "2");
+    PushOneCommit.Result change = push.to("refs/for/master");
+    submit(change.getChangeId());
+    RevCommit headAfterFirstSubmit = getRemoteLog(project, "master").get(0);
+    assertThat(headAfterFirstSubmit.getShortMessage()).isEqualTo(
+        change.getCommit().getShortMessage());
+
+    // Now cherry pick to stable
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "stable";
+    in.message = "This goes to stable as well\n"
+        + headAfterFirstSubmit.getFullMessage();
+    ChangeApi orig = gApi.changes()
+        .id(change.getChangeId());
+    String cherryId = orig.current().cherryPick(in).id();
+    gApi.changes().id(cherryId).current().review(ReviewInput.approve());
+    gApi.changes().id(cherryId).current().submit();
+
+    // Create the merge locally
+    RevCommit stable = getRemoteHead(project, "stable");
+    RevCommit master = getRemoteHead(project, "master");
+    testRepo.git().fetch().call();
+    testRepo.git()
+        .branchCreate()
+        .setName("stable")
+        .setStartPoint(stable)
+        .call();
+    testRepo.git()
+        .branchCreate()
+        .setName("master")
+        .setStartPoint(master)
+        .call();
+
+    RevCommit merge = testRepo.commit()
+        .parent(master)
+        .parent(stable)
+        .message("Merge stable into master")
+        .insertChangeId()
+        .create();
+
+    testRepo.branch("refs/heads/master").update(merge);
+    testRepo.git().push()
+        .setRefSpecs(new RefSpec("refs/heads/master:refs/for/master"))
+        .call();
+
+    String changeId = GitUtil.getChangeId(testRepo, merge).get();
+    approve(changeId);
+    submit(changeId);
+    RevCommit headAfterSecondSubmit = getRemoteLog(project, "master").get(0);
+    assertThat(headAfterSecondSubmit.getShortMessage())
+        .isEqualTo(merge.getShortMessage());
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
+        headAfterFirstSubmit, headAfterSecondSubmit);
+    assertChangeMergedEvents(
+        change.getChangeId(), headAfterFirstSubmit.name(),
+        changeId, headAfterSecondSubmit.name());
+  }
+
+  @Test
+  public void openChangeForTargetBranchPreventsMerge() throws Exception {
+    gApi.projects()
+        .name(project.get())
+        .branch("stable")
+        .create(new BranchInput());
+
+    // Propose a change for master, but leave it open for master!
+    PushOneCommit change =
+        pushFactory.create(db, user.getIdent(), testRepo,
+            "small fix", "a.txt", "2");
+    PushOneCommit.Result change2result = change.to("refs/for/master");
+
+    // Now cherry pick to stable
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "stable";
+    in.message = "it goes to stable branch";
+    ChangeApi orig = gApi.changes()
+        .id(change2result.getChangeId());
+    ChangeApi cherry = orig.current().cherryPick(in);
+    cherry.current().review(ReviewInput.approve());
+    cherry.current().submit();
+
+    // Create a commit locally
+    testRepo.git().fetch().setRefSpecs(new RefSpec("refs/heads/stable")).call();
+
+    PushOneCommit.Result change3 = createChange(testRepo, "stable",
+        "test","a.txt", "3", "");
+    submitWithConflict(change3.getChangeId(),
+        "Failed to submit 1 change due to the following problems:\n" +
+        "Change " + change3.getPatchSetId().getParentKey().get() +
+        ": depends on change that was not submitted");
+
+    assertRefUpdatedEvents();
+    assertChangeMergedEvents();
+  }
+
+  @Test
+  @TestProjectInput(createEmptyCommit = false)
+  public void mergeWithMissingChange() throws Exception {
+    // create a draft change
+    PushOneCommit.Result draftResult = createDraftChange();
+
+    // create a new change based on the draft change
+    PushOneCommit.Result changeResult = createChange();
+
+    // delete the draft change
+    gApi.changes().id(draftResult.getChangeId()).delete();
+
+    // approve and submit the change
+    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 222816e..aa5386f 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
@@ -17,14 +17,27 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.getChangeId;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.change.Submit.TestSubmitInput;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.Util;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -46,37 +59,119 @@
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
     RevCommit head = getRemoteHead();
-    assertThat(head.getId()).isEqualTo(change.getCommitId());
+    assertThat(head.getId()).isEqualTo(change.getCommit());
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertApproved(change.getChangeId());
     assertCurrentRevision(change.getChangeId(), 1, head);
     assertSubmitter(change.getChangeId(), 1);
     assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
     assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
+    assertRefUpdatedEvents(oldHead, head);
+    assertChangeMergedEvents(change.getChangeId(), head.name());
   }
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithRebase() throws Exception {
+    submitWithRebase(admin);
+  }
+
+  @Test
+  @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
+  public void submitWithRebaseWithoutAddPatchSetPermission() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    Util.block(cfg, Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/*");
+    Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*");
+    Util.allow(cfg, Permission.forLabel(Util.codeReview().getName()), -2, 2,
+        REGISTERED_USERS, "refs/heads/*");
+    saveProjectConfig(project, cfg);
+
+    submitWithRebase(user);
+  }
+
+  private void submitWithRebase(TestAccount submitter) throws Exception {
+    setApiUser(submitter);
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change =
         createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit headAfterFirstSubmit = getRemoteHead();
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 =
         createChange("Change 2", "b.txt", "other content");
     submit(change2.getChangeId());
     assertRebase(testRepo, false);
-    RevCommit head = getRemoteHead();
-    assertThat(head.getParent(0)).isEqualTo(oldHead);
+    RevCommit headAfterSecondSubmit = getRemoteHead();
+    assertThat(headAfterSecondSubmit.getParent(0))
+        .isEqualTo(headAfterFirstSubmit);
+    assertApproved(change2.getChangeId(), submitter);
+    assertCurrentRevision(change2.getChangeId(), 2, headAfterSecondSubmit);
+    assertSubmitter(change2.getChangeId(), 1, submitter);
+    assertSubmitter(change2.getChangeId(), 2, submitter);
+    assertPersonEquals(admin.getIdent(),
+        headAfterSecondSubmit.getAuthorIdent());
+    assertPersonEquals(submitter.getIdent(),
+        headAfterSecondSubmit.getCommitterIdent());
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
+        headAfterFirstSubmit, headAfterSecondSubmit);
+    assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(),
+        change2.getChangeId(), headAfterSecondSubmit.name());
+  }
+
+  @Test
+  public void submitWithRebaseMultipleChanges() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change1 =
+        createChange("Change 1", "a.txt", "content");
+    submit(change1.getChangeId());
+    RevCommit headAfterFirstSubmit = getRemoteHead();
+    assertThat(headAfterFirstSubmit.name())
+        .isEqualTo(change1.getCommit().name());
+
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change2 =
+        createChange("Change 2", "b.txt", "other content");
+    assertThat(change2.getCommit().getParent(0))
+        .isNotEqualTo(change1.getCommit());
+    PushOneCommit.Result change3 =
+        createChange("Change 3", "c.txt", "third content");
+    PushOneCommit.Result change4 =
+        createChange("Change 4", "d.txt", "fourth content");
+    approve(change2.getChangeId());
+    approve(change3.getChangeId());
+    submit(change4.getChangeId());
+
+    assertRebase(testRepo, false);
     assertApproved(change2.getChangeId());
-    assertCurrentRevision(change2.getChangeId(), 2, head);
-    assertSubmitter(change2.getChangeId(), 1);
-    assertSubmitter(change2.getChangeId(), 2);
-    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
+    assertApproved(change3.getChangeId());
+    assertApproved(change4.getChangeId());
+
+    RevCommit headAfterSecondSubmit = parse(getRemoteHead());
+    assertThat(headAfterSecondSubmit.getShortMessage()).isEqualTo("Change 4");
+    assertThat(headAfterSecondSubmit).isNotEqualTo(change4.getCommit());
+    assertCurrentRevision(change4.getChangeId(), 2, headAfterSecondSubmit);
+
+    RevCommit parent = parse(headAfterSecondSubmit.getParent(0));
+    assertThat(parent.getShortMessage()).isEqualTo("Change 3");
+    assertThat(parent).isNotEqualTo(change3.getCommit());
+    assertCurrentRevision(change3.getChangeId(), 2, parent);
+
+    RevCommit grandparent = parse(parent.getParent(0));
+    assertThat(grandparent).isNotEqualTo(change2.getCommit());
+    assertCurrentRevision(change2.getChangeId(), 2, grandparent);
+
+    RevCommit greatgrandparent = parse(grandparent.getParent(0));
+    assertThat(greatgrandparent).isEqualTo(change1.getCommit());
+    assertCurrentRevision(change1.getChangeId(), 1, greatgrandparent);
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
+        headAfterFirstSubmit, headAfterSecondSubmit);
+    assertChangeMergedEvents(change1.getChangeId(), headAfterFirstSubmit.name(),
+        change2.getChangeId(), headAfterSecondSubmit.name(),
+        change3.getChangeId(), headAfterSecondSubmit.name(),
+        change4.getChangeId(), headAfterSecondSubmit.name());
   }
 
   @Test
@@ -130,37 +225,38 @@
     assertThat(headGrandparent2.getId()).isEqualTo(change1.getCommit().getId());
   }
 
-  private RevCommit parse(ObjectId id) throws Exception {
-    try (Repository repo = repoManager.openRepository(project);
-        RevWalk rw = new RevWalk(repo)) {
-      RevCommit c = rw.parseCommit(id);
-      rw.parseBody(c);
-      return c;
-    }
-  }
-
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge() throws Exception {
+    RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change =
         createChange("Change 1", "a.txt", "aaa\nbbb\nccc\n");
     submit(change.getChangeId());
+    RevCommit headAfterFirstSubmit = getRemoteHead();
     PushOneCommit.Result change2 =
         createChange("Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
     submit(change2.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
-    testRepo.reset(change.getCommitId());
+    RevCommit headAfterSecondSubmit = getRemoteHead();
+    testRepo.reset(change.getCommit());
     PushOneCommit.Result change3 =
         createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
     assertRebase(testRepo, true);
-    RevCommit head = getRemoteHead();
-    assertThat(head.getParent(0)).isEqualTo(oldHead);
+    RevCommit headAfterThirdSubmit = getRemoteHead();
+    assertThat(headAfterThirdSubmit.getParent(0))
+        .isEqualTo(headAfterSecondSubmit);
     assertApproved(change3.getChangeId());
-    assertCurrentRevision(change3.getChangeId(), 2, head);
+    assertCurrentRevision(change3.getChangeId(), 2, headAfterThirdSubmit);
     assertSubmitter(change3.getChangeId(), 1);
     assertSubmitter(change3.getChangeId(), 2);
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
+        headAfterFirstSubmit, headAfterSecondSubmit,
+        headAfterSecondSubmit, headAfterThirdSubmit);
+    assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(),
+        change2.getChangeId(), headAfterSecondSubmit.name(),
+        change3.getChangeId(), headAfterThirdSubmit.name());
   }
 
   @Test
@@ -171,21 +267,102 @@
         createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit headAfterFirstSubmit = getRemoteHead();
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 =
         createChange("Change 2", "a.txt", "other content");
-    submitWithConflict(change2.getChangeId(), "Cannot rebase " +
-        change2.getCommit().name() +
-        ": The change could not be rebased due to a conflict during merge.");
+    submitWithConflict(change2.getChangeId(),
+        "Cannot rebase " + change2.getCommit().name()
+        + ": The change could not be rebased due to a conflict during merge.");
     RevCommit head = getRemoteHead();
-    assertThat(head).isEqualTo(oldHead);
-    assertCurrentRevision(change2.getChangeId(), 1, change2.getCommitId());
+    assertThat(head).isEqualTo(headAfterFirstSubmit);
+    assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit());
     assertNoSubmitter(change2.getChangeId(), 1);
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
+    assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name());
+  }
+
+  @Test
+  public void repairChangeStateAfterFailure() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange("Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit headAfterFirstSubmit = getRemoteHead();
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change2 =
+        createChange("Change 2", "b.txt", "other content");
+    Change.Id id2 = change2.getChange().getId();
+    SubmitInput failAfterRefUpdates =
+        new TestSubmitInput(new SubmitInput(), true);
+    submit(change2.getChangeId(), failAfterRefUpdates,
+        ResourceConflictException.class, "Failing after ref updates");
+    RevCommit headAfterFailedSubmit = getRemoteHead();
+
+    // Bad: ref advanced but change wasn't updated.
+    PatchSet.Id psId1 = new PatchSet.Id(id2, 1);
+    PatchSet.Id psId2 = new PatchSet.Id(id2, 2);
+    ChangeInfo info = gApi.changes().id(id2.get()).get();
+    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
+    assertThat(getPatchSet(psId2)).isNull();
+
+    ObjectId rev2;
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      ObjectId rev1 = repo.exactRef(psId1.toRefName()).getObjectId();
+      assertThat(rev1).isNotNull();
+
+      rev2 = repo.exactRef(psId2.toRefName()).getObjectId();
+      assertThat(rev2).isNotNull();
+      assertThat(rev2).isNotEqualTo(rev1);
+      assertThat(rw.parseCommit(rev2).getParent(0)).isEqualTo(headAfterFirstSubmit);
+
+      assertThat(repo.exactRef("refs/heads/master").getObjectId())
+          .isEqualTo(rev2);
+    }
+
+    submit(change2.getChangeId());
+    RevCommit headAfterSecondSubmit = getRemoteHead();
+    assertThat(headAfterSecondSubmit).isEqualTo(headAfterFailedSubmit);
+
+    // Change status and patch set entities were updated, and branch tip stayed
+    // the same.
+    info = gApi.changes().id(id2.get()).get();
+    assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+    assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(2);
+    PatchSet ps2 = getPatchSet(psId2);
+    assertThat(ps2).isNotNull();
+    assertThat(ps2.getRevision().get()).isEqualTo(rev2.name());
+    assertThat(Iterables.getLast(info.messages).message)
+        .isEqualTo("Change has been successfully rebased as "
+            + rev2.name() + " by Administrator");
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.exactRef("refs/heads/master").getObjectId())
+          .isEqualTo(rev2);
+    }
+
+    assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
+    assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(),
+        change2.getChangeId(), headAfterSecondSubmit.name());
+  }
+
+  private RevCommit parse(ObjectId id) throws Exception {
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      RevCommit c = rw.parseCommit(id);
+      rw.parseBody(c);
+      return c;
+    }
   }
 
   @Test
   public void submitAfterReorderOfCommits() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+
     // Create two commits and push.
     RevCommit c1 = commitBuilder()
         .add("a.txt", "1")
@@ -209,20 +386,32 @@
     approve(id1);
     approve(id2);
     submit(id1);
+    RevCommit headAfterSubmit = getRemoteHead();
+
+    assertRefUpdatedEvents(initialHead, headAfterSubmit);
+    assertChangeMergedEvents(id2, headAfterSubmit.name(),
+        id1, headAfterSubmit.name());
   }
 
   @Test
   public void submitChangesAfterBranchOnSecond() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+
     PushOneCommit.Result change = createChange();
     approve(change.getChangeId());
 
-    PushOneCommit.Result change2nd = createChange();
-    approve(change2nd.getChangeId());
-    Project.NameKey project = change2nd.getChange().change().getProject();
+    PushOneCommit.Result change2 = createChange();
+    approve(change2.getChangeId());
+    Project.NameKey project = change2.getChange().change().getProject();
     Branch.NameKey branch = new Branch.NameKey(project, "branch");
-    createBranchWithRevision(branch, change2nd.getCommit().getName());
-    gApi.changes().id(change2nd.getChangeId()).current().submit();
-    assertMerged(change2nd);
-    assertMerged(change);
+    createBranchWithRevision(branch, change2.getCommit().getName());
+    gApi.changes().id(change2.getChangeId()).current().submit();
+    assertMerged(change2.getChangeId());
+    assertMerged(change.getChangeId());
+
+    RevCommit newHead = getRemoteHead();
+    assertRefUpdatedEvents(initialHead, newHead);
+    assertChangeMergedEvents(change.getChangeId(), newHead.name(),
+        change2.getChangeId(), newHead.name());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
index 0fb5ce1..d5b6f14 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -92,10 +92,10 @@
     approve(g.getChangeId());
     approve(h.getChangeId());
 
-    assertMergeable(e.getChange(), true);
-    assertMergeable(f.getChange(), true);
-    assertMergeable(g.getChange(), false);
-    assertMergeable(h.getChange(), false);
+    assertMergeable(e.getChange());
+    assertMergeable(f.getChange());
+    assertNotMergeable(g.getChange());
+    assertNotMergeable(h.getChange());
 
     PushOneCommit.Result m = createChange("M", "new.txt", "Resolved conflict",
         ImmutableList.of(d.getCommit(), h.getCommit()));
@@ -103,7 +103,7 @@
 
     assertChangeSetMergeable(m.getChange(), true);
 
-    assertMergeable(m.getChange(), true);
+    assertMergeable(m.getChange());
     submit(m.getChangeId());
 
     assertMerged(e.getChangeId());
@@ -138,15 +138,15 @@
     PushOneCommit.Result g = createChange("G", "new.txt", "Conflicting line #2",
         ImmutableList.of(f.getCommit()));
 
-    assertMergeable(e.getChange(), true);
+    assertMergeable(e.getChange());
 
     approve(a.getChangeId());
     approve(b.getChangeId());
     submit(b.getChangeId());
 
-    assertMergeable(e.getChange(), false);
-    assertMergeable(f.getChange(), true);
-    assertMergeable(g.getChange(), true);
+    assertNotMergeable(e.getChange());
+    assertMergeable(f.getChange());
+    assertMergeable(g.getChange());
 
     approve(c.getChangeId());
     approve(d.getChangeId());
@@ -156,7 +156,7 @@
     approve(f.getChangeId());
     approve(g.getChangeId());
 
-    assertMergeable(g.getChange(), false);
+    assertNotMergeable(g.getChange());
     assertChangeSetMergeable(g.getChange(), false);
   }
 
@@ -278,7 +278,7 @@
     approve(c.getChangeId());
     approve(e.getChangeId());
     approve(d.getChangeId());
-    assertMergeable(d.getChange(), false);
+    assertNotMergeable(d.getChange());
     assertChangeSetMergeable(d.getChange(), false);
   }
 
@@ -289,21 +289,22 @@
         .submit();
   }
 
-  private void assertChangeSetMergeable(ChangeData change,
-      boolean expected) throws MissingObjectException,
-          IncorrectObjectTypeException, IOException, OrmException {
-    ChangeSet cs = mergeSuperSet.completeChangeSet(db, change.change());
-    if (expected) {
-      assertThat(submit.unmergeableChanges(cs)).isEmpty();
-    } else {
-      assertThat(submit.unmergeableChanges(cs)).isNotEmpty();
-    }
+  private void assertChangeSetMergeable(ChangeData change, boolean expected)
+      throws MissingObjectException, IncorrectObjectTypeException, IOException,
+      OrmException {
+    ChangeSet cs =
+        mergeSuperSet.completeChangeSet(db, change.change(), user(admin));
+    assertThat(submit.unmergeableChanges(cs).isEmpty()).isEqualTo(expected);
   }
 
-  private void assertMergeable(ChangeData change, boolean expected)
-      throws Exception {
+  private void assertMergeable(ChangeData change) throws Exception {
     change.setMergeable(null);
-    assertThat(change.isMergeable()).isEqualTo(expected);
+    assertThat(change.isMergeable()).isTrue();
+  }
+
+  private void assertNotMergeable(ChangeData change) throws Exception {
+    change.setMergeable(null);
+    assertThat(change.isMergeable()).isFalse();
   }
 
   private void assertMerged(String changeId) throws Exception {
@@ -347,7 +348,8 @@
         "refs/for/master");
   }
 
-  private PushOneCommit.Result createChange(String subject) throws Exception {
+  @Override
+  protected PushOneCommit.Result createChange(String subject) throws Exception {
     return createChange(testRepo, subject, "", "",
         Collections.<RevCommit> emptyList(), "refs/for/master");
   }
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 7d2930a..7fab6b1 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 
 import com.google.common.base.Function;
 import com.google.common.collect.FluentIterable;
@@ -22,6 +23,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GerritConfigs;
+import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupDescription;
@@ -41,6 +43,7 @@
 import java.util.Arrays;
 import java.util.List;
 
+@Sandboxed
 public class SuggestReviewersIT extends AbstractDaemonTest {
   @Inject
   private CreateGroup.Factory createGroupFactory;
@@ -70,7 +73,7 @@
   }
 
   @Test
-  @GerritConfig(name = "suggest.accounts", value = "false")
+  @GerritConfig(name = "accounts.visibility", value = "NONE")
   public void suggestReviewersNoResult1() throws Exception {
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers =
@@ -80,8 +83,7 @@
 
   @Test
   @GerritConfigs(
-      {@GerritConfig(name = "suggest.accounts", value = "true"),
-       @GerritConfig(name = "suggest.from", value = "1"),
+      {@GerritConfig(name = "suggest.from", value = "1"),
        @GerritConfig(name = "accounts.visibility", value = "NONE")
       })
   public void suggestReviewersNoResult2() throws Exception {
@@ -141,6 +143,18 @@
   }
 
   @Test
+  public void suggestReviewsPrivateProjectVisibility() throws Exception {
+    String changeId = createChange().getChangeId();
+    List<SuggestedReviewerInfo> reviewers;
+
+    setApiUser(user3);
+    block("read", ANONYMOUS_USERS, "refs/*");
+    allow("read", group1.getGroupUUID(), "refs/*");
+    reviewers = suggestReviewers(changeId, user2.username, 2);
+    assertThat(reviewers).isEmpty();
+  }
+
+  @Test
   @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
   public void suggestReviewersViewAllAccounts() throws Exception {
     String changeId = createChange().getChangeId();
@@ -169,71 +183,58 @@
   }
 
   @Test
-  @GerritConfig(name = "suggest.fullTextSearch", value = "true")
   public void suggestReviewersFullTextSearch() throws Exception {
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    reviewers = suggestReviewers(changeId, "first", 4);
+    reviewers = suggestReviewers(changeId, "first");
     assertThat(reviewers).hasSize(3);
 
-    reviewers = suggestReviewers(changeId, "first1", 2);
+    reviewers = suggestReviewers(changeId, "first1");
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, "last", 4);
+    reviewers = suggestReviewers(changeId, "last");
     assertThat(reviewers).hasSize(3);
 
-    reviewers = suggestReviewers(changeId, "last1", 2);
+    reviewers = suggestReviewers(changeId, "last1");
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, "fi la", 4);
+    reviewers = suggestReviewers(changeId, "fi la");
     assertThat(reviewers).hasSize(3);
 
-    reviewers = suggestReviewers(changeId, "la fi", 4);
+    reviewers = suggestReviewers(changeId, "la fi");
     assertThat(reviewers).hasSize(3);
 
-    reviewers = suggestReviewers(changeId, "first1 la", 2);
+    reviewers = suggestReviewers(changeId, "first1 la");
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, "fi last1", 2);
+    reviewers = suggestReviewers(changeId, "fi last1");
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, "first1 last2", 1);
+    reviewers = suggestReviewers(changeId, "first1 last2");
     assertThat(reviewers).hasSize(0);
 
-    reviewers = suggestReviewers(changeId, name("user"), 7);
+    reviewers = suggestReviewers(changeId, name("user"));
     assertThat(reviewers).hasSize(6);
 
-    reviewers = suggestReviewers(changeId, user1.username, 2);
+    reviewers = suggestReviewers(changeId, user1.username);
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, "example.com", 7);
+    reviewers = suggestReviewers(changeId, "example.com");
     assertThat(reviewers).hasSize(6);
 
-    reviewers = suggestReviewers(changeId, user1.email, 2);
+    reviewers = suggestReviewers(changeId, user1.email);
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, user1.username + " example", 2);
+    reviewers = suggestReviewers(changeId, user1.username + " example");
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, user4.email.toLowerCase(), 2);
+    reviewers = suggestReviewers(changeId, user4.email.toLowerCase());
     assertThat(reviewers).hasSize(1);
     assertThat(reviewers.get(0).account.email).isEqualTo(user4.email);
   }
 
   @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;
@@ -244,6 +245,53 @@
     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) throws Exception {
+    return gApi.changes()
+        .id(changeId)
+        .suggestReviewers(query)
+        .get();
+  }
+
   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
index edf55f7..f52fccd 100644
--- 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
@@ -14,13 +14,10 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.RestResponse;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 public class TopicIT extends AbstractDaemonTest {
@@ -28,16 +25,16 @@
   public void topic() throws Exception {
     Result result = createChange();
     String endpoint = "/changes/" + result.getChangeId() + "/topic";
-    RestResponse response = adminSession.put(endpoint, "topic");
-    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    RestResponse response = adminRestSession.put(endpoint, "topic");
+    response.assertOK();
 
-    response = adminSession.delete(endpoint);
-    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    response = adminRestSession.delete(endpoint);
+    response.assertNoContent();
 
-    response = adminSession.put(endpoint, "topic");
-    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    response = adminRestSession.put(endpoint, "topic");
+    response.assertOK();
 
-    response = adminSession.put(endpoint, "");
-    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    response = adminRestSession.put(endpoint, "");
+    response.assertNoContent();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK
index 0802e7c..d65b84a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK
@@ -1,7 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  group = 'rest-config',
+  group = 'rest_config',
   srcs = glob(['*IT.java']),
   labels = ['rest']
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUILD
new file mode 100644
index 0000000..b9d3ffb
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUILD
@@ -0,0 +1,7 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'rest_config',
+  srcs = glob(['*IT.java']),
+  labels = ['rest']
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
index 7e68a03..13f7070 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.config.ListCaches.CacheInfo;
 import com.google.gerrit.server.config.PostCaches;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 import java.util.Arrays;
@@ -34,83 +33,85 @@
 
   @Test
   public void flushAll() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/project_list");
+    RestResponse r = adminRestSession.get("/config/server/caches/project_list");
     CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isGreaterThan((long) 0);
 
-    r = adminSession.post("/config/server/caches/", new PostCaches.Input(FLUSH_ALL));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    r = adminRestSession.post("/config/server/caches/", new PostCaches.Input(FLUSH_ALL));
+    r.assertOK();
     r.consume();
 
-    r = adminSession.get("/config/server/caches/project_list");
+    r = adminRestSession.get("/config/server/caches/project_list");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isNull();
   }
 
   @Test
   public void flushAll_Forbidden() throws Exception {
-    RestResponse r = userSession.post("/config/server/caches/",
-        new PostCaches.Input(FLUSH_ALL));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    userRestSession.post("/config/server/caches/",
+        new PostCaches.Input(FLUSH_ALL)).assertForbidden();
   }
 
   @Test
   public void flushAll_BadRequest() throws Exception {
-    RestResponse r = adminSession.post("/config/server/caches/",
-        new PostCaches.Input(FLUSH_ALL, Arrays.asList("projects")));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+    adminRestSession
+        .post("/config/server/caches/",
+            new PostCaches.Input(FLUSH_ALL, Arrays.asList("projects")))
+        .assertBadRequest();
   }
 
   @Test
   public void flush() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/project_list");
+    RestResponse r = adminRestSession.get("/config/server/caches/project_list");
     CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isGreaterThan((long)0);
 
-    r = adminSession.get("/config/server/caches/projects");
+    r = adminRestSession.get("/config/server/caches/projects");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isGreaterThan((long)1);
 
-    r = adminSession.post("/config/server/caches/",
+    r = adminRestSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH, Arrays.asList("accounts", "project_list")));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    r.assertOK();
     r.consume();
 
-    r = adminSession.get("/config/server/caches/project_list");
+    r = adminRestSession.get("/config/server/caches/project_list");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isNull();
 
-    r = adminSession.get("/config/server/caches/projects");
+    r = adminRestSession.get("/config/server/caches/projects");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isGreaterThan((long)1);
   }
 
   @Test
   public void flush_Forbidden() throws Exception {
-    RestResponse r = userSession.post("/config/server/caches/",
-        new PostCaches.Input(FLUSH, Arrays.asList("projects")));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    userRestSession
+        .post("/config/server/caches/",
+            new PostCaches.Input(FLUSH, Arrays.asList("projects")))
+        .assertForbidden();
   }
 
   @Test
   public void flush_BadRequest() throws Exception {
-    RestResponse r = adminSession.post("/config/server/caches/",
-        new PostCaches.Input(FLUSH));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+    adminRestSession
+        .post("/config/server/caches/",
+            new PostCaches.Input(FLUSH))
+        .assertBadRequest();
   }
 
   @Test
   public void flush_UnprocessableEntity() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/projects");
+    RestResponse r = adminRestSession.get("/config/server/caches/projects");
     CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isGreaterThan((long)0);
 
-    r = adminSession.post("/config/server/caches/",
+    r = adminRestSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH, Arrays.asList("projects", "unprocessable")));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+    r.assertUnprocessableEntity();
     r.consume();
 
-    r = adminSession.get("/config/server/caches/projects");
+    r = adminRestSession.get("/config/server/caches/projects");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(cacheInfo.entries.mem).isGreaterThan((long)0);
   }
@@ -120,14 +121,15 @@
     allowGlobalCapabilities(REGISTERED_USERS,
         GlobalCapability.FLUSH_CACHES, GlobalCapability.VIEW_CACHES);
     try {
-      RestResponse r = userSession.post("/config/server/caches/",
+      RestResponse r = userRestSession.post("/config/server/caches/",
           new PostCaches.Input(FLUSH, Arrays.asList("projects")));
-      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+      r.assertOK();
       r.consume();
 
-      r = userSession.post("/config/server/caches/",
-          new PostCaches.Input(FLUSH, Arrays.asList("web_sessions")));
-      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+      userRestSession
+          .post("/config/server/caches/",
+              new PostCaches.Input(FLUSH, Arrays.asList("web_sessions")))
+          .assertForbidden();
     } finally {
       removeGlobalCapabilities(REGISTERED_USERS,
           GlobalCapability.FLUSH_CACHES, GlobalCapability.VIEW_CACHES);
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 e51ed08..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
@@ -14,17 +14,13 @@
 
 package com.google.gerrit.acceptance.rest.config;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.server.config.ConfirmEmail;
 import com.google.gerrit.server.mail.EmailTokenVerifier;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.inject.Inject;
 
-import org.apache.http.HttpStatus;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
@@ -44,31 +40,35 @@
   public void confirm() throws Exception {
     ConfirmEmail.Input in = new ConfirmEmail.Input();
     in.token = emailTokenVerifier.encode(admin.getId(), "new.mail@example.com");
-    RestResponse r = adminSession.put("/config/server/email.confirm", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    adminRestSession
+        .put("/config/server/email.confirm", in)
+        .assertNoContent();
   }
 
   @Test
   public void confirmForOtherUser_UnprocessableEntity() throws Exception {
     ConfirmEmail.Input in = new ConfirmEmail.Input();
     in.token = emailTokenVerifier.encode(user.getId(), "new.mail@example.com");
-    RestResponse r = adminSession.put("/config/server/email.confirm", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+    adminRestSession
+        .put("/config/server/email.confirm", in)
+        .assertUnprocessableEntity();
   }
 
   @Test
   public void confirmInvalidToken_UnprocessableEntity() throws Exception {
     ConfirmEmail.Input in = new ConfirmEmail.Input();
     in.token = "invalidToken";
-    RestResponse r = adminSession.put("/config/server/email.confirm", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+    adminRestSession
+        .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);
-    RestResponse r = adminSession.put("/config/server/email.confirm", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+    adminRestSession
+        .put("/config/server/email.confirm", in)
+        .assertUnprocessableEntity();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
index bb63928..149d05f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
@@ -22,48 +22,51 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.server.config.ListCaches.CacheInfo;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 public class FlushCacheIT extends AbstractDaemonTest {
 
   @Test
   public void flushCache() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/groups");
+    RestResponse r = adminRestSession.get("/config/server/caches/groups");
     CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(result.entries.mem).isGreaterThan((long)0);
 
-    r = adminSession.post("/config/server/caches/groups/flush");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    r = adminRestSession.post("/config/server/caches/groups/flush");
+    r.assertOK();
     r.consume();
 
-    r = adminSession.get("/config/server/caches/groups");
+    r = adminRestSession.get("/config/server/caches/groups");
     result = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(result.entries.mem).isNull();
   }
 
   @Test
   public void flushCache_Forbidden() throws Exception {
-    RestResponse r = userSession.post("/config/server/caches/accounts/flush");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    userRestSession
+        .post("/config/server/caches/accounts/flush")
+        .assertForbidden();
   }
 
   @Test
   public void flushCache_NotFound() throws Exception {
-    RestResponse r = adminSession.post("/config/server/caches/nonExisting/flush");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    adminRestSession
+        .post("/config/server/caches/nonExisting/flush")
+        .assertNotFound();
   }
 
   @Test
   public void flushCacheWithGerritPrefix() throws Exception {
-    RestResponse r = adminSession.post("/config/server/caches/gerrit-accounts/flush");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    adminRestSession
+        .post("/config/server/caches/gerrit-accounts/flush")
+        .assertOK();
   }
 
   @Test
   public void flushWebSessionsCache() throws Exception {
-    RestResponse r = adminSession.post("/config/server/caches/web_sessions/flush");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    adminRestSession
+        .post("/config/server/caches/web_sessions/flush")
+        .assertOK();
   }
 
   @Test
@@ -71,12 +74,13 @@
     allowGlobalCapabilities(REGISTERED_USERS,
         GlobalCapability.VIEW_CACHES, GlobalCapability.FLUSH_CACHES);
     try {
-      RestResponse r = userSession.post("/config/server/caches/accounts/flush");
-      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+      RestResponse r = userRestSession.post("/config/server/caches/accounts/flush");
+      r.assertOK();
       r.consume();
 
-      r = userSession.post("/config/server/caches/web_sessions/flush");
-      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+      userRestSession
+          .post("/config/server/caches/web_sessions/flush")
+          .assertForbidden();
     } finally {
       removeGlobalCapabilities(REGISTERED_USERS,
           GlobalCapability.VIEW_CACHES, GlobalCapability.FLUSH_CACHES);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
index 02a1b73..1a1ccd90 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
@@ -21,15 +21,14 @@
 import com.google.gerrit.server.config.ListCaches.CacheInfo;
 import com.google.gerrit.server.config.ListCaches.CacheType;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 public class GetCacheIT extends AbstractDaemonTest {
 
   @Test
   public void getCache() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/accounts");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    RestResponse r = adminRestSession.get("/config/server/caches/accounts");
+    r.assertOK();
     CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
 
     assertThat(result.name).isEqualTo("accounts");
@@ -43,28 +42,31 @@
     assertThat(result.hitRatio.mem).isAtMost(100);
     assertThat(result.hitRatio.disk).isNull();
 
-    userSession.get("/config/server/version").consume();
-    r = adminSession.get("/config/server/caches/accounts");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    userRestSession.get("/config/server/version").consume();
+    r = adminRestSession.get("/config/server/caches/accounts");
+    r.assertOK();
     result = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(result.entries.mem).isEqualTo(2);
   }
 
   @Test
   public void getCache_Forbidden() throws Exception {
-    RestResponse r = userSession.get("/config/server/caches/accounts");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    userRestSession
+        .get("/config/server/caches/accounts")
+        .assertForbidden();
   }
 
   @Test
   public void getCache_NotFound() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/nonExisting");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    adminRestSession
+        .get("/config/server/caches/nonExisting")
+        .assertNotFound();
   }
 
   @Test
   public void getCacheWithGerritPrefix() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/gerrit-accounts");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    adminRestSession
+        .get("/config/server/caches/gerrit-accounts")
+        .assertOK();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
index acd900c..1321650 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.config.ListTasks.TaskInfo;
 import com.google.gson.reflect.TypeToken;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 import java.util.List;
@@ -31,8 +30,8 @@
   @Test
   public void getTask() throws Exception {
     RestResponse r =
-        adminSession.get("/config/server/tasks/" + getLogFileCompressorTaskId());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+        adminRestSession.get("/config/server/tasks/" + getLogFileCompressorTaskId());
+    r.assertOK();
     TaskInfo info =
         newGson().fromJson(r.getReader(),
             new TypeToken<TaskInfo>() {}.getType());
@@ -44,13 +43,13 @@
 
   @Test
   public void getTask_NotFound() throws Exception {
-    RestResponse r =
-        userSession.get("/config/server/tasks/" + getLogFileCompressorTaskId());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    userRestSession
+        .get("/config/server/tasks/" + getLogFileCompressorTaskId())
+        .assertNotFound();
   }
 
   private String getLogFileCompressorTaskId() throws Exception {
-    RestResponse r = adminSession.get("/config/server/tasks/");
+    RestResponse r = adminRestSession.get("/config/server/tasks/");
     List<TaskInfo> result =
         newGson().fromJson(r.getReader(),
             new TypeToken<List<TaskInfo>>() {}.getType());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
index 11c61c6..306bb58 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.config.ListTasks.TaskInfo;
 import com.google.gson.reflect.TypeToken;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 import java.util.List;
@@ -29,18 +28,18 @@
 public class KillTaskIT extends AbstractDaemonTest {
 
   private void killTask() throws Exception {
-    RestResponse r = adminSession.get("/config/server/tasks/");
+    RestResponse r = adminRestSession.get("/config/server/tasks/");
     List<TaskInfo> result = newGson().fromJson(r.getReader(),
         new TypeToken<List<TaskInfo>>() {}.getType());
     r.consume();
     int taskCount = result.size();
     assertThat(taskCount).isGreaterThan(0);
 
-    r = adminSession.delete("/config/server/tasks/" + result.get(0).id);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    r = adminRestSession.delete("/config/server/tasks/" + result.get(0).id);
+    r.assertNoContent();
     r.consume();
 
-    r = adminSession.get("/config/server/tasks/");
+    r = adminRestSession.get("/config/server/tasks/");
     result = newGson().fromJson(r.getReader(),
         new TypeToken<List<TaskInfo>>() {}.getType());
     r.consume();
@@ -48,14 +47,15 @@
   }
 
   private void killTask_NotFound() throws Exception {
-    RestResponse r = adminSession.get("/config/server/tasks/");
+    RestResponse r = adminRestSession.get("/config/server/tasks/");
     List<TaskInfo> result = newGson().fromJson(r.getReader(),
         new TypeToken<List<TaskInfo>>() {}.getType());
     r.consume();
     assertThat(result.size()).isGreaterThan(0);
 
-    r = userSession.delete("/config/server/tasks/" + result.get(0).id);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    userRestSession
+        .delete("/config/server/tasks/" + result.get(0).id)
+        .assertNotFound();
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
index fb89e1b..d0a5070 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.config.ListCaches.CacheType;
 import com.google.gson.reflect.TypeToken;
 
-import org.apache.http.HttpStatus;
 import org.eclipse.jgit.util.Base64;
 import org.junit.Test;
 
@@ -36,8 +35,8 @@
 
   @Test
   public void listCaches() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    RestResponse r = adminRestSession.get("/config/server/caches/");
+    r.assertOK();
     Map<String, CacheInfo> result =
         newGson().fromJson(r.getReader(),
             new TypeToken<Map<String, CacheInfo>>() {}.getType());
@@ -54,9 +53,9 @@
     assertThat(accountsCacheInfo.hitRatio.mem).isAtMost(100);
     assertThat(accountsCacheInfo.hitRatio.disk).isNull();
 
-    userSession.get("/config/server/version").consume();
-    r = adminSession.get("/config/server/caches/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    userRestSession.get("/config/server/version").consume();
+    r = adminRestSession.get("/config/server/caches/");
+    r.assertOK();
     result = newGson().fromJson(r.getReader(),
         new TypeToken<Map<String, CacheInfo>>() {}.getType());
     assertThat(result.get("accounts").entries.mem).isEqualTo(2);
@@ -64,14 +63,15 @@
 
   @Test
   public void listCaches_Forbidden() throws Exception {
-    RestResponse r = userSession.get("/config/server/caches/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    userRestSession
+        .get("/config/server/caches/")
+        .assertForbidden();
   }
 
   @Test
   public void listCacheNames() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/?format=LIST");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    RestResponse r = adminRestSession.get("/config/server/caches/?format=LIST");
+    r.assertOK();
     List<String> result =
         newGson().fromJson(r.getReader(),
             new TypeToken<List<String>>() {}.getType());
@@ -82,8 +82,8 @@
 
   @Test
   public void listCacheNamesTextList() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/?format=TEXT_LIST");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    RestResponse r = adminRestSession.get("/config/server/caches/?format=TEXT_LIST");
+    r.assertOK();
     String result = new String(Base64.decode(r.getEntityContent()), UTF_8.name());
     List<String> list = Arrays.asList(result.split("\n"));
     assertThat(list).contains("accounts");
@@ -93,7 +93,8 @@
 
   @Test
   public void listCaches_BadRequest() throws Exception {
-    RestResponse r = adminSession.get("/config/server/caches/?format=NONSENSE");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+    adminRestSession
+        .get("/config/server/caches/?format=NONSENSE")
+        .assertBadRequest();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
index 58f3361..c405ff2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.config.ListTasks.TaskInfo;
 import com.google.gson.reflect.TypeToken;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 import java.util.List;
@@ -30,8 +29,8 @@
 
   @Test
   public void listTasks() throws Exception {
-    RestResponse r = adminSession.get("/config/server/tasks/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    RestResponse r = adminRestSession.get("/config/server/tasks/");
+    r.assertOK();
     List<TaskInfo> result =
         newGson().fromJson(r.getReader(),
             new TypeToken<List<TaskInfo>>() {}.getType());
@@ -51,8 +50,8 @@
 
   @Test
   public void listTasksWithoutViewQueueCapability() throws Exception {
-    RestResponse r = userSession.get("/config/server/tasks/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    RestResponse r = userRestSession.get("/config/server/tasks/");
+    r.assertOK();
     List<TaskInfo> result =
         newGson().fromJson(r.getReader(),
             new TypeToken<List<TaskInfo>>() {}.getType());
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 a656760..54fa74c 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
@@ -29,6 +30,9 @@
 
 import org.junit.Test;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
+
 public class ServerInfoIT extends AbstractDaemonTest {
 
   @Test
@@ -70,8 +74,7 @@
     @GerritConfig(name = "user.anonymousCoward", value = "Unnamed User"),
   })
   public void serverConfig() throws Exception {
-    RestResponse r = adminSession.get("/config/server/info/");
-    ServerInfo i = newGson().fromJson(r.getReader(), ServerInfo.class);
+    ServerInfo i = getServerConfig();
 
     // auth
     assertThat(i.auth.authType).isEqualTo(AuthType.HTTP);
@@ -104,8 +107,8 @@
     assertThat(i.gerrit.reportBugUrl).isEqualTo("https://example.com/report");
     assertThat(i.gerrit.reportBugText).isEqualTo("REPORT BUG");
 
-    // gitweb
-    assertThat(i.gitweb).isNull();
+    // plugin
+    assertThat(i.plugin.jsResourcePaths).isEmpty();
 
     // sshd
     assertThat(i.sshd).isNotNull();
@@ -115,12 +118,31 @@
 
     // user
     assertThat(i.user.anonymousCowardName).isEqualTo("Unnamed User");
+
+    // notedb
+    notesMigration.setReadChanges(true);
+    assertThat(getServerConfig().noteDbEnabled).isTrue();
+    notesMigration.setReadChanges(false);
+    assertThat(getServerConfig().noteDbEnabled).isNull();
+  }
+
+  @Test
+  @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true")
+  public void serverConfigWithPlugin() throws Exception {
+    Path plugins = tempSiteDir.newFolder("plugins").toPath();
+    Path jsplugin = plugins.resolve("js-plugin-1.js");
+    Files.write(jsplugin, "Gerrit.install(function(self){});\n".getBytes(UTF_8));
+    adminSshSession.exec("gerrit plugin reload");
+
+    ServerInfo i = getServerConfig();
+
+    // plugin
+    assertThat(i.plugin.jsResourcePaths).hasSize(1);
   }
 
   @Test
   public void serverConfigWithDefaults() throws Exception {
-    RestResponse r = adminSession.get("/config/server/info/");
-    ServerInfo i = newGson().fromJson(r.getReader(), ServerInfo.class);
+    ServerInfo i = getServerConfig();
 
     // auth
     assertThat(i.auth.authType).isEqualTo(AuthType.OPENID);
@@ -154,8 +176,8 @@
     assertThat(i.gerrit.reportBugUrl).isNull();
     assertThat(i.gerrit.reportBugText).isNull();
 
-    // gitweb
-    assertThat(i.gitweb).isNull();
+    // plugin
+    assertThat(i.plugin.jsResourcePaths).isEmpty();
 
     // sshd
     assertThat(i.sshd).isNotNull();
@@ -166,4 +188,10 @@
     // user
     assertThat(i.user.anonymousCowardName).isEqualTo(AnonymousCowardNameProvider.DEFAULT);
   }
+
+  private ServerInfo getServerConfig() throws Exception {
+    RestResponse r = adminRestSession.get("/config/server/info/");
+    r.assertOK();
+    return newGson().fromJson(r.getReader(), ServerInfo.class);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java
index 242e1ee..e37567c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java
@@ -14,19 +14,15 @@
 
 package com.google.gerrit.acceptance.rest.group;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 public class AddMemberIT extends AbstractDaemonTest {
   @Test
   public void addNonExistingMember_NotFound() throws Exception {
-    int status =
-        adminSession.put("/groups/Administrators/members/non-existing")
-            .getStatusCode();
-    assertThat(status).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    adminRestSession
+        .put("/groups/Administrators/members/non-existing")
+        .assertNotFound();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
index d991417..1947148 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
@@ -1,7 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  group = 'rest-group',
+  group = 'rest_group',
   srcs = glob(['*IT.java']),
   labels = ['rest']
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUILD
new file mode 100644
index 0000000..d9a400c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUILD
@@ -0,0 +1,8 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'rest_group',
+  srcs = glob(['*IT.java']),
+  labels = ['rest']
+)
+
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
new file mode 100644
index 0000000..c78b291
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -0,0 +1,493 @@
+// 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.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.access.AccessSectionInfo;
+import com.google.gerrit.extensions.api.access.PermissionInfo;
+import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
+import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
+import com.google.gerrit.extensions.api.access.ProjectAccessInput;
+import com.google.gerrit.extensions.api.projects.ProjectApi;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+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;
+
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.HashMap;
+
+public class AccessIT extends AbstractDaemonTest {
+
+  private final String PROJECT_NAME = "newProject";
+
+  private final String REFS_ALL = Constants.R_REFS + "*";
+  private final String REFS_HEADS = Constants.R_HEADS + "*";
+
+  private final String LABEL_CODE_REVIEW = "Code-Review";
+
+  private String newProjectName;
+  private ProjectApi pApi;
+
+  @Before
+  public void setUp() throws Exception  {
+    newProjectName = createProject(PROJECT_NAME).get();
+    pApi = gApi.projects().name(newProjectName);
+  }
+
+  @Test
+  public void getDefaultInheritance() throws Exception {
+    String inheritedName = pApi.access().inheritsFrom.name;
+    assertThat(inheritedName).isEqualTo(AllProjectsNameProvider.DEFAULT);
+  }
+
+  @Test
+  public void addAccessSection() throws Exception {
+    Project.NameKey p = new Project.NameKey(newProjectName);
+    RevCommit initialHead = getRemoteHead(p, RefNames.REFS_CONFIG);
+
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+    accessInput.add.put(REFS_HEADS, accessSectionInfo);
+    pApi.access(accessInput);
+
+    assertThat(pApi.access().local).isEqualTo(accessInput.add);
+
+    RevCommit updatedHead = getRemoteHead(p, RefNames.REFS_CONFIG);
+    eventRecorder.assertRefUpdatedEvents(p.get(), RefNames.REFS_CONFIG,
+        null, initialHead,
+        initialHead, updatedHead);
+  }
+
+  @Test
+  public void removePermission() throws Exception {
+    // Add initial permission set
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+    accessInput.add.put(REFS_HEADS, accessSectionInfo);
+    pApi.access(accessInput);
+
+    // Remove specific permission
+    AccessSectionInfo accessSectionToRemove = newAccessSectionInfo();
+    accessSectionToRemove.permissions
+        .put(Permission.LABEL + LABEL_CODE_REVIEW, newPermissionInfo());
+    ProjectAccessInput removal = newProjectAccessInput();
+    removal.remove.put(REFS_HEADS, accessSectionToRemove);
+    pApi.access(removal);
+
+    // Remove locally
+    accessInput.add.get(REFS_HEADS).permissions
+        .remove(Permission.LABEL + LABEL_CODE_REVIEW);
+
+    // Check
+    assertThat(pApi.access().local).isEqualTo(accessInput.add);
+  }
+
+  @Test
+  public void removePermissionRule() throws Exception {
+    // Add initial permission set
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+    accessInput.add.put(REFS_HEADS, accessSectionInfo);
+    pApi.access(accessInput);
+
+    // Remove specific permission rule
+    AccessSectionInfo accessSectionToRemove = newAccessSectionInfo();
+    PermissionInfo codeReview = newPermissionInfo();
+    codeReview.label = LABEL_CODE_REVIEW;
+    PermissionRuleInfo pri = new PermissionRuleInfo(
+        PermissionRuleInfo.Action.DENY, false);
+    codeReview.rules.put(
+        SystemGroupBackend.REGISTERED_USERS.get(), pri);
+    accessSectionToRemove.permissions
+        .put(Permission.LABEL +LABEL_CODE_REVIEW, codeReview);
+    ProjectAccessInput removal = newProjectAccessInput();
+    removal.remove.put(REFS_HEADS, accessSectionToRemove);
+    pApi.access(removal);
+
+    // Remove locally
+    accessInput.add.get(REFS_HEADS).permissions
+        .get(Permission.LABEL + LABEL_CODE_REVIEW)
+        .rules.remove(SystemGroupBackend.REGISTERED_USERS.get());
+
+    // Check
+    assertThat(pApi.access().local).isEqualTo(accessInput.add);
+  }
+
+  @Test
+  public void removePermissionRulesAndCleanupEmptyEntries() throws Exception {
+    // Add initial permission set
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+    accessInput.add.put(REFS_HEADS, accessSectionInfo);
+    pApi.access(accessInput);
+
+    // Remove specific permission rules
+    AccessSectionInfo accessSectionToRemove = newAccessSectionInfo();
+    PermissionInfo codeReview = newPermissionInfo();
+    codeReview.label = LABEL_CODE_REVIEW;
+    PermissionRuleInfo pri = new PermissionRuleInfo(
+        PermissionRuleInfo.Action.DENY, false);
+    codeReview.rules.put(
+        SystemGroupBackend.REGISTERED_USERS.get(), pri);
+    pri = new PermissionRuleInfo(
+        PermissionRuleInfo.Action.DENY, false);
+    codeReview.rules.put(
+        SystemGroupBackend.PROJECT_OWNERS.get(), pri);
+    accessSectionToRemove.permissions
+        .put(Permission.LABEL +LABEL_CODE_REVIEW, codeReview);
+    ProjectAccessInput removal = newProjectAccessInput();
+    removal.remove.put(REFS_HEADS, accessSectionToRemove);
+    pApi.access(removal);
+
+    // Remove locally
+    accessInput.add.get(REFS_HEADS)
+        .permissions.remove(Permission.LABEL + LABEL_CODE_REVIEW);
+
+    // Check
+    assertThat(pApi.access().local).isEqualTo(accessInput.add);
+  }
+
+  @Test
+  public void getPermissionsWithDisallowedUser() throws Exception {
+    // Add initial permission set
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo = createAccessSectionInfoDenyAll();
+
+    // Disallow READ
+    accessInput.add.put(REFS_ALL, accessSectionInfo);
+    pApi.access(accessInput);
+
+    setApiUser(user);
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name(newProjectName).access();
+  }
+
+  @Test
+  public void setPermissionsWithDisallowedUser() throws Exception {
+    // Add initial permission set
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo = createAccessSectionInfoDenyAll();
+
+    // Disallow READ
+    accessInput.add.put(REFS_ALL, accessSectionInfo);
+    pApi.access(accessInput);
+
+    // Create a change to apply
+    ProjectAccessInput accessInfoToApply = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfoToApply =
+        createDefaultAccessSectionInfo();
+    accessInfoToApply.add.put(REFS_HEADS, accessSectionInfoToApply);
+
+    setApiUser(user);
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name(newProjectName).access();
+  }
+
+  @Test
+  public void updateParentAsUser() throws Exception {
+    // Create child
+    String newParentProjectName = createProject(PROJECT_NAME + "PA").get();
+
+    // Set new parent
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    accessInput.parent = newParentProjectName;
+
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage("not administrator");
+    gApi.projects().name(newProjectName).access(accessInput);
+  }
+
+  @Test
+  public void updateParentAsAdministrator() throws Exception {
+    // Create parent
+    String newParentProjectName = createProject(PROJECT_NAME + "PA").get();
+
+    // Set new parent
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    accessInput.parent = newParentProjectName;
+
+    gApi.projects().name(newProjectName).access(accessInput);
+
+    assertThat(pApi.access().inheritsFrom.name).isEqualTo(newParentProjectName);
+  }
+
+  @Test
+  public void addGlobalCapabilityAsUser() throws Exception {
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo =
+        createDefaultGlobalCapabilitiesAccessSectionInfo();
+
+    accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES,
+        accessSectionInfo);
+
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    gApi.projects().name(allProjects.get()).access(accessInput);
+  }
+
+  @Test
+  public void addGlobalCapabilityAsAdmin() throws Exception {
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo =
+        createDefaultGlobalCapabilitiesAccessSectionInfo();
+
+    accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES,
+        accessSectionInfo);
+
+    ProjectAccessInfo updatedAccessSectionInfo =
+        gApi.projects().name(allProjects.get()).access(accessInput);
+    assertThat(updatedAccessSectionInfo.local.get(
+        AccessSection.GLOBAL_CAPABILITIES).permissions.keySet())
+        .containsAllIn(accessSectionInfo.permissions.keySet());
+  }
+
+  @Test
+  public void addGlobalCapabilityForNonRootProject() throws Exception {
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo =
+        createDefaultGlobalCapabilitiesAccessSectionInfo();
+
+    accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES,
+        accessSectionInfo);
+
+    exception.expect(BadRequestException.class);
+    pApi.access(accessInput);
+  }
+
+  @Test
+  public void addNonGlobalCapabilityToGlobalCapabilities() throws Exception {
+    AccountGroup adminGroup =
+        groupCache.get(new AccountGroup.NameKey("Administrators"));
+
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
+
+    PermissionInfo permissionInfo = newPermissionInfo();
+    permissionInfo.rules.put(
+        adminGroup.getGroupUUID().get(), null);
+    accessSectionInfo.permissions.put(Permission.PUSH,
+        permissionInfo);
+
+    accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES,
+        accessSectionInfo);
+
+    exception.expect(BadRequestException.class);
+    gApi.projects().name(allProjects.get()).access(accessInput);
+  }
+
+  @Test
+  public void removeGlobalCapabilityAsUser() throws Exception {
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo =
+        createDefaultGlobalCapabilitiesAccessSectionInfo();
+
+    accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES,
+        accessSectionInfo);
+
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    gApi.projects().name(allProjects.get()).access(accessInput);
+  }
+
+  @Test
+  public void removeGlobalCapabilityAsAdmin() throws Exception {
+    AccountGroup adminGroup =
+        groupCache.get(new AccountGroup.NameKey("Administrators"));
+
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
+
+    PermissionInfo permissionInfo = newPermissionInfo();
+    permissionInfo.rules.put(
+        adminGroup.getGroupUUID().get(), null);
+    accessSectionInfo.permissions.put(GlobalCapability.ACCESS_DATABASE,
+        permissionInfo);
+
+    // Add and validate first as removing existing privileges such as
+    // administrateServer would break upcoming tests
+    accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES,
+        accessSectionInfo);
+
+    ProjectAccessInfo updatedProjectAccessInfo =
+        gApi.projects().name(allProjects.get()).access(accessInput);
+    assertThat(updatedProjectAccessInfo.local.get(
+        AccessSection.GLOBAL_CAPABILITIES).permissions.keySet())
+        .containsAllIn(accessSectionInfo.permissions.keySet());
+
+    // Remove
+    accessInput.add.clear();
+    accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES,
+        accessSectionInfo);
+
+    updatedProjectAccessInfo =
+        gApi.projects().name(allProjects.get()).access(accessInput);
+    assertThat(updatedProjectAccessInfo.local.get(
+        AccessSection.GLOBAL_CAPABILITIES).permissions.keySet())
+        .containsNoneIn(accessSectionInfo.permissions.keySet());
+  }
+
+  @Test
+  public void unknownPermissionRemainsUnchanged() throws Exception {
+    String access = "access";
+    String unknownPermission = "unknownPermission";
+    String registeredUsers = "group Registered Users";
+    String refsFor = "refs/for/*";
+    // Clone repository to forcefully add permission
+    TestRepository<InMemoryRepository> allProjectsRepo =
+        cloneProject(allProjects, admin);
+
+    // Fetch permission ref
+    GitUtil.fetch(allProjectsRepo, "refs/meta/config:cfg");
+    allProjectsRepo.reset("cfg");
+
+    // Load current permissions
+    String config = gApi.projects()
+        .name(allProjects.get())
+        .branch(RefNames.REFS_CONFIG).file("project.config").asString();
+
+    // Append and push unknown permission
+    Config cfg = new Config();
+    cfg.fromText(config);
+    cfg.setString(access, refsFor, unknownPermission, registeredUsers);
+    config = cfg.toText();
+    PushOneCommit push = pushFactory.create(
+        db, admin.getIdent(), allProjectsRepo, "Subject", "project.config",
+        config);
+    push.to(RefNames.REFS_CONFIG).assertOkStatus();
+
+    // Verify that unknownPermission is present
+    config = gApi.projects()
+        .name(allProjects.get())
+        .branch(RefNames.REFS_CONFIG).file("project.config").asString();
+    cfg.fromText(config);
+    assertThat(cfg.getString(access, refsFor, unknownPermission))
+        .isEqualTo(registeredUsers);
+
+    // Make permission change through API
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+    accessInput.add.put(refsFor, accessSectionInfo);
+    gApi.projects().name(allProjects.get()).access(accessInput);
+    accessInput.add.clear();
+    accessInput.remove.put(refsFor, accessSectionInfo);
+    gApi.projects().name(allProjects.get()).access(accessInput);
+
+    // Verify that unknownPermission is still present
+    config = gApi.projects()
+        .name(allProjects.get())
+        .branch(RefNames.REFS_CONFIG).file("project.config").asString();
+    cfg.fromText(config);
+    assertThat(cfg.getString(access, refsFor, unknownPermission))
+        .isEqualTo(registeredUsers);
+  }
+
+  private ProjectAccessInput newProjectAccessInput() {
+    ProjectAccessInput p = new ProjectAccessInput();
+    p.add = new HashMap<>();
+    p.remove = new HashMap<>();
+    return p;
+  }
+
+  private PermissionInfo newPermissionInfo() {
+    PermissionInfo p = new PermissionInfo(null, null);
+    p.rules = new HashMap<>();
+    return p;
+  }
+
+  private AccessSectionInfo newAccessSectionInfo() {
+    AccessSectionInfo a = new AccessSectionInfo();
+    a.permissions = new HashMap<>();
+    return a;
+  }
+
+  private AccessSectionInfo createDefaultAccessSectionInfo() {
+    AccessSectionInfo accessSection = newAccessSectionInfo();
+
+    PermissionInfo push = newPermissionInfo();
+    PermissionRuleInfo pri = new PermissionRuleInfo(
+        PermissionRuleInfo.Action.ALLOW, false);
+    push.rules.put(
+        SystemGroupBackend.REGISTERED_USERS.get(), pri);
+    accessSection.permissions.put(Permission.PUSH, push);
+
+    PermissionInfo codeReview = newPermissionInfo();
+    codeReview.label = LABEL_CODE_REVIEW;
+    pri = new PermissionRuleInfo(
+        PermissionRuleInfo.Action.DENY, false);
+    codeReview.rules.put(
+        SystemGroupBackend.REGISTERED_USERS.get(), pri);
+
+    pri = new PermissionRuleInfo(
+        PermissionRuleInfo.Action.ALLOW, false);
+    pri.max = 1;
+    pri.min = -1;
+    codeReview.rules.put(
+        SystemGroupBackend.PROJECT_OWNERS.get(), pri);
+    accessSection.permissions.put(Permission.LABEL
+        + LABEL_CODE_REVIEW, codeReview);
+
+    return accessSection;
+  }
+
+
+  private AccessSectionInfo createDefaultGlobalCapabilitiesAccessSectionInfo() {
+    AccessSectionInfo accessSection = newAccessSectionInfo();
+
+    PermissionInfo email = newPermissionInfo();
+    PermissionRuleInfo pri = new PermissionRuleInfo(
+        PermissionRuleInfo.Action.ALLOW, false);
+    email.rules.put(
+        SystemGroupBackend.REGISTERED_USERS.get(), pri);
+    accessSection.permissions.put(GlobalCapability.EMAIL_REVIEWERS, email);
+
+    return accessSection;
+  }
+
+  private AccessSectionInfo createAccessSectionInfoDenyAll() {
+    AccessSectionInfo accessSection = newAccessSectionInfo();
+
+    PermissionInfo read = newPermissionInfo();
+    PermissionRuleInfo pri = new PermissionRuleInfo(
+        PermissionRuleInfo.Action.DENY, false);
+    read.rules.put(
+        SystemGroupBackend.ANONYMOUS_USERS.get(), pri);
+    accessSection.permissions.put(Permission.READ, read);
+
+    return accessSection;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
index c1618fb..d53e69a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
@@ -1,13 +1,13 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  group = 'rest-project',
+  group = 'rest_project',
   srcs = glob(['*IT.java']),
   deps = [
     ':branch',
     ':project',
   ],
-  labels = ['rest']
+  labels = ['rest'],
 )
 
 java_library(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD
new file mode 100644
index 0000000..579171f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD
@@ -0,0 +1,37 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'rest_project',
+  srcs = glob(['*IT.java']),
+  deps = [
+    ':branch',
+    ':project',
+  ],
+  labels = ['rest'],
+)
+
+java_library(
+  name = 'branch',
+  srcs = [
+    'BranchAssert.java',
+  ],
+  deps = [
+    '//lib:truth',
+    '//gerrit-extension-api:api',
+    '//gerrit-server:server',
+  ],
+)
+
+java_library(
+  name = 'project',
+  srcs = [
+    'ProjectAssert.java',
+  ],
+  deps = [
+    '//gerrit-extension-api:api',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//lib:gwtorm',
+    '//lib:truth',
+  ],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
index 682059c..23fe562 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static org.eclipse.jgit.transport.RemoteRefUpdate.Status.REJECTED_OTHER_REASON;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -23,9 +24,8 @@
 import com.google.gerrit.server.project.BanCommit;
 import com.google.gerrit.server.project.BanCommit.BanResultInfo;
 
-import org.apache.http.HttpStatus;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.junit.Test;
 
 public class BanCommitIT extends AbstractDaemonTest {
@@ -37,29 +37,31 @@
         .create();
 
     RestResponse r =
-        adminSession.put("/projects/" + project.get() + "/ban/",
+        adminRestSession.put("/projects/" + project.get() + "/ban/",
             BanCommit.Input.fromCommits(c.name()));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    r.assertOK();
     BanResultInfo info = newGson().fromJson(r.getReader(), BanResultInfo.class);
     assertThat(Iterables.getOnlyElement(info.newlyBanned)).isEqualTo(c.name());
     assertThat(info.alreadyBanned).isNull();
     assertThat(info.ignored).isNull();
 
-    PushResult pushResult = pushHead(testRepo, "refs/heads/master", false);
-    assertThat(pushResult.getRemoteUpdate("refs/heads/master").getMessage())
-        .startsWith("contains banned commit");
+    RemoteRefUpdate u = pushHead(testRepo, "refs/heads/master", false)
+        .getRemoteUpdate("refs/heads/master");
+    assertThat(u).isNotNull();
+    assertThat(u.getStatus()).isEqualTo(REJECTED_OTHER_REASON);
+    assertThat(u.getMessage()).startsWith("contains banned commit");
   }
 
   @Test
   public void banAlreadyBannedCommit() throws Exception {
     RestResponse r =
-        adminSession.put("/projects/" + project.get() + "/ban/",
+        adminRestSession.put("/projects/" + project.get() + "/ban/",
             BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
     r.consume();
 
-    r = adminSession.put("/projects/" + project.get() + "/ban/",
+    r = adminRestSession.put("/projects/" + project.get() + "/ban/",
         BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    r.assertOK();
     BanResultInfo info = newGson().fromJson(r.getReader(), BanResultInfo.class);
     assertThat(Iterables.getOnlyElement(info.alreadyBanned))
       .isEqualTo("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96");
@@ -69,9 +71,9 @@
 
   @Test
   public void banCommit_Forbidden() throws Exception {
-    RestResponse r =
-        userSession.put("/projects/" + project.get() + "/ban/",
-            BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    userRestSession
+        .put("/projects/" + project.get() + "/ban/", BanCommit.Input.fromCommits(
+            "a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"))
+        .assertForbidden();
   }
 }
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/CreateBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 62dd729..46f93b6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -17,7 +17,6 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.block;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -29,7 +28,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.server.git.ProjectConfig;
 
 import org.eclipse.jgit.lib.Constants;
 import org.junit.Before;
@@ -84,9 +82,7 @@
   }
 
   private void blockCreateReference() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    block(cfg, Permission.CREATE, ANONYMOUS_USERS, "refs/*");
-    saveProjectConfig(project, cfg);
+    block(Permission.CREATE, ANONYMOUS_USERS, "refs/*");
   }
 
   private void grantOwner() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index d14ea44..d09eeec 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -40,9 +40,7 @@
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectState;
 
-import org.apache.http.HttpStatus;
 import org.apache.http.message.BasicHeader;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -50,7 +48,6 @@
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.Collections;
 import java.util.Set;
 
@@ -58,8 +55,8 @@
   @Test
   public void testCreateProjectHttp() throws Exception {
     String newProjectName = name("newProject");
-    RestResponse r = adminSession.put("/projects/" + newProjectName);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
+    RestResponse r = adminRestSession.put("/projects/" + newProjectName);
+    r.assertCreated();
     ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
     assertThat(p.name).isEqualTo(newProjectName);
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
@@ -71,32 +68,36 @@
   @Test
   public void testCreateProjectHttpWhenProjectAlreadyExists_Conflict()
       throws Exception {
-    RestResponse r = adminSession.put("/projects/" + allProjects.get());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+    adminRestSession
+        .put("/projects/" + allProjects.get())
+        .assertConflict();
   }
 
   @Test
   public void testCreateProjectHttpWhenProjectAlreadyExists_PreconditionFailed()
       throws Exception {
-    RestResponse r = adminSession.putWithHeader("/projects/" + allProjects.get(),
-        new BasicHeader(HttpHeaders.IF_NONE_MATCH, "*"));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_PRECONDITION_FAILED);
+    adminRestSession
+        .putWithHeader("/projects/" + allProjects.get(),
+            new BasicHeader(HttpHeaders.IF_NONE_MATCH, "*"))
+        .assertPreconditionFailed();
   }
 
   @Test
   @UseLocalDisk
   public void testCreateProjectHttpWithUnreasonableName_BadRequest()
       throws Exception {
-    RestResponse r = adminSession.put("/projects/" + Url.encode(name("invalid/../name")));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+    adminRestSession
+        .put("/projects/" + Url.encode(name("invalid/../name")))
+        .assertBadRequest();
   }
 
   @Test
   public void testCreateProjectHttpWithNameMismatch_BadRequest() throws Exception {
     ProjectInput in = new ProjectInput();
     in.name = name("otherName");
-    RestResponse r = adminSession.put("/projects/" + name("someName"), in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+    adminRestSession
+        .put("/projects/" + name("someName"), in)
+        .assertBadRequest();
   }
 
   @Test
@@ -104,8 +105,9 @@
       throws Exception {
     ProjectInput in = new ProjectInput();
     in.branches = Collections.singletonList(name("invalid ref name"));
-    RestResponse r = adminSession.put("/projects/" + name("newProject"), in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+    adminRestSession
+        .put("/projects/" + name("newProject"), in)
+        .assertBadRequest();
   }
 
   @Test
@@ -299,7 +301,7 @@
   }
 
   private void assertHead(String projectName, String expectedRef)
-      throws RepositoryNotFoundException, IOException {
+      throws Exception {
     try (Repository repo =
         repoManager.openRepository(new Project.NameKey(projectName))) {
       assertThat(repo.exactRef(Constants.HEAD).getTarget().getName())
@@ -308,7 +310,7 @@
   }
 
   private void assertEmptyCommit(String projectName, String... refs)
-      throws RepositoryNotFoundException, IOException {
+      throws Exception {
     Project.NameKey projectKey = new Project.NameKey(projectName);
     try (Repository repo = repoManager.openRepository(projectKey);
         RevWalk rw = new RevWalk(repo);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index c9347cd..955e580 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.block;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -26,7 +25,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.server.git.ProjectConfig;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -76,9 +74,7 @@
   }
 
   private void blockForcePush() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    block(cfg, Permission.PUSH, ANONYMOUS_USERS, "refs/heads/*").setForce(true);
-    saveProjectConfig(project, cfg);
+    block(Permission.PUSH, ANONYMOUS_USERS, "refs/heads/*").setForce(true);
   }
 
   private void grantOwner() throws Exception {
@@ -92,7 +88,11 @@
   }
 
   private void assertDeleteSucceeds() throws Exception {
+    String branchRev = branch().get().revision;
     branch().delete();
+    eventRecorder.assertRefUpdatedEvents(project.get(), branch.get(),
+        null, branchRev,
+        branchRev, null);
     exception.expect(ResourceNotFoundException.class);
     branch().get();
   }
@@ -100,6 +100,5 @@
   private void assertDeleteForbidden() throws Exception {
     exception.expect(AuthException.class);
     branch().delete();
-    branch().get();
   }
 }
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
new file mode 100644
index 0000000..af1383b
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.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.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertRefNames;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+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;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.List;
+
+@NoHttpd
+public class DeleteBranchesIT extends AbstractDaemonTest {
+  private static final List<String> BRANCHES = ImmutableList.of(
+      "refs/heads/test-1", "refs/heads/test-2", "refs/heads/test-3");
+
+  @Before
+  public void setUp() throws Exception {
+    for (String name : BRANCHES) {
+      project().branch(name).create(new BranchInput());
+    }
+    assertBranches(BRANCHES);
+  }
+
+  @Test
+  public void deleteBranches() throws Exception {
+    HashMap<String, RevCommit> initialRevisions = initialRevisions(BRANCHES);
+    DeleteBranchesInput input = new DeleteBranchesInput();
+    input.branches = BRANCHES;
+    project().deleteBranches(input);
+    assertBranchesDeleted();
+    assertRefUpdatedEvents(initialRevisions);
+  }
+
+  @Test
+  public void deleteBranchesForbidden() throws Exception {
+    DeleteBranchesInput input = new DeleteBranchesInput();
+    input.branches = BRANCHES;
+    setApiUser(user);
+    try {
+      project().deleteBranches(input);
+      fail("Expected ResourceConflictException");
+    } catch (ResourceConflictException e) {
+      assertThat(e).hasMessage(errorMessageForBranches(BRANCHES));
+    }
+    setApiUser(admin);
+    assertBranches(BRANCHES);
+  }
+
+  @Test
+  public void deleteBranchesNotFound() throws Exception {
+    DeleteBranchesInput input = new DeleteBranchesInput();
+    List<String> branches = Lists.newArrayList(BRANCHES);
+    branches.add("refs/heads/does-not-exist");
+    input.branches = branches;
+    try {
+      project().deleteBranches(input);
+      fail("Expected ResourceConflictException");
+    } catch (ResourceConflictException e) {
+      assertThat(e).hasMessage(errorMessageForBranches(
+          ImmutableList.of("refs/heads/does-not-exist")));
+    }
+    assertBranchesDeleted();
+  }
+
+  @Test
+  public void deleteBranchesNotFoundContinue() throws Exception {
+    // If it fails on the first branch in the input, it should still
+    // continue to process the remaining branches.
+    DeleteBranchesInput input = new DeleteBranchesInput();
+    List<String> branches = Lists.newArrayList("refs/heads/does-not-exist");
+    branches.addAll(BRANCHES);
+    input.branches = branches;
+    try {
+      project().deleteBranches(input);
+      fail("Expected ResourceConflictException");
+    } catch (ResourceConflictException e) {
+      assertThat(e).hasMessage(errorMessageForBranches(
+          ImmutableList.of("refs/heads/does-not-exist")));
+    }
+    assertBranchesDeleted();
+  }
+
+  private String errorMessageForBranches(List<String> branches) {
+    StringBuilder message = new StringBuilder();
+    for (String branch : branches) {
+      message.append("Cannot delete ")
+        .append(branch)
+        .append(": it doesn't exist or you do not have permission ")
+        .append("to delete it\n");
+    }
+    return message.toString();
+  }
+
+  private HashMap<String, RevCommit> initialRevisions(List<String> branches)
+      throws Exception {
+    HashMap<String, RevCommit> result = new HashMap<>();
+    for (String branch : branches) {
+      result.put(branch, getRemoteHead(project, branch));
+    }
+    return result;
+  }
+
+  private void assertRefUpdatedEvents(HashMap<String, RevCommit> revisions)
+      throws Exception {
+    for (String branch : revisions.keySet()) {
+      RevCommit revision = revisions.get(branch);
+      eventRecorder.assertRefUpdatedEvents(project.get(), branch,
+          null, revision,
+          revision, null);
+    }
+  }
+
+  private ProjectApi project() throws Exception {
+    return gApi.projects().name(project.get());
+  }
+
+  private void assertBranches(List<String> branches) throws Exception {
+    List<String> expected = Lists.newArrayList(
+        "HEAD", RefNames.REFS_CONFIG, "refs/heads/master");
+    expected.addAll(branches);
+    assertRefNames(expected, project().branches().get());
+  }
+
+  private void assertBranchesDeleted() throws Exception {
+    assertBranches(ImmutableList.<String>of());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/FileBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/FileBranchIT.java
new file mode 100644
index 0000000..66d04df
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/FileBranchIT.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Branch;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+public class FileBranchIT extends AbstractDaemonTest {
+
+  private Branch.NameKey branch;
+
+  @Before
+  public void setUp() throws Exception {
+    branch = new Branch.NameKey(project, "master");
+    PushOneCommit.Result change = createChange();
+    approve(change.getChangeId());
+    revision(change).submit();
+  }
+
+  @Test
+  public void getFileContent() throws Exception {
+    BinaryResult content = branch().file(PushOneCommit.FILE_NAME);
+    assertThat(content.asString()).isEqualTo(PushOneCommit.FILE_CONTENT);
+  }
+
+  @Test(expected = ResourceNotFoundException.class)
+  public void getNonExistingFile() throws Exception {
+    branch().file("does-not-exist");
+  }
+
+  private BranchApi branch() throws Exception {
+    return gApi.projects()
+        .name(branch.getParentKey().get())
+        .branch(branch.get());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
index d680541..8522a4d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GcAssert;
 import com.google.gerrit.acceptance.RestResponse;
@@ -23,12 +21,9 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 
-import org.apache.http.HttpStatus;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class GarbageCollectionIT extends AbstractDaemonTest {
 
   @Inject
@@ -43,29 +38,27 @@
 
   @Test
   public void testGcNonExistingProject_NotFound() throws Exception {
-    assertThat(POST("/projects/non-existing/gc")).isEqualTo(
-        HttpStatus.SC_NOT_FOUND);
+    POST("/projects/non-existing/gc").assertNotFound();
   }
 
   @Test
   public void testGcNotAllowed_Forbidden() throws Exception {
-    assertThat(
-        userSession.post("/projects/" + allProjects.get() + "/gc")
-            .getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    userRestSession
+        .post("/projects/" + allProjects.get() + "/gc")
+        .assertForbidden();
   }
 
   @Test
   @UseLocalDisk
   public void testGcOneProject() throws Exception {
-    assertThat(POST("/projects/" + allProjects.get() + "/gc")).isEqualTo(
-        HttpStatus.SC_OK);
+    POST("/projects/" + allProjects.get() + "/gc").assertOK();
     gcAssert.assertHasPackFile(allProjects);
     gcAssert.assertHasNoPackFile(project, project2);
   }
 
-  private int POST(String endPoint) throws IOException {
-    RestResponse r = adminSession.post(endPoint);
+  private RestResponse POST(String endPoint) throws Exception {
+    RestResponse r = adminRestSession.post(endPoint);
     r.consume();
-    return r.getStatusCode();
+    return r;
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
index ea3a5db..f87b921 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -71,10 +70,8 @@
 
   private void assertChildNotFound(Project.NameKey parent, String child)
       throws Exception {
-    try {
-      gApi.projects().name(parent.get()).child(child);
-    } catch (ResourceNotFoundException e) {
-      assertThat(e.getMessage()).contains(child);
-    }
+    exception.expect(ResourceNotFoundException.class);
+    exception.expectMessage(child);
+    gApi.projects().name(parent.get()).child(child).get();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
index 7044ad0..307d512 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.server.git.ProjectConfig;
 
-import org.apache.http.HttpStatus;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -40,7 +39,7 @@
   @Before
   public void setUp() throws Exception {
     repo = GitUtil.newTestRepository(repoManager.openRepository(project));
-    blockRead(project, "refs/*");
+    blockRead("refs/*");
   }
 
   @After
@@ -97,8 +96,8 @@
         .to("refs/for/master");
     r.assertOkStatus();
 
-    CommitInfo info = getCommit(r.getCommitId());
-    assertThat(info.commit).isEqualTo(r.getCommitId().name());
+    CommitInfo info = getCommit(r.getCommit());
+    assertThat(info.commit).isEqualTo(r.getCommit().name());
     assertThat(info.subject).isEqualTo("test commit");
     assertThat(info.message).isEqualTo(
         "test commit\n\nChange-Id: " + r.getChangeId() + "\n");
@@ -120,7 +119,7 @@
     PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), testRepo)
         .to("refs/for/master");
     r.assertOkStatus();
-    assertNotFound(r.getCommitId());
+    assertNotFound(r.getCommit());
   }
 
   private void unblockRead() throws Exception {
@@ -130,15 +129,15 @@
   }
 
   private void assertNotFound(ObjectId id) throws Exception {
-    RestResponse r = userSession.get(
-        "/projects/" + project.get() + "/commits/" + id.name());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    userRestSession
+        .get("/projects/" + project.get() + "/commits/" + id.name())
+        .assertNotFound();
   }
 
   private CommitInfo getCommit(ObjectId id) throws Exception {
-    RestResponse r = userSession.get(
+    RestResponse r = userRestSession.get(
         "/projects/" + project.get() + "/commits/" + id.name());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    r.assertOK();
     CommitInfo result = newGson().fromJson(r.getReader(), CommitInfo.class);
     r.consume();
     return result;
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 0799d48..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;
 
@@ -37,7 +38,7 @@
 
   @Test
   public void listBranchesOfNonVisibleProject_NotFound() throws Exception {
-    blockRead(project, "refs/*");
+    blockRead("refs/*");
     setApiUser(user);
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name(project.get()).branches().get();
@@ -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());
@@ -66,7 +67,7 @@
 
   @Test
   public void listBranchesSomeHidden() throws Exception {
-    blockRead(project, "refs/heads/dev");
+    blockRead("refs/heads/dev");
     String master = pushTo("refs/heads/master").getCommit().name();
     pushTo("refs/heads/dev");
     setApiUser(user);
@@ -79,7 +80,7 @@
 
   @Test
   public void listBranchesHeadHidden() throws Exception {
-    blockRead(project, "refs/heads/master");
+    blockRead("refs/heads/master");
     pushTo("refs/heads/master");
     String dev = pushTo("refs/heads/dev").getCommit().name();
     setApiUser(user);
@@ -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/rest/project/ListChildProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
index 80ad493..78e0ba2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -29,11 +28,9 @@
 
   @Test
   public void listChildrenOfNonExistingProject_NotFound() throws Exception {
-    try {
-      gApi.projects().name(name("non-existing")).child("children");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e.getMessage()).contains("non-existing");
-    }
+    exception.expect(ResourceNotFoundException.class);
+    exception.expectMessage("non-existing");
+    gApi.projects().name(name("non-existing")).child("children");
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 1e51571..e86bb29 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.junit.Assert.fail;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
@@ -197,9 +198,10 @@
         .containsExactly(allProjects, allUsers, project).inOrder();
   }
 
-  private static void assertBadRequest(ListRequest req) throws Exception {
+  private void assertBadRequest(ListRequest req) throws Exception {
     try {
       req.get();
+      fail("Expected BadRequestException");
     } catch (BadRequestException expected) {
       // Expected.
     }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
index da8ebed..b106e99 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.project.SetParent;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 public class SetParentIT extends AbstractDaemonTest {
@@ -30,9 +29,9 @@
   public void setParent_Forbidden() throws Exception {
     String parent = createProject("parent", null, true).get();
     RestResponse r =
-        userSession.put("/projects/" + project.get() + "/parent",
+        userRestSession.put("/projects/" + project.get() + "/parent",
             newParentInput(parent));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    r.assertForbidden();
     r.consume();
   }
 
@@ -40,13 +39,13 @@
   public void setParent() throws Exception {
     String parent = createProject("parent", null, true).get();
     RestResponse r =
-        adminSession.put("/projects/" + project.get() + "/parent",
+        adminRestSession.put("/projects/" + project.get() + "/parent",
             newParentInput(parent));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    r.assertOK();
     r.consume();
 
-    r = adminSession.get("/projects/" + project.get() + "/parent");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    r = adminRestSession.get("/projects/" + project.get() + "/parent");
+    r.assertOK();
     String newParent =
         newGson().fromJson(r.getReader(), String.class);
     assertThat(newParent).isEqualTo(parent);
@@ -54,13 +53,13 @@
 
     // When the parent name is not explicitly set, it should be
     // set to "All-Projects".
-    r = adminSession.put("/projects/" + project.get() + "/parent",
+    r = adminRestSession.put("/projects/" + project.get() + "/parent",
           newParentInput(null));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    r.assertOK();
     r.consume();
 
-    r = adminSession.get("/projects/" + project.get() + "/parent");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    r = adminRestSession.get("/projects/" + project.get() + "/parent");
+    r.assertOK();
     newParent = newGson().fromJson(r.getReader(), String.class);
     assertThat(newParent).isEqualTo(AllProjectsNameProvider.DEFAULT);
     r.consume();
@@ -69,39 +68,39 @@
   @Test
   public void setParentForAllProjects_Conflict() throws Exception {
     RestResponse r =
-        adminSession.put("/projects/" + allProjects.get() + "/parent",
+        adminRestSession.put("/projects/" + allProjects.get() + "/parent",
             newParentInput(project.get()));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+    r.assertConflict();
     r.consume();
   }
 
   @Test
   public void setInvalidParent_Conflict() throws Exception {
     RestResponse r =
-        adminSession.put("/projects/" + project.get() + "/parent",
+        adminRestSession.put("/projects/" + project.get() + "/parent",
             newParentInput(project.get()));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+    r.assertConflict();
     r.consume();
 
     Project.NameKey child = createProject("child", project, true);
-    r = adminSession.put("/projects/" + project.get() + "/parent",
+    r = adminRestSession.put("/projects/" + project.get() + "/parent",
            newParentInput(child.get()));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+    r.assertConflict();
     r.consume();
 
     String grandchild = createProject("grandchild", child, true).get();
-    r = adminSession.put("/projects/" + project.get() + "/parent",
+    r = adminRestSession.put("/projects/" + project.get() + "/parent",
            newParentInput(grandchild));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+    r.assertConflict();
     r.consume();
   }
 
   @Test
   public void setNonExistingParent_UnprocessibleEntity() throws Exception {
     RestResponse r =
-        adminSession.put("/projects/" + project.get() + "/parent",
+        adminRestSession.put("/projects/" + project.get() + "/parent",
             newParentInput("non-existing"));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+    r.assertUnprocessableEntity();
     r.consume();
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
index b22d26b9..33aa726 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -15,170 +15,103 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
-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.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
+import com.google.gerrit.extensions.api.projects.TagApi;
 import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.api.projects.TagInput;
+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.ResourceNotFoundException;
 
-import org.apache.http.HttpStatus;
-import org.eclipse.jgit.api.PushCommand;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.transport.PushResult;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 import org.junit.Test;
 
 import java.util.List;
 
+@NoHttpd
 public class TagsIT extends AbstractDaemonTest {
   private static final List<String> testTags = ImmutableList.of(
       "tag-A", "tag-B", "tag-C", "tag-D", "tag-E", "tag-F", "tag-G", "tag-H");
 
-  @Test
-  public void listTagsOfNonExistingProject() throws Exception {
-    assertThat(adminSession.get("/projects/non-existing/tags").getStatusCode())
-        .isEqualTo(HttpStatus.SC_NOT_FOUND);
-  }
+  private static final String SIGNED_ANNOTATION = "annotation\n"
+      + "-----BEGIN PGP SIGNATURE-----\n"
+      + "Version: GnuPG v1\n"
+      + "\n"
+      + "iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa\n"
+      + "9tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7\n"
+      + "htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V\n"
+      + "4ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG\n"
+      + "IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY\n"
+      + "+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=\n"
+      + "=XFeC\n"
+      + "-----END PGP SIGNATURE-----";
 
   @Test
-  public void listTagsOfNonExistingProjectWithApi() throws Exception {
+  public void listTagsOfNonExistingProject() throws Exception {
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name("does-not-exist").tags().get();
   }
 
   @Test
-  public void getTagOfNonExistingProjectWithApi() throws Exception {
+  public void getTagOfNonExistingProject() throws Exception {
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name("does-not-exist").tag("tag").get();
   }
 
   @Test
   public void listTagsOfNonVisibleProject() throws Exception {
-    blockRead(project, "refs/*");
-    assertThat(
-        userSession.get("/projects/" + project.get() + "/tags").getStatusCode())
-        .isEqualTo(HttpStatus.SC_NOT_FOUND);
-  }
-
-  @Test
-  public void listTagsOfNonVisibleProjectWithApi() throws Exception {
-    blockRead(project, "refs/*");
+    blockRead("refs/*");
     setApiUser(user);
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name(project.get()).tags().get();
   }
 
   @Test
-  public void getTagOfNonVisibleProjectWithApi() throws Exception {
-    blockRead(project, "refs/*");
+  public void getTagOfNonVisibleProject() throws Exception {
+    blockRead("refs/*");
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name(project.get()).tag("tag").get();
   }
 
   @Test
   public void listTags() throws Exception {
-    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
-    grant(Permission.CREATE, project, "refs/tags/*");
-    grant(Permission.PUSH, project, "refs/tags/*");
-
-    PushOneCommit.Tag tag1 = new PushOneCommit.Tag("v1.0");
-    PushOneCommit push1 = pushFactory.create(db, admin.getIdent(), testRepo);
-    push1.setTag(tag1);
-    PushOneCommit.Result r1 = push1.to("refs/for/master%submit");
-    r1.assertOkStatus();
-
-    PushOneCommit.AnnotatedTag tag2 =
-        new PushOneCommit.AnnotatedTag("v2.0", "annotation", admin.getIdent());
-    PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo);
-    push2.setTag(tag2);
-    PushOneCommit.Result r2 = push2.to("refs/for/master%submit");
-    r2.assertOkStatus();
-
-    String tag3Ref = Constants.R_TAGS + "vLatest";
-    PushCommand pushCmd = testRepo.git().push();
-    pushCmd.setRefSpecs(new RefSpec(tag2.name + ":" + tag3Ref));
-    Iterable<PushResult> r = pushCmd.call();
-    assertThat(Iterables.getOnlyElement(r).getRemoteUpdate(tag3Ref).getStatus())
-        .isEqualTo(Status.OK);
-
-    List<TagInfo> result = getTags().get();
-    assertThat(result).hasSize(3);
-
-    TagInfo t = result.get(0);
-    assertThat(t.ref).isEqualTo(Constants.R_TAGS + tag1.name);
-    assertThat(t.revision).isEqualTo(r1.getCommitId().getName());
-
-    t = result.get(1);
-    assertThat(t.ref).isEqualTo(Constants.R_TAGS + tag2.name);
-    assertThat(t.object).isEqualTo(r2.getCommitId().getName());
-    assertThat(t.message).isEqualTo(tag2.message);
-    assertThat(t.tagger.name).isEqualTo(tag2.tagger.getName());
-    assertThat(t.tagger.email).isEqualTo(tag2.tagger.getEmailAddress());
-
-    t = result.get(2);
-    assertThat(t.ref).isEqualTo(tag3Ref);
-    assertThat(t.object).isEqualTo(r2.getCommitId().getName());
-    assertThat(t.message).isEqualTo(tag2.message);
-    assertThat(t.tagger.name).isEqualTo(tag2.tagger.getName());
-    assertThat(t.tagger.email).isEqualTo(tag2.tagger.getEmailAddress());
-  }
-
-  private void assertTagList(FluentIterable<String> expected, List<TagInfo> actual)
-      throws Exception {
-    assertThat(actual).hasSize(expected.size());
-    for (int i = 0; i < expected.size(); i ++) {
-      assertThat(actual.get(i).ref).isEqualTo("refs/tags/" + expected.get(i));
-    }
-  }
-
-  @Test
-  public void listTagsWithoutOptions() throws Exception {
     createTags();
+
+    // No options
     List<TagInfo> result = getTags().get();
     assertTagList(FluentIterable.from(testTags), result);
-  }
 
-  @Test
-  public void listTagsWithStartOption() throws Exception {
-    createTags();
-    List<TagInfo> result = getTags().withStart(1).get();
+    // With start option
+    result = getTags().withStart(1).get();
     assertTagList(FluentIterable.from(testTags).skip(1), result);
-  }
 
-  @Test
-  public void listTagsWithLimitOption() throws Exception {
-    createTags();
+    // With limit option
     int limit = testTags.size() - 1;
-    List<TagInfo> result = getTags().withLimit(limit).get();
+    result = getTags().withLimit(limit).get();
     assertTagList(FluentIterable.from(testTags).limit(limit), result);
-  }
 
-  @Test
-  public void listTagsWithLimitAndStartOption() throws Exception {
-    createTags();
-    int limit = testTags.size() - 3;
-    List<TagInfo> result = getTags().withStart(1).withLimit(limit).get();
+    // With both start and limit
+    limit = testTags.size() - 3;
+    result = getTags().withStart(1).withLimit(limit).get();
     assertTagList(FluentIterable.from(testTags).skip(1).limit(limit), result);
-  }
 
-  @Test
-  public void listTagsWithRegexFilter() throws Exception {
-    createTags();
-    List<TagInfo> result = getTags().withRegex("^tag-[C|D]$").get();
+    // With regular expression filter
+    result = getTags().withRegex("^tag-[C|D]$").get();
     assertTagList(
         FluentIterable.from(ImmutableList.of("tag-C", "tag-D")), result);
-  }
 
-  @Test
-  public void listTagsWithSubstringFilter() throws Exception {
-    createTags();
-    List<TagInfo> result = getTags().withSubstring("tag-").get();
+    // With substring filter
+    result = getTags().withSubstring("tag-").get();
     assertTagList(FluentIterable.from(testTags), result);
     result = getTags().withSubstring("ag-B").get();
     assertTagList(FluentIterable.from(ImmutableList.of("tag-B")), result);
@@ -186,73 +119,234 @@
 
   @Test
   public void listTagsOfNonVisibleBranch() throws Exception {
-    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
-    grant(Permission.SUBMIT, project, "refs/for/refs/heads/hidden");
-    grant(Permission.CREATE, project, "refs/tags/*");
-    grant(Permission.PUSH, project, "refs/tags/*");
+    grantTagPermissions();
 
-    PushOneCommit.Tag tag1 = new PushOneCommit.Tag("v1.0");
     PushOneCommit push1 = pushFactory.create(db, admin.getIdent(), testRepo);
-    push1.setTag(tag1);
-    PushOneCommit.Result r1 = push1.to("refs/for/master%submit");
+    PushOneCommit.Result r1 = push1.to("refs/heads/master");
     r1.assertOkStatus();
+    TagInput tag1 = new TagInput();
+    tag1.ref = "v1.0";
+    tag1.revision = r1.getCommit().getName();
+    TagInfo result = tag(tag1.ref).create(tag1).get();
+    assertThat(result.ref).isEqualTo(R_TAGS + tag1.ref);
+    assertThat(result.revision).isEqualTo(tag1.revision);
 
     pushTo("refs/heads/hidden");
-    PushOneCommit.Tag tag2 = new PushOneCommit.Tag("v2.0");
     PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo);
-    push2.setTag(tag2);
-    PushOneCommit.Result r2 = push2.to("refs/for/hidden%submit");
+    PushOneCommit.Result r2 = push2.to("refs/heads/hidden");
     r2.assertOkStatus();
 
-    List<TagInfo> result = getTags().get();
-    assertThat(result).hasSize(2);
-    assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
-    assertThat(result.get(0).revision).isEqualTo(r1.getCommitId().getName());
-    assertThat(result.get(1).ref).isEqualTo("refs/tags/" + tag2.name);
-    assertThat(result.get(1).revision).isEqualTo(r2.getCommitId().getName());
+    TagInput tag2 = new TagInput();
+    tag2.ref = "v2.0";
+    tag2.revision = r2.getCommit().getName();
+    result = tag(tag2.ref).create(tag2).get();
+    assertThat(result.ref).isEqualTo(R_TAGS + tag2.ref);
+    assertThat(result.revision).isEqualTo(tag2.revision);
 
-    blockRead(project, "refs/heads/hidden");
-    result = getTags().get();
-    assertThat(result).hasSize(1);
-    assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
-    assertThat(result.get(0).revision).isEqualTo(r1.getCommitId().getName());
+    List<TagInfo> tags = getTags().get();
+    assertThat(tags).hasSize(2);
+    assertThat(tags.get(0).ref).isEqualTo(R_TAGS + tag1.ref);
+    assertThat(tags.get(0).revision).isEqualTo(tag1.revision);
+    assertThat(tags.get(1).ref).isEqualTo(R_TAGS + tag2.ref);
+    assertThat(tags.get(1).revision).isEqualTo(tag2.revision);
+
+    blockRead("refs/heads/hidden");
+    tags = getTags().get();
+    assertThat(tags).hasSize(1);
+    assertThat(tags.get(0).ref).isEqualTo(R_TAGS + tag1.ref);
+    assertThat(tags.get(0).revision).isEqualTo(tag1.revision);
   }
 
   @Test
-  public void getTag() throws Exception {
-    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
-    grant(Permission.CREATE, project, "refs/tags/*");
-    grant(Permission.PUSH, project, "refs/tags/*");
+  public void lightweightTag() throws Exception {
+    grantTagPermissions();
 
-    PushOneCommit.Tag tag1 = new PushOneCommit.Tag("v1.0");
-    PushOneCommit push1 = pushFactory.create(db, admin.getIdent(), testRepo);
-    push1.setTag(tag1);
-    PushOneCommit.Result r1 = push1.to("refs/for/master%submit");
-    r1.assertOkStatus();
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result r = push.to("refs/heads/master");
+    r.assertOkStatus();
 
-    TagInfo tagInfo = getTag(tag1.name);
-    assertThat(tagInfo.ref).isEqualTo("refs/tags/" + tag1.name);
-    assertThat(tagInfo.revision).isEqualTo(r1.getCommitId().getName());
+    TagInput input = new TagInput();
+    input.ref = "v1.0";
+    input.revision = r.getCommit().getName();
+
+    TagInfo result = tag(input.ref).create(input).get();
+    assertThat(result.ref).isEqualTo(R_TAGS + input.ref);
+    assertThat(result.revision).isEqualTo(input.revision);
+
+    input.ref = "refs/tags/v2.0";
+    result = tag(input.ref).create(input).get();
+    assertThat(result.ref).isEqualTo(input.ref);
+    assertThat(result.revision).isEqualTo(input.revision);
+
+    eventRecorder.assertRefUpdatedEvents(project.get(), result.ref,
+        null, result.revision);
+  }
+
+  @Test
+  public void annotatedTag() throws Exception {
+    grantTagPermissions();
+
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result r = push.to("refs/heads/master");
+    r.assertOkStatus();
+
+    TagInput input = new TagInput();
+    input.ref = "v1.0";
+    input.revision = r.getCommit().getName();
+    input.message = "annotation message";
+
+    TagInfo result = tag(input.ref).create(input).get();
+    assertThat(result.ref).isEqualTo(R_TAGS + input.ref);
+    assertThat(result.object).isEqualTo(input.revision);
+    assertThat(result.message).isEqualTo(input.message);
+    assertThat(result.tagger.name).isEqualTo(admin.fullName);
+    assertThat(result.tagger.email).isEqualTo(admin.email);
+
+    eventRecorder.assertRefUpdatedEvents(project.get(), result.ref,
+        null, result.revision);
+
+    // A second tag pushed on the same ref should have the same ref
+    TagInput input2 = new TagInput();
+    input2.ref = "refs/tags/v2.0";
+    input2.revision = input.revision;
+    input2.message = "second annotation message";
+    TagInfo result2 = tag(input2.ref).create(input2).get();
+    assertThat(result2.ref).isEqualTo(input2.ref);
+    assertThat(result2.object).isEqualTo(input2.revision);
+    assertThat(result2.message).isEqualTo(input2.message);
+    assertThat(result2.tagger.name).isEqualTo(admin.fullName);
+    assertThat(result2.tagger.email).isEqualTo(admin.email);
+
+    eventRecorder.assertRefUpdatedEvents(project.get(), result2.ref,
+        null, result2.revision);
+  }
+
+  @Test
+  public void createExistingTag() throws Exception {
+    grantTagPermissions();
+
+    TagInput input = new TagInput();
+    input.ref = "test";
+    TagInfo result = tag(input.ref).create(input).get();
+    assertThat(result.ref).isEqualTo(R_TAGS + "test");
+
+    input.ref = "refs/tags/test";
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("tag \"" + R_TAGS + "test\" already exists");
+    tag(input.ref).create(input);
+  }
+
+  @Test
+  public void createTagNotAllowed() throws Exception {
+    TagInput input = new TagInput();
+    input.ref = "test";
+    exception.expect(AuthException.class);
+    exception.expectMessage("Cannot create tag \"" + R_TAGS + "test\"");
+    tag(input.ref).create(input);
+  }
+
+  @Test
+  public void createAnnotatedTagNotAllowed() throws Exception {
+    block(Permission.PUSH_TAG, REGISTERED_USERS, R_TAGS + "*");
+    TagInput input = new TagInput();
+    input.ref = "test";
+    input.message = "annotation";
+    exception.expect(AuthException.class);
+    exception.expectMessage(
+        "Cannot create annotated tag \"" + R_TAGS + "test\"");
+    tag(input.ref).create(input);
+  }
+
+  @Test
+  public void createSignedTagNotSupported() throws Exception {
+    TagInput input = new TagInput();
+    input.ref = "test";
+    input.message = SIGNED_ANNOTATION;
+    exception.expect(MethodNotAllowedException.class);
+    exception.expectMessage("Cannot create signed tag \"" + R_TAGS + "test\"");
+    tag(input.ref).create(input);
+  }
+
+  @Test
+  public void mismatchedInput() throws Exception {
+    TagInput input = new TagInput();
+    input.ref = "test";
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("ref must match URL");
+    tag("TEST").create(input);
+  }
+
+  @Test
+  public void invalidTagName() throws Exception {
+    grantTagPermissions();
+
+    TagInput input = new TagInput();
+    input.ref = "refs/heads/test";
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("invalid tag name \"" + input.ref + "\"");
+    tag(input.ref).create(input);
+  }
+
+  @Test
+  public void invalidTagNameOnlySlashes() throws Exception {
+    grantTagPermissions();
+
+    TagInput input = new TagInput();
+    input.ref = "//";
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("invalid tag name \"refs/tags/\"");
+    tag(input.ref).create(input);
+  }
+
+  @Test
+  public void invalidBaseRevision() throws Exception {
+    grantTagPermissions();
+
+    TagInput input = new TagInput();
+    input.ref = "test";
+    input.revision = "abcdefg";
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("Invalid base revision");
+    tag(input.ref).create(input);
+  }
+
+  private void assertTagList(FluentIterable<String> expected,
+      List<TagInfo> actual) throws Exception {
+    assertThat(actual).hasSize(expected.size());
+    for (int i = 0; i < expected.size(); i ++) {
+      assertThat(actual.get(i).ref).isEqualTo(R_TAGS + expected.get(i));
+    }
   }
 
   private void createTags() throws Exception {
-    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
-    grant(Permission.CREATE, project, "refs/tags/*");
-    grant(Permission.PUSH, project, "refs/tags/*");
+    grantTagPermissions();
+
+    String revision = pushTo("refs/heads/master").getCommit().name();
+    TagInput input = new TagInput();
+    input.revision = revision;
+
     for (String tagname : testTags) {
-      PushOneCommit.Tag tag = new PushOneCommit.Tag(tagname);
-      PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
-      push.setTag(tag);
-      PushOneCommit.Result result = push.to("refs/for/master%submit");
-      result.assertOkStatus();
+      TagInfo result = tag(tagname).create(input).get();
+      assertThat(result.revision).isEqualTo(input.revision);
+      assertThat(result.ref).isEqualTo(R_TAGS + tagname);
     }
   }
 
+  private void grantTagPermissions() throws Exception {
+    grant(Permission.CREATE, project, R_TAGS + "*");
+    grant(Permission.PUSH_TAG, project, R_TAGS + "*");
+    grant(Permission.PUSH_SIGNED_TAG, project, R_TAGS + "*");
+  }
+
   private ListRefsRequest<TagInfo> getTags() throws Exception {
     return gApi.projects().name(project.get()).tags();
   }
 
-  private TagInfo getTag(String ref) throws Exception {
-    return gApi.projects().name(project.get()).tag(ref).get();
+  private TagApi tag(String tagname) throws Exception {
+    return gApi.projects().name(project.get()).tag(tagname);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK
index 94e69da..5384447 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK
@@ -1,7 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  group = 'server-change',
+  group = 'server_change',
   srcs = glob(['*IT.java']),
   labels = ['server'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUILD
new file mode 100644
index 0000000..a5e6d36
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUILD
@@ -0,0 +1,7 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'server_change',
+  srcs = glob(['*IT.java']),
+  labels = ['server'],
+)
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 b784f05..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,8 +18,8 @@
 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.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -33,12 +33,12 @@
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.PostReview;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.change.Revisions;
 import com.google.gerrit.testutil.FakeEmailSender;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
 import com.google.inject.Inject;
@@ -48,17 +48,18 @@
 import org.junit.Test;
 
 import java.sql.Timestamp;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 @NoHttpd
 public class CommentsIT extends AbstractDaemonTest {
-  @Inject
-  private Provider<ChangesCollection> changes;
 
   @Inject
-  private Provider<Revisions> revisions;
+  private Provider<ChangesCollection> changes;
 
   @Inject
   private Provider<PostReview> postReview;
@@ -74,17 +75,49 @@
   }
 
   @Test
+  public void getNonExistingComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    exception.expect(ResourceNotFoundException.class);
+    getPublishedComment(changeId, revId, "non-existing");
+  }
+
+  @Test
   public void createDraft() throws Exception {
     for (Integer line : lines) {
       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);
     }
   }
 
@@ -106,41 +139,121 @@
       Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
       assertThat(result).isNotEmpty();
       CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
-      assertCommentInfo(comment, actual);
+      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);
+    }
+  }
+
+  @Test
+  public void listComments() throws Exception {
+    String file = "file";
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+        "first subject", file, "contents");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    assertThat(getPublishedComments(changeId, revId)).isEmpty();
+
+    List<CommentInput> expectedComments = new ArrayList<>();
+    for (Integer line : lines) {
+      ReviewInput input = new ReviewInput();
+      CommentInput comment = newComment(file, Side.REVISION, line, "comment " + line);
+      expectedComments.add(comment);
+      input.comments = new HashMap<>();
+      input.comments.put(comment.path, Lists.newArrayList(comment));
+      revision(r).review(input);
+    }
+
+    Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
+    assertThat(result).isNotEmpty();
+    List<CommentInfo> actualComments = result.get(file);
+    assertThat(Lists.transform(actualComments, infoToInput(file)))
+        .containsExactlyElementsIn(expectedComments);
+  }
+
+  @Test
   public void putDraft() throws Exception {
     for (Integer line : lines) {
       PushOneCommit.Result r = createChange();
+      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())
+          .isEqualTo(origLastUpdated);
     }
   }
 
   @Test
+  public void listDrafts() throws Exception {
+    String file = "file";
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    assertThat(getDraftComments(changeId, revId)).isEmpty();
+
+    List<DraftInput> expectedDrafts = new ArrayList<>();
+    for (Integer line : lines) {
+      DraftInput comment = newDraft(file, Side.REVISION, line, "comment " + line);
+      expectedDrafts.add(comment);
+      addDraft(changeId, revId, comment);
+    }
+
+    Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
+    assertThat(result).isNotEmpty();
+    List<CommentInfo> actualComments = result.get(file);
+    assertThat(Lists.transform(actualComments, infoToDraft(file)))
+        .containsExactlyElementsIn(expectedDrafts);
+  }
+
+  @Test
   public void getDraft() throws Exception {
     for (Integer line : lines) {
       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));
     }
   }
 
@@ -148,6 +261,7 @@
   public void deleteDraft() throws Exception {
     for (Integer line : lines) {
       PushOneCommit.Result r = createChange();
+      Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
       DraftInput draft = newDraft("file1", Side.REVISION, line, "comment 1");
@@ -155,6 +269,10 @@
       deleteDraft(changeId, revId, returned.id);
       Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
       assertThat(drafts).isEmpty();
+
+      // Deleting a draft comment doesn't cause lastUpdatedOn to change.
+      assertThat(r.getChange().change().getLastUpdatedOn())
+          .isEqualTo(origLastUpdated);
     }
   }
 
@@ -169,6 +287,8 @@
       PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
+      Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn();
+
       ReviewInput input = new ReviewInput();
       CommentInput comment = newComment(file, Side.REVISION, line, "comment 1");
       comment.updated = timestamp;
@@ -178,13 +298,20 @@
           changes.get().parse(TopLevelResource.INSTANCE,
               IdString.fromDecoded(changeId));
       RevisionResource revRsrc =
-          revisions.get().parse(changeRsrc, IdString.fromDecoded(revId));
+          revisions.parse(changeRsrc, IdString.fromDecoded(revId));
       postReview.get().apply(revRsrc, input, timestamp);
       Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
       assertThat(result).isNotEmpty();
       CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
-      assertCommentInfo(comment, actual);
-      assertThat(comment.updated).isEqualTo(timestamp);
+      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);
+
+      // Updating historic comments doesn't cause lastUpdatedOn to regress.
+      assertThat(r.getChange().change().getLastUpdatedOn())
+          .isEqualTo(origLastUpdated);
     }
   }
 
@@ -289,6 +416,20 @@
   }
 
   @Test
+  public void listChangeWithDrafts() throws Exception {
+    for (Integer line : lines) {
+      PushOneCommit.Result r = createChange();
+      String changeId = r.getChangeId();
+      String revId = r.getCommit().getName();
+      DraftInput comment = newDraft(
+          "file1", Side.REVISION, line, "comment 1");
+      addDraft(changeId, revId, comment);
+      assertThat(gApi.changes().query(
+          "change:" + changeId + " has:draft").get()).hasSize(1);
+    }
+  }
+
+  @Test
   public void publishCommentsAllRevisions() throws Exception {
     PushOneCommit.Result r1 = createChange();
 
@@ -299,10 +440,16 @@
 
     addDraft(r1.getChangeId(), r1.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "nit: trailing whitespace"));
+    addDraft(r1.getChangeId(), r1.getCommit().getName(),
+        newDraft(FILE_NAME, Side.PARENT, 2, "what happened to this?"));
     addDraft(r2.getChangeId(), r2.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "join lines"));
     addDraft(r2.getChangeId(), r2.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 2, "typo: content"));
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.PARENT, 1, "comment 1 on base"));
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.PARENT, 2, "comment 2 on base"));
 
     PushOneCommit.Result other = createChange();
     // Drafts on other changes aren't returned.
@@ -334,8 +481,11 @@
         .comments();
     assertThat(ps1Map.keySet()).containsExactly(FILE_NAME);
     List<CommentInfo> ps1List = ps1Map.get(FILE_NAME);
-    assertThat(ps1List).hasSize(1);
-    assertThat(ps1List.get(0).message).isEqualTo("nit: trailing whitespace");
+    assertThat(ps1List).hasSize(2);
+    assertThat(ps1List.get(0).message).isEqualTo("what happened to this?");
+    assertThat(ps1List.get(0).side).isEqualTo(Side.PARENT);
+    assertThat(ps1List.get(1).message).isEqualTo("nit: trailing whitespace");
+    assertThat(ps1List.get(1).side).isNull();
 
     assertThat(gApi.changes()
           .id(r2.getChangeId())
@@ -348,26 +498,31 @@
         .comments();
     assertThat(ps2Map.keySet()).containsExactly(FILE_NAME);
     List<CommentInfo> ps2List = ps2Map.get(FILE_NAME);
-    assertThat(ps2List).hasSize(2);
-    assertThat(ps2List.get(0).message).isEqualTo("join lines");
-    assertThat(ps2List.get(1).message).isEqualTo("typo: content");
+    assertThat(ps2List).hasSize(4);
+    assertThat(ps2List.get(0).message).isEqualTo("comment 1 on base");
+    assertThat(ps2List.get(1).message).isEqualTo("comment 2 on base");
+    assertThat(ps2List.get(2).message).isEqualTo("join lines");
+    assertThat(ps2List.get(3).message).isEqualTo("typo: content");
 
     ImmutableList<Message> messages =
         email.getMessages(r2.getChangeId(), "comment");
     assertThat(messages).hasSize(1);
     String url = canonicalWebUrl.get();
     int c = r1.getChange().getId().get();
-    assertThat(messages.get(0).body()).contains(
-        "\n"
-        + "Patch Set 2:\n"
+    assertThat(extractComments(messages.get(0).body())).isEqualTo(
+        "Patch Set 2:\n"
         + "\n"
-        + "(3 comments)\n"
+        + "(6 comments)\n"
         + "\n"
         + "comments\n"
         + "\n"
         + url + "#/c/" + c + "/1/a.txt\n"
         + "File a.txt:\n"
         + "\n"
+        + "PS1, Line 2: \n"
+        + "what happened to this?\n"
+        + "\n"
+        + "\n"
         + "PS1, Line 1: ew\n"
         + "nit: trailing whitespace\n"
         + "\n"
@@ -375,15 +530,65 @@
         + url + "#/c/" + c + "/2/a.txt\n"
         + "File a.txt:\n"
         + "\n"
+        + "PS2, Line 1: \n"
+        + "comment 1 on base\n"
+        + "\n"
+        + "\n"
+        + "PS2, Line 2: \n"
+        + "comment 2 on base\n"
+        + "\n"
+        + "\n"
         + "PS2, Line 1: ew\n"
         + "join lines\n"
         + "\n"
         + "\n"
         + "PS2, Line 2: nten\n"
         + "typo: content\n"
-        + "\n"
-        + "\n"
-        + "-- \n");
+        + "\n");
+  }
+
+  @Test
+  public void commentTags() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    CommentInput pub = new CommentInput();
+    pub.line = 1;
+    pub.message = "published comment";
+    pub.path = FILE_NAME;
+    ReviewInput rin = newInput(pub);
+    rin.tag = "tag1";
+    gApi.changes().id(r.getChangeId()).current().review(rin);
+
+    List<CommentInfo> comments =
+        gApi.changes().id(r.getChangeId()).current().commentsAsList();
+    assertThat(comments).hasSize(1);
+    assertThat(comments.get(0).tag).isEqualTo("tag1");
+
+    DraftInput draft = new DraftInput();
+    draft.line = 2;
+    draft.message = "draft comment";
+    draft.path = FILE_NAME;
+    draft.tag = "tag2";
+    addDraft(r.getChangeId(), r.getCommit().name(), draft);
+
+    List<CommentInfo> drafts =
+        gApi.changes().id(r.getChangeId()).current().draftsAsList();
+    assertThat(drafts).hasSize(1);
+    assertThat(drafts.get(0).tag).isEqualTo("tag2");
+  }
+
+  private static String extractComments(String msg) {
+    // Extract lines between start "....." and end "-- ".
+    Pattern p = Pattern.compile(".*[.]{5}\n+(.*)\\n+-- \n.*", Pattern.DOTALL);
+    Matcher m = p.matcher(msg);
+    return m.matches() ? m.group(1) : msg;
+  }
+
+  private ReviewInput newInput(CommentInput c) {
+    ReviewInput in = new ReviewInput();
+    in.comments = new HashMap<>();
+    in.comments.put(c.path, Lists.newArrayList(c));
+    return in;
   }
 
   private void addComment(PushOneCommit.Result r, String message)
@@ -397,9 +602,7 @@
     c.line = 1;
     c.message = message;
     c.path = FILE_NAME;
-    ReviewInput in = new ReviewInput();
-    in.comments = ImmutableMap.<String, List<CommentInput>> of(
-        FILE_NAME, ImmutableList.of(c));
+    ReviewInput in = newInput(c);
     in.omitDuplicateComments = omitDuplicateComments;
     gApi.changes()
         .id(r.getChangeId())
@@ -422,6 +625,11 @@
     gApi.changes().id(changeId).revision(revId).draft(uuid).delete();
   }
 
+  private CommentInfo getPublishedComment(String changeId, String revId,
+      String uuid) throws Exception {
+    return gApi.changes().id(changeId).revision(revId).comment(uuid).get();
+  }
+
   private Map<String, List<CommentInfo>> getPublishedComments(String changeId,
       String revId) throws Exception {
     return gApi.changes().id(changeId).revision(revId).comments();
@@ -437,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) {
-      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) {
@@ -488,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 272cdcd..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,43 +14,94 @@
 
 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.gerrit.testutil.TestChanges.newChange;
+import static com.google.common.truth.TruthJUnit.assume;
+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;
 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;
+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.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
 public class ConsistencyCheckerIT extends AbstractDaemonTest {
   @Inject
+  private ChangeControl.GenericFactory changeControlFactory;
+
+  @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;
@@ -68,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
@@ -131,394 +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 = db.changes().get(c.getId());
-    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
-    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
-    assertThat(db.patchSets().get(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 = db.changes().get(c.getId());
-    assertThat(c.currentPatchSetId().get()).isEqualTo(3);
-    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
-    assertThat(db.patchSets().get(ps2.getId())).isNull();
-    assertThat(db.patchSets().get(ps3.getId())).isNotNull();
-    assertThat(db.patchSets().get(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 = db.changes().get(c.getId());
-    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
-    assertThat(db.patchSets().get(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 = db.changes().get(c.getId());
-    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 {
+    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(ctl.getId().get())
+        .info();
+    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+
+    info = gApi.changes()
+        .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 = db.changes().get(c.getId());
-    PatchSet.Id psId2 = new PatchSet.Id(c.getId(), 2);
-    assertThat(c.currentPatchSetId()).isEqualTo(psId2);
-    assertThat(db.patchSets().get(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 = db.changes().get(c.getId());
-    PatchSet.Id psId2 = new PatchSet.Id(c.getId(), 2);
-    assertThat(c.currentPatchSetId()).isEqualTo(psId2);
-    assertThat(db.patchSets().get(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)
@@ -527,77 +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 + ", " + ps3 + "]");
+    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));
-    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 {
@@ -606,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/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index c59a3eb..40ea296 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -16,31 +16,52 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.GetRelated.ChangeAndCommit;
 import com.google.gerrit.server.change.GetRelated.RelatedInfo;
 import com.google.gerrit.server.edit.ChangeEditModifier;
 import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.testutil.TestTimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.List;
 
 public class GetRelatedIT extends AbstractDaemonTest {
+  private String systemTimeZone;
+
+  @Before
+  public void setTimeForTesting() {
+    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
+  }
+
+  @After
+  public void resetTime() {
+    TestTimeUtil.useSystemTime();
+    System.setProperty("user.timezone", systemTimeZone);
+  }
+
   @Inject
   private ChangeEditUtil editUtil;
 
@@ -48,7 +69,10 @@
   private ChangeEditModifier editModifier;
 
   @Inject
-  private ChangeData.Factory changeDataFactory;
+  private BatchUpdate.Factory updateFactory;
+
+  @Inject
+  private ChangesCollection changes;
 
   @Test
   public void getRelatedNoResult() throws Exception {
@@ -79,6 +103,38 @@
   }
 
   @Test
+  public void getRelatedLinearSeparatePushes() throws Exception {
+    // 1,1---2,1
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+
+    testRepo.reset(c1_1);
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    String oldETag = changes.parse(ps1_1.getParentKey()).getETag();
+
+    testRepo.reset(c2_1);
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+
+    // Push of change 2 should not affect groups (or anything else) of change 1.
+    assertThat(changes.parse(ps1_1.getParentKey()).getETag())
+        .isEqualTo(oldETag);
+
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps2_1, c2_1, 1),
+          changeAndCommit(ps1_1, c1_1, 1));
+    }
+  }
+
+  @Test
   public void getRelatedReorder() throws Exception {
     // 1,1---2,1
     //
@@ -521,9 +577,9 @@
     pushHead(testRepo, "refs/for/master", false);
 
     Change ch2 = getChange(c2_1).change();
-    editModifier.createEdit(ch2, getPatchSet(ch2));
+    editModifier.createEdit(ch2, getPatchSet(ch2.currentPatchSetId()));
     editModifier.modifyFile(editUtil.byChange(ch2).get(), "a.txt",
-        RestSession.newRawInput(new byte[] {'a'}));
+        RawInputUtil.create(new byte[] {'a'}));
     ObjectId editRev =
         ObjectId.fromString(editUtil.byChange(ch2).get().getRevision().get());
 
@@ -569,15 +625,12 @@
     }
 
     // Pretend PS1,1 was pushed before the groups field was added.
-    PatchSet ps1_1 = db.patchSets().get(psId1_1);
-    ps1_1.setGroups(null);
-    db.patchSets().update(ImmutableList.of(ps1_1));
-    indexer.index(changeDataFactory.create(db, psId1_1.getParentKey()));
+    clearGroups(psId1_1);
+    indexer.index(
+        changeDataFactory.create(db, project, psId1_1.getParentKey()));
 
-    if (!cfg.getBoolean("change", null, "getRelatedByAncestors", false)) {
-      // PS1,1 has no groups, so disappeared from related changes.
-      assertRelated(psId2_1);
-    }
+    // PS1,1 has no groups, so disappeared from related changes.
+    assertRelated(psId2_1);
 
     RevCommit c2_2 = testRepo.amend(c2_1)
         .add("c.txt", "2")
@@ -593,32 +646,28 @@
         changeAndCommit(psId1_1, c1_1, 1));
   }
 
-  private List<ChangeAndCommit> getRelated(PatchSet.Id ps) throws IOException {
+  private List<ChangeAndCommit> getRelated(PatchSet.Id ps) throws Exception {
     return getRelated(ps.getParentKey(), ps.get());
   }
 
   private List<ChangeAndCommit> getRelated(Change.Id changeId, int ps)
-      throws IOException {
+      throws Exception {
     String url = String.format("/changes/%d/revisions/%d/related",
         changeId.get(), ps);
-    return newGson().fromJson(adminSession.get(url).getReader(),
+    return newGson().fromJson(adminRestSession.get(url).getReader(),
         RelatedInfo.class).changes;
   }
 
-  private RevCommit parseBody(RevCommit c) throws IOException {
+  private RevCommit parseBody(RevCommit c) throws Exception {
     testRepo.getRevWalk().parseBody(c);
     return c;
   }
 
-  private PatchSet.Id getPatchSetId(ObjectId c) throws OrmException {
+  private PatchSet.Id getPatchSetId(ObjectId c) throws Exception {
     return getChange(c).change().currentPatchSetId();
   }
 
-  private PatchSet getPatchSet(Change c) throws OrmException {
-    return db.patchSets().get(c.currentPatchSetId());
-  }
-
-  private ChangeData getChange(ObjectId c) throws OrmException {
+  private ChangeData getChange(ObjectId c) throws Exception {
     return Iterables.getOnlyElement(queryProvider.get().byCommit(c));
   }
 
@@ -634,6 +683,23 @@
     return result;
   }
 
+  private void clearGroups(final PatchSet.Id psId) throws Exception {
+    try (BatchUpdate bu = updateFactory.create(
+        db, project, user(user), TimeUtil.nowTs())) {
+      bu.addOp(psId.getParentKey(), new BatchUpdate.Op() {
+        @Override
+        public boolean updateChange(ChangeContext ctx) throws OrmException {
+          PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+          psUtil.setGroups(ctx.getDb(), ctx.getUpdate(psId), ps,
+              ImmutableList.<String> of());
+          ctx.bumpLastUpdatedOn(false);
+          return true;
+        }
+      });
+      bu.execute();
+    }
+  }
+
   private void assertRelated(PatchSet.Id psId, ChangeAndCommit... expected)
       throws Exception {
     List<ChangeAndCommit> actual = getRelated(psId);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
index c4a07f2..3b4b6a8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
@@ -21,13 +21,11 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.PatchListKey;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -235,14 +233,14 @@
   }
 
   private List<PatchListEntry> getCurrentPatches(String changeId)
-      throws PatchListNotAvailableException, RestApiException {
+      throws Exception {
     return patchListCache
         .get(getKey(null, getCurrentRevisionId(changeId)), project)
         .getPatches();
   }
 
   private List<PatchListEntry> getPatches(ObjectId revisionIdA, ObjectId revisionIdB)
-      throws PatchListNotAvailableException {
+      throws Exception {
     return patchListCache.get(getKey(revisionIdA, revisionIdB), project)
         .getPatches();
   }
@@ -251,7 +249,7 @@
     return new PatchListKey(revisionIdA, revisionIdB, Whitespace.IGNORE_NONE);
   }
 
-  private ObjectId getCurrentRevisionId(String changeId) throws RestApiException {
+  private ObjectId getCurrentRevisionId(String changeId) throws Exception {
     return ObjectId.fromString(gApi.changes().id(changeId).get().currentRevision);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index 4cbf068..06170d0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -16,23 +16,32 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.testutil.ConfigSuite;
 
 import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Test;
 
-import java.io.IOException;
+import java.util.EnumSet;
+import java.util.List;
 
 public class SubmittedTogetherIT extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
 
   @Test
   public void returnsAncestors() throws Exception {
@@ -54,6 +63,17 @@
   }
 
   @Test
+  public void anonymousAncestors() throws Exception {
+    RevCommit a = commitBuilder().add("a", "1").message("change 1").create();
+    RevCommit b = commitBuilder().add("b", "1").message("change 2").create();
+    pushHead(testRepo, "refs/for/master", false);
+
+    setApiUserAnonymous();
+    assertSubmittedTogether(getChangeId(a));
+    assertSubmittedTogether(getChangeId(b), getChangeId(b), getChangeId(a));
+  }
+
+  @Test
   public void respectsWholeTopicAndAncestors() throws Exception {
     RevCommit initialHead = getRemoteHead();
     // Create two independent commits and push.
@@ -82,6 +102,144 @@
   }
 
   @Test
+  public void anonymousWholeTopic() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    RevCommit a = commitBuilder().add("a", "1").message("change 1").create();
+    pushHead(testRepo, "refs/for/master/" + name("topic"), false);
+    String id1 = getChangeId(a);
+
+    testRepo.reset(initialHead);
+    RevCommit b = commitBuilder().add("b", "1").message("change 2").create();
+    pushHead(testRepo, "refs/for/master/" + name("topic"), false);
+    String id2 = getChangeId(b);
+
+    setApiUserAnonymous();
+    if (isSubmitWholeTopicEnabled()) {
+      assertSubmittedTogether(id1, id2, id1);
+      assertSubmittedTogether(id2, id2, id1);
+    } else {
+      assertSubmittedTogether(id1);
+      assertSubmittedTogether(id2);
+    }
+  }
+
+  @Test
+  public void hiddenDraftInTopic() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    RevCommit a = commitBuilder().add("a", "1").message("change 1").create();
+    pushHead(testRepo, "refs/for/master/" + name("topic"), false);
+    String id1 = getChangeId(a);
+
+    testRepo.reset(initialHead);
+    commitBuilder().add("b", "2").message("invisible change").create();
+    pushHead(testRepo, "refs/drafts/master/" + name("topic"), false);
+
+    setApiUser(user);
+    SubmittedTogetherInfo result = gApi.changes()
+        .id(id1)
+        .submittedTogether(EnumSet.of(NON_VISIBLE_CHANGES));
+
+    if (isSubmitWholeTopicEnabled()) {
+      assertThat(result.changes).hasSize(1);
+      assertThat(result.changes.get(0).changeId).isEqualTo(id1);
+      assertThat(result.nonVisibleChanges).isEqualTo(1);
+    } else {
+      assertThat(result.changes).hasSize(0);
+      assertThat(result.nonVisibleChanges).isEqualTo(0);
+    }
+  }
+
+  @Test
+  public void hiddenDraftInTopicOldApi() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    RevCommit a = commitBuilder().add("a", "1").message("change 1").create();
+    pushHead(testRepo, "refs/for/master/" + name("topic"), false);
+    String id1 = getChangeId(a);
+
+    testRepo.reset(initialHead);
+    commitBuilder().add("b", "2").message("invisible change").create();
+    pushHead(testRepo, "refs/drafts/master/" + name("topic"), false);
+
+    setApiUser(user);
+    if (isSubmitWholeTopicEnabled()) {
+      exception.expect(AuthException.class);
+      exception.expectMessage(
+          "change would be submitted with a change that you cannot see");
+      gApi.changes().id(id1).submittedTogether();
+    } else {
+      List<ChangeInfo> result = gApi.changes().id(id1).submittedTogether();
+      assertThat(result).hasSize(0);
+    }
+  }
+
+  @Test
+  public void draftPatchSetInTopic() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    RevCommit a1 = commitBuilder().add("a", "1").message("change 1").create();
+    pushHead(testRepo, "refs/for/master/" + name("topic"), false);
+    String id1 = getChangeId(a1);
+
+    testRepo.reset(initialHead);
+    RevCommit parent = commitBuilder().message("parent").create();
+    pushHead(testRepo, "refs/for/master", false);
+    String parentId = getChangeId(parent);
+
+    // TODO(jrn): use insertChangeId(id1) once jgit TestRepository accepts
+    // the leading "I".
+    commitBuilder()
+        .insertChangeId(id1.substring(1))
+        .add("a", "2")
+        .message("draft patch set on change 1")
+        .create();
+    pushHead(testRepo, "refs/drafts/master/" + name("topic"), false);
+
+    testRepo.reset(initialHead);
+    RevCommit b = commitBuilder().message("change with same topic").create();
+    pushHead(testRepo, "refs/for/master/" + name("topic"), false);
+    String id2 = getChangeId(b);
+
+    if (isSubmitWholeTopicEnabled()) {
+      setApiUser(user);
+      assertSubmittedTogether(id2, id2, id1);
+      setApiUser(admin);
+      assertSubmittedTogether(id2, id2, id1, parentId);
+    } else {
+      setApiUser(user);
+      assertSubmittedTogether(id2);
+      setApiUser(admin);
+      assertSubmittedTogether(id2);
+    }
+  }
+
+  @Test
+  public void doNotRevealVisibleAncestorOfHiddenDraft() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    commitBuilder().message("parent").create();
+    pushHead(testRepo, "refs/for/master", false);
+
+    commitBuilder().message("draft").create();
+    pushHead(testRepo, "refs/drafts/master/" + name("topic"), false);
+
+    testRepo.reset(initialHead);
+    RevCommit change = commitBuilder().message("same topic").create();
+    pushHead(testRepo, "refs/for/master/" + name("topic"), false);
+    String id = getChangeId(change);
+
+    setApiUser(user);
+    SubmittedTogetherInfo result = gApi.changes()
+        .id(id)
+        .submittedTogether(EnumSet.of(NON_VISIBLE_CHANGES));
+    if (isSubmitWholeTopicEnabled()) {
+      assertThat(result.changes).hasSize(1);
+      assertThat(result.changes.get(0).changeId).isEqualTo(id);
+      assertThat(result.nonVisibleChanges).isEqualTo(2);
+    } else {
+      assertThat(result.changes).isEmpty();
+      assertThat(result.nonVisibleChanges).isEqualTo(0);
+    }
+  }
+
+  @Test
   public void testTopicChaining() throws Exception {
     RevCommit initialHead = getRemoteHead();
     // Create two independent commits and push.
@@ -191,13 +349,6 @@
     assertSubmittedTogether(id2, id2, id1);
   }
 
-  private RevCommit getRemoteHead() throws IOException {
-    try (Repository repo = repoManager.openRepository(project);
-        RevWalk rw = new RevWalk(repo)) {
-      return rw.parseCommit(repo.exactRef("refs/heads/master").getObjectId());
-    }
-  }
-
   private String getChangeId(RevCommit c) throws Exception {
     return GitUtil.getChangeId(testRepo, c).get();
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUCK
new file mode 100644
index 0000000..4fbc977
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUCK
@@ -0,0 +1,7 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  group = 'server_event',
+  srcs = glob(['*IT.java']),
+  labels = ['server'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUILD
new file mode 100644
index 0000000..ff0c51b
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUILD
@@ -0,0 +1,7 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'server_event',
+  srcs = glob(['*IT.java']),
+  labels = ['server'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
new file mode 100644
index 0000000..40ec9da
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
@@ -0,0 +1,271 @@
+// 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.server.event;
+
+import static com.google.common.truth.Truth.assertThat;
+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 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.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.events.CommentAddedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.Util;
+import com.google.inject.Inject;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+public class CommentAddedEventIT extends AbstractDaemonTest {
+
+  @Inject
+  private DynamicSet<CommentAddedListener> source;
+
+  private final LabelType label = category("CustomLabel",
+      value(1, "Positive"),
+      value(0, "No score"),
+      value(-1, "Negative"));
+
+  private final LabelType pLabel = category("CustomLabel2",
+      value(1, "Positive"),
+      value(0, "No score"));
+
+  private RegistrationHandle eventListenerRegistration;
+  private CommentAddedListener.Event lastCommentAddedEvent;
+
+  @Before
+  public void setUp() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    AccountGroup.UUID anonymousUsers =
+        SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
+    Util.allow(cfg, Permission.forLabel(label.getName()), -1, 1, anonymousUsers,
+        "refs/heads/*");
+    Util.allow(cfg, Permission.forLabel(pLabel.getName()), 0, 1, anonymousUsers,
+        "refs/heads/*");
+    saveProjectConfig(project, cfg);
+
+    eventListenerRegistration = source.add(new CommentAddedListener() {
+      @Override
+      public void onCommentAdded(Event event) {
+        lastCommentAddedEvent = event;
+      }
+    });
+  }
+
+  @After
+  public void cleanup() {
+    eventListenerRegistration.remove();
+  }
+
+  private void saveLabelConfig() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().put(label.getName(), label);
+    cfg.getLabelSections().put(pLabel.getName(), pLabel);
+    saveProjectConfig(project, cfg);
+  }
+
+  /* Need to lookup info for the label under test since there can be multiple
+   * labels defined.  By default Gerrit already has a Code-Review label.
+   */
+  private ApprovalValues getApprovalValues(LabelType label) {
+    ApprovalValues res = new ApprovalValues();
+    ApprovalInfo info =
+        lastCommentAddedEvent.getApprovals().get(label.getName());
+    if (info != null) {
+      res.value = info.value;
+    }
+    info = lastCommentAddedEvent.getOldApprovals().get(label.getName());
+    if (info != null) {
+      res.oldValue = info.value;
+    }
+    return res;
+  }
+
+  @Test
+  public void newChangeWithVote() throws Exception {
+    saveLabelConfig();
+
+    // push a new change with -1 vote
+    PushOneCommit.Result r = createChange();
+    ReviewInput reviewInput = new ReviewInput().label(
+        label.getName(), (short)-1);
+    revision(r).review(reviewInput);
+    ApprovalValues attr = getApprovalValues(label);
+    assertThat(attr.oldValue).isEqualTo(0);
+    assertThat(attr.value).isEqualTo(-1);
+    assertThat(lastCommentAddedEvent.getComment()).isEqualTo(
+        String.format("Patch Set 1: %s-1", label.getName()));
+  }
+
+  @Test
+  public void newPatchSetWithVote() throws Exception {
+    saveLabelConfig();
+
+    // push a new change
+    PushOneCommit.Result r = createChange();
+    ReviewInput reviewInput = new ReviewInput().message(label.getName());
+    revision(r).review(reviewInput);
+
+    // push a new revision with +1 vote
+    ChangeInfo c = get(r.getChangeId());
+    r = amendChange(c.changeId);
+    reviewInput = new ReviewInput().label(
+        label.getName(), (short)1);
+    revision(r).review(reviewInput);
+    ApprovalValues attr = getApprovalValues(label);
+    assertThat(attr.oldValue).isEqualTo(0);
+    assertThat(attr.value).isEqualTo(1);
+    assertThat(lastCommentAddedEvent.getComment()).isEqualTo(
+        String.format("Patch Set 2: %s+1", label.getName()));
+  }
+
+  @Test
+  public void reviewChange() throws Exception {
+    saveLabelConfig();
+
+    // push a change
+    PushOneCommit.Result r = createChange();
+
+    // review with message only, do not apply votes
+    ReviewInput reviewInput = new ReviewInput().message(label.getName());
+    revision(r).review(reviewInput);
+    // reply message only so vote is shown as 0
+    ApprovalValues attr = getApprovalValues(label);
+    assertThat(attr.oldValue).isNull();
+    assertThat(attr.value).isEqualTo(0);
+    assertThat(lastCommentAddedEvent.getComment()).isEqualTo(
+        String.format("Patch Set 1:\n\n%s", label.getName()));
+
+    // transition from un-voted to -1 vote
+    reviewInput = new ReviewInput().label(label.getName(), -1);
+    revision(r).review(reviewInput);
+    attr = getApprovalValues(label);
+    assertThat(attr.oldValue).isEqualTo(0);
+    assertThat(attr.value).isEqualTo(-1);
+    assertThat(lastCommentAddedEvent.getComment()).isEqualTo(
+        String.format("Patch Set 1: %s-1", label.getName()));
+
+    // transition vote from -1 to 0
+    reviewInput = new ReviewInput().label(label.getName(), 0);
+    revision(r).review(reviewInput);
+    attr = getApprovalValues(label);
+    assertThat(attr.oldValue).isEqualTo(-1);
+    assertThat(attr.value).isEqualTo(0);
+    assertThat(lastCommentAddedEvent.getComment()).isEqualTo(
+        String.format("Patch Set 1: -%s", label.getName()));
+
+    // transition vote from 0 to 1
+    reviewInput = new ReviewInput().label(label.getName(), 1);
+    revision(r).review(reviewInput);
+    attr = getApprovalValues(label);
+    assertThat(attr.oldValue).isEqualTo(0);
+    assertThat(attr.value).isEqualTo(1);
+    assertThat(lastCommentAddedEvent.getComment()).isEqualTo(
+        String.format("Patch Set 1: %s+1", label.getName()));
+
+    // transition vote from 1 to -1
+    reviewInput = new ReviewInput().label(label.getName(), -1);
+    revision(r).review(reviewInput);
+    attr = getApprovalValues(label);
+    assertThat(attr.oldValue).isEqualTo(1);
+    assertThat(attr.value).isEqualTo(-1);
+    assertThat(lastCommentAddedEvent.getComment()).isEqualTo(
+        String.format("Patch Set 1: %s-1", label.getName()));
+
+    // review with message only, do not apply votes
+    reviewInput = new ReviewInput().message(label.getName());
+    revision(r).review(reviewInput);
+    attr = getApprovalValues(label);
+    assertThat(attr.oldValue).isNull();  // no vote change so not included
+    assertThat(attr.value).isEqualTo(-1);
+    assertThat(lastCommentAddedEvent.getComment()).isEqualTo(
+        String.format("Patch Set 1:\n\n%s", label.getName()));
+  }
+
+  @Test
+  public void reviewChange_MultipleVotes() throws Exception {
+    saveLabelConfig();
+    PushOneCommit.Result r = createChange();
+    ReviewInput reviewInput = new ReviewInput().label(label.getName(), -1);
+    reviewInput.message = label.getName();
+    revision(r).review(reviewInput);
+
+    ChangeInfo c = get(r.getChangeId());
+    LabelInfo q = c.labels.get(label.getName());
+    assertThat(q.all).hasSize(1);
+    ApprovalValues labelAttr = getApprovalValues(label);
+    assertThat(labelAttr.oldValue).isEqualTo(0);
+    assertThat(labelAttr.value).isEqualTo(-1);
+    assertThat(lastCommentAddedEvent.getComment()).isEqualTo(
+        String.format("Patch Set 1: %s-1\n\n%s",
+            label.getName(), label.getName()));
+
+    // there should be 3 approval labels (label, pLabel, and CRVV)
+    assertThat(lastCommentAddedEvent.getApprovals()).hasSize(3);
+
+    // check the approvals that were not voted on
+    ApprovalValues pLabelAttr = getApprovalValues(pLabel);
+    assertThat(pLabelAttr.oldValue).isNull();
+    assertThat(pLabelAttr.value).isEqualTo(0);
+
+    LabelType crLabel = LabelType.withDefaultValues("Code-Review");
+    ApprovalValues crlAttr = getApprovalValues(crLabel);
+    assertThat(crlAttr.oldValue).isNull();
+    assertThat(crlAttr.value).isEqualTo(0);
+
+    // update pLabel approval
+    reviewInput = new ReviewInput().label(pLabel.getName(), 1);
+    reviewInput.message = pLabel.getName();
+    revision(r).review(reviewInput);
+
+    c = get(r.getChangeId());
+    q = c.labels.get(label.getName());
+    assertThat(q.all).hasSize(1);
+    pLabelAttr = getApprovalValues(pLabel);
+    assertThat(pLabelAttr.oldValue).isEqualTo(0);
+    assertThat(pLabelAttr.value).isEqualTo(1);
+    assertThat(lastCommentAddedEvent.getComment()).isEqualTo(
+        String.format("Patch Set 1: %s+1\n\n%s",
+            pLabel.getName(), pLabel.getName()));
+
+    // check the approvals that were not voted on
+    labelAttr = getApprovalValues(label);
+    assertThat(labelAttr.oldValue).isNull();
+    assertThat(labelAttr.value).isEqualTo(-1);
+
+    crlAttr = getApprovalValues(crLabel);
+    assertThat(crlAttr.oldValue).isNull();
+    assertThat(crlAttr.value).isEqualTo(0);
+  }
+
+  private static class ApprovalValues {
+    Integer value;
+    Integer oldValue;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/BUCK
new file mode 100644
index 0000000..d9976e5
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/BUCK
@@ -0,0 +1,7 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  group = 'server_notedb',
+  srcs = glob(['*IT.java']),
+  labels = ['notedb', 'server'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/BUILD
new file mode 100644
index 0000000..17c4cdc
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/BUILD
@@ -0,0 +1,7 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'server_notedb',
+  srcs = glob(['*IT.java']),
+  labels = ['notedb', 'server'],
+)
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
new file mode 100644
index 0000000..b443e66
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -0,0 +1,1089 @@
+// 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.server.notedb;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
+import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+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.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+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.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;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.RepoRefCache;
+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;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.NoteDbChecker;
+import com.google.gerrit.testutil.NoteDbMode;
+import com.google.gerrit.testutil.TestChanges;
+import com.google.gerrit.testutil.TestTimeUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+public class ChangeRebuilderIT extends AbstractDaemonTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    Config cfg = new Config();
+    cfg.setBoolean("noteDb", null, "testRebuilderWrapper", true);
+    return cfg;
+  }
+
+  @Inject
+  private AllUsersName allUsers;
+
+  @Inject
+  private NoteDbChecker checker;
+
+  @Inject
+  private Rebuild rebuildHandler;
+
+  @Inject
+  private Provider<ReviewDb> dbProvider;
+
+  @Inject
+  private PatchLineCommentsUtil plcUtil;
+
+  @Inject
+  private Provider<PostReview> postReview;
+
+  @Inject
+  private TestChangeRebuilderWrapper rebuilderWrapper;
+
+  @Inject
+  private BatchUpdate.Factory batchUpdateFactory;
+
+  @Inject
+  private Sequences seq;
+
+  @Before
+  public void setUp() throws Exception {
+    assume().that(NoteDbMode.readWrite()).isFalse();
+    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+    setNotesMigration(false, false);
+  }
+
+  @After
+  public void tearDown() {
+    TestTimeUtil.useSystemTime();
+  }
+
+  @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
+  public void changeFields() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    gApi.changes().id(id.get()).topic(name("a-topic"));
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void patchSets() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    r = amendChange(r.getChangeId());
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void publishedComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    putComment(user, id, 1, "comment");
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void patchSetWithNullGroups() throws Exception {
+    Timestamp ts = TimeUtil.nowTs();
+    Change c = TestChanges.newChange(project, user.getId(), seq.nextChangeId());
+    c.setCreatedOn(ts);
+    c.setLastUpdatedOn(ts);
+    PatchSet ps = TestChanges.newPatchSet(
+        c.currentPatchSetId(), "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+        user.getId());
+    ps.setCreatedOn(ts);
+    db.changes().insert(Collections.singleton(c));
+    db.patchSets().insert(Collections.singleton(ps));
+
+    assertThat(ps.getGroups()).isEmpty();
+    checker.rebuildAndCheckChanges(c.getId());
+  }
+
+  @Test
+  public void draftComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    putDraft(user, id, 1, "comment");
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void draftAndPublishedComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    putDraft(user, id, 1, "draft comment");
+    putComment(user, id, 1, "published comment");
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void publishDraftComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    putDraft(user, id, 1, "draft comment");
+    publishDrafts(user, id);
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void nullAccountId() throws Exception {
+    PushOneCommit.Result r = createChange();
+    PatchSet.Id psId = r.getPatchSetId();
+    Change.Id id = psId.getParentKey();
+
+    // Events need to be otherwise identical for the account ID to be compared.
+    ChangeMessage msg1 =
+        insertMessage(id, psId, user.getId(), TimeUtil.nowTs(), "message 1");
+    insertMessage(id, psId, null, msg1.getWrittenOn(), "message 2");
+
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void nullPatchSetId() throws Exception {
+    PushOneCommit.Result r = createChange();
+    PatchSet.Id psId1 = r.getPatchSetId();
+    Change.Id id = psId1.getParentKey();
+
+    // Events need to be otherwise identical for the PatchSet.ID to be compared.
+    ChangeMessage msg1 =
+        insertMessage(id, null, user.getId(), TimeUtil.nowTs(), "message 1");
+    insertMessage(id, null, user.getId(), msg1.getWrittenOn(), "message 2");
+
+    PatchSet.Id psId2 = amendChange(r.getChangeId()).getPatchSetId();
+
+    ChangeMessage msg3 =
+        insertMessage(id, null, user.getId(), TimeUtil.nowTs(), "message 3");
+    insertMessage(id, null, user.getId(), msg3.getWrittenOn(), "message 4");
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    Map<String, PatchSet.Id> psIds = new HashMap<>();
+    for (ChangeMessage msg : notes.getChangeMessages()) {
+      PatchSet.Id psId = msg.getPatchSetId();
+      assertThat(psId).named("patchset for " + msg).isNotNull();
+      psIds.put(msg.getMessage(), psId);
+    }
+    // Patch set IDs were replaced during conversion process.
+    assertThat(psIds).containsEntry("message 1", psId1);
+    assertThat(psIds).containsEntry("message 2", psId1);
+    assertThat(psIds).containsEntry("message 3", psId2);
+    assertThat(psIds).containsEntry("message 4", psId2);
+  }
+
+  @Test
+  public void noWriteToNewRef() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    checker.assertNoChangeRef(project, id);
+
+    setNotesMigration(true, false);
+    gApi.changes().id(id.get()).topic(name("a-topic"));
+
+    // First write doesn't create the ref, but rebuilding works.
+    checker.assertNoChangeRef(project, id);
+    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isNull();
+    checker.rebuildAndCheckChanges(id);
+
+    // Now that there is a ref, writes are "turned on" for this change, and
+    // NoteDb stays up to date without explicit rebuilding.
+    gApi.changes().id(id.get()).topic(name("new-topic"));
+    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isNotNull();
+    checker.checkChanges(id);
+  }
+
+  @Test
+  public void restApiNotFoundWhenNoteDbDisabled() throws Exception {
+    PushOneCommit.Result r = createChange();
+    exception.expect(ResourceNotFoundException.class);
+    rebuildHandler.apply(
+        parseChangeResource(r.getChangeId()),
+        new Rebuild.Input());
+  }
+
+  @Test
+  public void rebuildViaRestApi() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    setNotesMigration(true, false);
+
+    checker.assertNoChangeRef(project, id);
+    rebuildHandler.apply(
+        parseChangeResource(r.getChangeId()),
+        new Rebuild.Input());
+    checker.checkChanges(id);
+  }
+
+  @Test
+  public void writeToNewRefForNewChange() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    Change.Id id1 = r1.getPatchSetId().getParentKey();
+
+    setNotesMigration(true, false);
+    gApi.changes().id(id1.get()).topic(name("a-topic"));
+    PushOneCommit.Result r2 = createChange();
+    Change.Id id2 = r2.getPatchSetId().getParentKey();
+
+    // Second change was created after NoteDb writes were turned on, so it was
+    // allowed to write to a new ref.
+    checker.checkChanges(id2);
+
+    // First change was created before NoteDb writes were turned on, so its meta
+    // ref doesn't exist until a manual rebuild.
+    checker.assertNoChangeRef(project, id1);
+    checker.rebuildAndCheckChanges(id1);
+  }
+
+  @Test
+  public void noteDbChangeState() throws Exception {
+    setNotesMigration(true, true);
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+
+    ObjectId changeMetaId = getMetaRef(project, changeMetaRef(id));
+    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(
+        changeMetaId.name());
+
+    putDraft(user, id, 1, "comment by user");
+    ObjectId userDraftsId = getMetaRef(
+        allUsers, refsDraftComments(id, user.getId()));
+    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(
+        changeMetaId.name()
+        + "," + user.getId() + "=" + userDraftsId.name());
+
+    putDraft(admin, id, 2, "comment by admin");
+    ObjectId adminDraftsId = getMetaRef(
+        allUsers, refsDraftComments(id, admin.getId()));
+    assertThat(admin.getId().get()).isLessThan(user.getId().get());
+    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(
+        changeMetaId.name()
+        + "," + admin.getId() + "=" + adminDraftsId.name()
+        + "," + user.getId() + "=" + userDraftsId.name());
+
+    putDraft(admin, id, 2, "revised comment by admin");
+    adminDraftsId = getMetaRef(
+        allUsers, refsDraftComments(id, admin.getId()));
+    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(
+        changeMetaId.name()
+        + "," + admin.getId() + "=" + adminDraftsId.name()
+        + "," + user.getId() + "=" + userDraftsId.name());
+  }
+
+  @Test
+  public void rebuildAutomaticallyWhenChangeOutOfDate() throws Exception {
+    setNotesMigration(true, true);
+
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    assertChangeUpToDate(true, id);
+
+    // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
+    setNotesMigration(false, false);
+    gApi.changes().id(id.get()).topic(name("a-topic"));
+    setInvalidNoteDbState(id);
+    assertChangeUpToDate(false, id);
+
+    // On next NoteDb read, the change is transparently rebuilt.
+    setNotesMigration(true, true);
+    assertThat(gApi.changes().id(id.get()).info().topic)
+        .isEqualTo(name("a-topic"));
+    assertChangeUpToDate(true, id);
+
+    // Check that the bundles are equal.
+    ChangeBundle actual = ChangeBundle.fromNotes(
+        plcUtil, notesFactory.create(dbProvider.get(), project, id));
+    ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
+    assertThat(actual.differencesFrom(expected)).isEmpty();
+  }
+
+  @Test
+  public void rebuildAutomaticallyWithinBatchUpdate() throws Exception {
+    setNotesMigration(true, true);
+
+    PushOneCommit.Result r = createChange();
+    final Change.Id id = r.getPatchSetId().getParentKey();
+    assertChangeUpToDate(true, id);
+
+    // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
+    setNotesMigration(false, false);
+    gApi.changes().id(id.get()).topic(name("a-topic"));
+    setInvalidNoteDbState(id);
+    assertChangeUpToDate(false, id);
+
+    // Next NoteDb read comes inside the transaction started by BatchUpdate. In
+    // reality this could be caused by a failed update happening between when
+    // the change is parsed by ChangesCollection and when the BatchUpdate
+    // executes. We simulate it here by using BatchUpdate directly and not going
+    // through an API handler.
+    setNotesMigration(true, true);
+    final String msg = "message from BatchUpdate";
+    try (BatchUpdate bu = batchUpdateFactory.create(db, project,
+          identifiedUserFactory.create(user.getId()), TimeUtil.nowTs())) {
+      bu.addOp(id, new BatchUpdate.Op() {
+        @Override
+        public boolean updateChange(ChangeContext ctx) throws OrmException {
+          PatchSet.Id psId = ctx.getChange().currentPatchSetId();
+          ChangeMessage cm = new ChangeMessage(
+              new ChangeMessage.Key(id, ChangeUtil.messageUUID(ctx.getDb())),
+                  ctx.getAccountId(), ctx.getWhen(), psId);
+          cm.setMessage(msg);
+          ctx.getDb().changeMessages().insert(Collections.singleton(cm));
+          ctx.getUpdate(psId).setChangeMessage(msg);
+          return true;
+        }
+      });
+      bu.execute();
+    }
+    // As an implementation detail, change wasn't actually rebuilt inside the
+    // BatchUpdate transaction, but it was rebuilt during read for the
+    // subsequent reindex. Thus it's impossible to actually observe an
+    // out-of-date state in the caller.
+    assertChangeUpToDate(true, id);
+
+    // Check that the bundles are equal.
+    ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
+    ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notes);
+    ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
+    assertThat(actual.differencesFrom(expected)).isEmpty();
+    assertThat(
+            Iterables.transform(
+                notes.getChangeMessages(),
+                new Function<ChangeMessage, String>() {
+                  @Override
+                  public String apply(ChangeMessage in) {
+                    return in.getMessage();
+                  }
+                }))
+        .contains(msg);
+  }
+
+  @Test
+  public void rebuildIgnoresErrorIfChangeIsUpToDateAfter() throws Exception {
+    setNotesMigration(true, true);
+
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    assertChangeUpToDate(true, id);
+
+    // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
+    setNotesMigration(false, false);
+    gApi.changes().id(id.get()).topic(name("a-topic"));
+    setInvalidNoteDbState(id);
+    assertChangeUpToDate(false, id);
+
+    // Force the next rebuild attempt to fail but also rebuild the change in the
+    // background.
+    rebuilderWrapper.stealNextUpdate();
+    setNotesMigration(true, true);
+    assertThat(gApi.changes().id(id.get()).info().topic)
+        .isEqualTo(name("a-topic"));
+    assertChangeUpToDate(true, id);
+
+    // Check that the bundles are equal.
+    ChangeBundle actual = ChangeBundle.fromNotes(
+        plcUtil, notesFactory.create(dbProvider.get(), project, id));
+    ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
+    assertThat(actual.differencesFrom(expected)).isEmpty();
+  }
+
+  @Test
+  public void rebuildReturnsCorrectResultEvenIfSavingToNoteDbFailed()
+      throws Exception {
+    setNotesMigration(true, true);
+
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    assertChangeUpToDate(true, id);
+    ObjectId oldMetaId = getMetaRef(project, changeMetaRef(id));
+
+    // Make a ReviewDb change behind NoteDb's back.
+    setNotesMigration(false, false);
+    gApi.changes().id(id.get()).topic(name("a-topic"));
+    setInvalidNoteDbState(id);
+    assertChangeUpToDate(false, id);
+    assertThat(getMetaRef(project, changeMetaRef(id))).isEqualTo(oldMetaId);
+
+    // Force the next rebuild attempt to fail.
+    rebuilderWrapper.failNextUpdate();
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
+
+    // Not up to date, but the actual returned state matches anyway.
+    assertChangeUpToDate(false, id);
+    assertThat(getMetaRef(project, changeMetaRef(id))).isEqualTo(oldMetaId);
+    ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notes);
+    ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
+    assertThat(actual.differencesFrom(expected)).isEmpty();
+    assertChangeUpToDate(false, id);
+
+    // Another rebuild attempt succeeds
+    notesFactory.create(dbProvider.get(), project, id);
+    assertThat(getMetaRef(project, changeMetaRef(id))).isNotEqualTo(oldMetaId);
+    assertChangeUpToDate(true, id);
+  }
+
+  @Test
+  public void rebuildReturnsDraftResultWhenRebuildingInChangeNotesFails()
+      throws Exception {
+    setNotesMigration(true, true);
+
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    putDraft(user, id, 1, "comment by user");
+    assertChangeUpToDate(true, id);
+
+    ObjectId oldMetaId =
+        getMetaRef(allUsers, refsDraftComments(id, user.getId()));
+
+    // Add a draft behind NoteDb's back.
+    setNotesMigration(false, false);
+    putDraft(user, id, 1, "second comment by user");
+    setInvalidNoteDbState(id);
+    assertDraftsUpToDate(false, id, user);
+    assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId())))
+        .isEqualTo(oldMetaId);
+
+    // Force the next rebuild attempt to fail (in ChangeNotes).
+    rebuilderWrapper.failNextUpdate();
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
+    notes.getDraftComments(user.getId());
+    assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId())))
+        .isEqualTo(oldMetaId);
+
+    // Not up to date, but the actual returned state matches anyway.
+    assertDraftsUpToDate(false, id, user);
+    ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notes);
+    ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
+    assertThat(actual.differencesFrom(expected)).isEmpty();
+
+    // Another rebuild attempt succeeds
+    notesFactory.create(dbProvider.get(), project, id);
+    assertChangeUpToDate(true, id);
+    assertDraftsUpToDate(true, id, user);
+    assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId())))
+        .isNotEqualTo(oldMetaId);
+  }
+
+  @Test
+  public void rebuildReturnsDraftResultWhenRebuildingInDraftCommentNotesFails()
+      throws Exception {
+    setNotesMigration(true, true);
+
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    putDraft(user, id, 1, "comment by user");
+    assertChangeUpToDate(true, id);
+
+    ObjectId oldMetaId =
+        getMetaRef(allUsers, refsDraftComments(id, user.getId()));
+
+    // Add a draft behind NoteDb's back.
+    setNotesMigration(false, false);
+    putDraft(user, id, 1, "second comment by user");
+
+    ReviewDb db = getUnwrappedDb();
+    Change c = db.changes().get(id);
+    // Leave change meta ID alone so DraftCommentNotes does the rebuild.
+    NoteDbChangeState bogusState = new NoteDbChangeState(
+        id, NoteDbChangeState.parse(c).getChangeMetaId(),
+        ImmutableMap.<Account.Id, ObjectId>of(
+            user.getId(),
+            ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")));
+    c.setNoteDbState(bogusState.toString());
+    db.changes().update(Collections.singleton(c));
+
+    assertDraftsUpToDate(false, id, user);
+    assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId())))
+        .isEqualTo(oldMetaId);
+
+    // Force the next rebuild attempt to fail (in DraftCommentNotes).
+    rebuilderWrapper.failNextUpdate();
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
+    notes.getDraftComments(user.getId());
+    assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId())))
+        .isEqualTo(oldMetaId);
+
+    // Not up to date, but the actual returned state matches anyway.
+    assertChangeUpToDate(true, id);
+    assertDraftsUpToDate(false, id, user);
+    ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notes);
+    ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
+    assertThat(actual.differencesFrom(expected)).isEmpty();
+
+    // Another rebuild attempt succeeds
+    notesFactory.create(dbProvider.get(), project, id)
+        .getDraftComments(user.getId());
+    assertChangeUpToDate(true, id);
+    assertDraftsUpToDate(true, id, user);
+    assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId())))
+        .isNotEqualTo(oldMetaId);
+  }
+
+  @Test
+  public void rebuildAutomaticallyWhenDraftsOutOfDate() throws Exception {
+    setNotesMigration(true, true);
+    setApiUser(user);
+
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    putDraft(user, id, 1, "comment");
+    assertDraftsUpToDate(true, id, user);
+
+    // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
+    setNotesMigration(false, false);
+    putDraft(user, id, 1, "comment");
+    setInvalidNoteDbState(id);
+    assertDraftsUpToDate(false, id, user);
+
+    // On next NoteDb read, the drafts are transparently rebuilt.
+    setNotesMigration(true, true);
+    assertThat(gApi.changes().id(id.get()).current().drafts())
+        .containsKey(PushOneCommit.FILE_NAME);
+    assertDraftsUpToDate(true, id, user);
+  }
+
+  @Test
+  public void pushCert() throws Exception {
+    // We don't have the code in our test harness to do signed pushes, so just
+    // use a hard-coded cert. This cert was actually generated by C git 2.2.0
+    // (albeit not for sending to Gerrit).
+    String cert = "certificate version 0.1\n"
+        + "pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700\n"
+        + "pushee git://localhost/repo.git\n"
+        + "nonce 1433954361-bde756572d665bba81d8\n"
+        + "\n"
+        + "0000000000000000000000000000000000000000"
+        + "b981a177396fb47345b7df3e4d3f854c6bea7"
+        + "s/heads/master\n"
+        + "-----BEGIN PGP SIGNATURE-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa\n"
+        + "9tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7\n"
+        + "htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V\n"
+        + "4ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG\n"
+        + "IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY\n"
+        + "+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=\n"
+        + "=XFeC\n"
+        + "-----END PGP SIGNATURE-----\n";
+
+    PushOneCommit.Result r = createChange();
+    PatchSet.Id psId = r.getPatchSetId();
+    Change.Id id = psId.getParentKey();
+
+    PatchSet ps = db.patchSets().get(psId);
+    ps.setPushCertificate(cert);
+    db.patchSets().update(Collections.singleton(ps));
+    indexer.index(db, project, id);
+
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void emptyTopic() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    Change c = db.changes().get(id);
+    assertThat(c.getTopic()).isNull();
+    c.setTopic("");
+    db.changes().update(Collections.singleton(c));
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+
+    // Rebuild and check was successful, but NoteDb doesn't support storing an
+    // empty topic, so it comes out as null.
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    assertThat(notes.getChange().getTopic()).isNull();
+  }
+
+  @Test
+  public void commentBeforeFirstPatchSet() throws Exception {
+    PushOneCommit.Result r = createChange();
+    PatchSet.Id psId = r.getPatchSetId();
+    Change.Id id = psId.getParentKey();
+
+    Change c = db.changes().get(id);
+    c.setCreatedOn(new Timestamp(c.getCreatedOn().getTime() - 5000));
+    db.changes().update(Collections.singleton(c));
+    indexer.index(db, project, id);
+
+    ReviewInput rin = new ReviewInput();
+    rin.message = "comment";
+
+    Timestamp ts = new Timestamp(c.getCreatedOn().getTime() + 2000);
+    RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
+    postReview.get().apply(revRsrc, rin, ts);
+
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void commentPredatingChangeBySomeoneOtherThanOwner() throws Exception {
+    PushOneCommit.Result r = createChange();
+    PatchSet.Id psId = r.getPatchSetId();
+    Change.Id id = psId.getParentKey();
+    Change c = db.changes().get(id);
+
+    ReviewInput rin = new ReviewInput();
+    rin.message = "comment";
+
+    Timestamp ts = new Timestamp(c.getCreatedOn().getTime() - 10000);
+    RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
+    setApiUser(user);
+    postReview.get().apply(revRsrc, rin, ts);
+
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void noteDbUsesOriginalSubjectFromPatchSetAndIgnoresChangeField()
+      throws Exception {
+    PushOneCommit.Result r = createChange();
+    String orig = r.getChange().change().getSubject();
+    r = pushFactory.create(
+            db, admin.getIdent(), testRepo, orig + " v2",
+            PushOneCommit.FILE_NAME, "new contents", r.getChangeId())
+        .to("refs/heads/master");
+    r.assertOkStatus();
+
+    PatchSet.Id psId = r.getPatchSetId();
+    Change.Id id = psId.getParentKey();
+    Change c = db.changes().get(id);
+
+    c.setCurrentPatchSet(psId, c.getSubject(), "Bogus original subject");
+    db.changes().update(Collections.singleton(c));
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    Change nc = notes.getChange();
+    assertThat(nc.getSubject()).isEqualTo(c.getSubject());
+    assertThat(nc.getSubject()).isEqualTo(orig + " v2");
+    assertThat(nc.getOriginalSubject()).isNotEqualTo(c.getOriginalSubject());
+    assertThat(nc.getOriginalSubject()).isEqualTo(orig);
+  }
+
+  @Test
+  public void deleteDraftPS1WithNoOtherEntities() throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result r = push.to("refs/drafts/master");
+    push = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT, "b.txt", "4711", r.getChangeId());
+    r = push.to("refs/drafts/master");
+    PatchSet.Id psId = r.getPatchSetId();
+    Change.Id id = psId.getParentKey();
+
+    gApi.changes().id(r.getChangeId()).revision(1).delete();
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    assertThat(notes.getPatchSets().keySet()).containsExactly(psId);
+  }
+
+  @Test
+  public void ignorePatchLineCommentsOnPatchSet0() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change change = r.getChange().change();
+    Change.Id id = change.getId();
+
+    PatchLineComment comment = new PatchLineComment(
+        new PatchLineComment.Key(
+            new Patch.Key(new PatchSet.Id(id, 0), PushOneCommit.FILE_NAME),
+            "uuid"),
+        0, user.getId(), null, TimeUtil.nowTs());
+    comment.setSide((short) 1);
+    comment.setMessage("message");
+    comment.setStatus(PatchLineComment.Status.PUBLISHED);
+    db.patchComments().insert(Collections.singleton(comment));
+    indexer.index(db, change.getProject(), id);
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    assertThat(notes.getComments()).isEmpty();
+  }
+
+  @Test
+  public void skipPatchSetsGreaterThanCurrentPatchSet() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change change = r.getChange().change();
+    Change.Id id = change.getId();
+
+    PatchSet badPs =
+        new PatchSet(new PatchSet.Id(id, change.currentPatchSetId().get() + 1));
+    badPs.setCreatedOn(TimeUtil.nowTs());
+    badPs.setUploader(new Account.Id(12345));
+    badPs.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+    db.patchSets().insert(Collections.singleton(badPs));
+    indexer.index(db, change.getProject(), id);
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    assertThat(notes.getPatchSets().keySet())
+        .containsExactly(change.currentPatchSetId());
+  }
+
+  @Test
+  public void leadingSpacesInSubject() throws Exception {
+    String subj = "   " + PushOneCommit.SUBJECT;
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+        subj, PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT);
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+    Change change = r.getChange().change();
+    assertThat(change.getSubject()).isEqualTo(subj);
+    Change.Id id = r.getPatchSetId().getParentKey();
+
+    checker.rebuildAndCheckChanges(id);
+
+    setNotesMigration(true, true);
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    assertThat(notes.getChange().getSubject()).isNotEqualTo(subj);
+    assertThat(notes.getChange().getSubject()).isEqualTo(PushOneCommit.SUBJECT);
+  }
+
+  @Test
+  public void createWithAutoRebuildingDisabled() throws Exception {
+    ReviewDb oldDb = db;
+    setNotesMigration(true, true);
+
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    ChangeNotes oldNotes = notesFactory.create(db, project, id);
+
+    // Make a ReviewDb change behind NoteDb's back.
+    Change c = oldDb.changes().get(id);
+    assertThat(c.getTopic()).isNull();
+    String topic = name("a-topic");
+    c.setTopic(topic);
+    oldDb.changes().update(Collections.singleton(c));
+
+    c = oldDb.changes().get(c.getId());
+    ChangeNotes newNotes =
+        notesFactory.createWithAutoRebuildingDisabled(c, null);
+    assertThat(newNotes.getChange().getTopic()).isNotEqualTo(topic);
+    assertThat(newNotes.getChange().getTopic())
+        .isEqualTo(oldNotes.getChange().getTopic());
+  }
+
+  @Test
+  public void rebuildDeletesOldDraftRefs() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    putDraft(user, id, 1, "comment");
+
+    Account.Id otherAccountId = new Account.Id(user.getId().get() + 1234);
+    String otherDraftRef = refsDraftComments(id, otherAccountId);
+
+    try (Repository repo = repoManager.openRepository(allUsers);
+        ObjectInserter ins = repo.newObjectInserter()) {
+      ObjectId sha = ins.insert(OBJ_BLOB, "garbage data".getBytes(UTF_8));
+      ins.flush();
+      RefUpdate ru = repo.updateRef(otherDraftRef);
+      ru.setExpectedOldObjectId(ObjectId.zeroId());
+      ru.setNewObjectId(sha);
+      assertThat(ru.update()).isEqualTo(RefUpdate.Result.NEW);
+    }
+
+    checker.rebuildAndCheckChanges(id);
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(otherDraftRef)).isNull();
+    }
+  }
+
+  @Test
+  public void failWhenWritesDisabled() throws Exception {
+    setNotesMigration(true, true);
+
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    assertChangeUpToDate(true, id);
+    assertThat(gApi.changes().id(id.get()).info().topic).isNull();
+
+    // Turning off writes causes failure.
+    setNotesMigration(false, true);
+    try {
+      gApi.changes().id(id.get()).topic(name("a-topic"));
+      fail("Expected write to fail");
+    } catch (RestApiException e) {
+      assertChangesReadOnly(e);
+    }
+
+    // Update was not written.
+    assertThat(gApi.changes().id(id.get()).info().topic).isNull();
+    assertChangeUpToDate(true, id);
+  }
+
+  @Test
+  public void rebuildWhenWritesDisabledWorksButDoesNotWrite() throws Exception {
+    setNotesMigration(true, true);
+
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    assertChangeUpToDate(true, id);
+
+    // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
+    setNotesMigration(false, false);
+    gApi.changes().id(id.get()).topic(name("a-topic"));
+    setInvalidNoteDbState(id);
+    assertChangeUpToDate(false, id);
+
+    // On next NoteDb read, change is rebuilt in-memory but not stored.
+    setNotesMigration(false, true);
+    assertThat(gApi.changes().id(id.get()).info().topic)
+        .isEqualTo(name("a-topic"));
+    assertChangeUpToDate(false, id);
+
+    // Attempting to write directly causes failure.
+    try {
+      gApi.changes().id(id.get()).topic(name("other-topic"));
+      fail("Expected write to fail");
+    } catch (RestApiException e) {
+      assertChangesReadOnly(e);
+    }
+
+    // Update was not written.
+    assertThat(gApi.changes().id(id.get()).info().topic)
+        .isEqualTo(name("a-topic"));
+    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);
+    assertThat(cause.getCause()).isInstanceOf(OrmException.class);
+    assertThat(cause.getCause())
+        .hasMessage(NoteDbUpdateManager.CHANGES_READ_ONLY);
+  }
+
+  private void setInvalidNoteDbState(Change.Id id) throws Exception {
+    ReviewDb db = getUnwrappedDb();
+    Change c = db.changes().get(id);
+    // In reality we would have NoteDb writes enabled, which would write a real
+    // state into this field. For tests however, we turn NoteDb writes off, so
+    // just use a dummy state to force ChangeNotes to view the notes as
+    // out-of-date.
+    c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    db.changes().update(Collections.singleton(c));
+  }
+
+  private void assertChangeUpToDate(boolean expected, Change.Id id)
+      throws Exception {
+    try (Repository repo = repoManager.openRepository(project)) {
+      Change c = getUnwrappedDb().changes().get(id);
+      assertThat(c).isNotNull();
+      assertThat(c.getNoteDbState()).isNotNull();
+      assertThat(NoteDbChangeState.parse(c).isChangeUpToDate(
+              new RepoRefCache(repo)))
+          .isEqualTo(expected);
+    }
+  }
+
+  private void assertDraftsUpToDate(boolean expected, Change.Id changeId,
+      TestAccount account) throws Exception {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      Change c = getUnwrappedDb().changes().get(changeId);
+      assertThat(c).isNotNull();
+      assertThat(c.getNoteDbState()).isNotNull();
+      NoteDbChangeState state = NoteDbChangeState.parse(c);
+      assertThat(state.areDraftsUpToDate(
+              new RepoRefCache(repo), account.getId()))
+          .isEqualTo(expected);
+    }
+  }
+
+  private ObjectId getMetaRef(Project.NameKey p, String name) throws Exception {
+    try (Repository repo = repoManager.openRepository(p)) {
+      Ref ref = repo.exactRef(name);
+      return ref != null ? ref.getObjectId() : null;
+    }
+  }
+
+  private void putDraft(TestAccount account, Change.Id id, int line, String msg)
+      throws Exception {
+    DraftInput in = new DraftInput();
+    in.line = line;
+    in.message = msg;
+    in.path = PushOneCommit.FILE_NAME;
+    AcceptanceTestRequestScope.Context old = setApiUser(account);
+    try {
+      gApi.changes().id(id.get()).current().createDraft(in);
+    } finally {
+      atrScope.set(old);
+    }
+  }
+
+  private void putComment(TestAccount account, Change.Id id, int line, String msg)
+      throws Exception {
+    CommentInput in = new CommentInput();
+    in.line = line;
+    in.message = msg;
+    ReviewInput rin = new ReviewInput();
+    rin.comments = new HashMap<>();
+    rin.comments.put(PushOneCommit.FILE_NAME, ImmutableList.of(in));
+    rin.drafts = ReviewInput.DraftHandling.KEEP;
+    AcceptanceTestRequestScope.Context old = setApiUser(account);
+    try {
+      gApi.changes().id(id.get()).current().review(rin);
+    } finally {
+      atrScope.set(old);
+    }
+  }
+
+  private void publishDrafts(TestAccount account, Change.Id id)
+      throws Exception {
+    ReviewInput rin = new ReviewInput();
+    rin.drafts = ReviewInput.DraftHandling.PUBLISH_ALL_REVISIONS;
+    AcceptanceTestRequestScope.Context old = setApiUser(account);
+    try {
+      gApi.changes().id(id.get()).current().review(rin);
+    } finally {
+      atrScope.set(old);
+    }
+  }
+
+  private ChangeMessage insertMessage(Change.Id id, PatchSet.Id psId,
+      Account.Id author, Timestamp ts, String message) throws Exception {
+    ChangeMessage msg = new ChangeMessage(
+        new ChangeMessage.Key(id, ChangeUtil.messageUUID(db)),
+        author, ts, psId);
+    msg.setMessage(message);
+    db.changeMessages().insert(Collections.singleton(msg));
+
+    Change c = db.changes().get(id);
+    if (ts.compareTo(c.getLastUpdatedOn()) > 0) {
+      c.setLastUpdatedOn(ts);
+      db.changes().update(Collections.singleton(c));
+    }
+
+    return msg;
+  }
+
+  private ReviewDb getUnwrappedDb() {
+    ReviewDb db = dbProvider.get();
+    return ReviewDbUtil.unwrapDb(db);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK
index ad7d597..013115d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK
@@ -1,15 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
-FLAKY_TEST_CASES=['ProjectWatchIT.java']
-
 acceptance_tests(
-  group = 'server-project',
-  srcs = glob(['*IT.java'], excludes=FLAKY_TEST_CASES),
+  group = 'server_project',
+  srcs = glob(['*IT.java']),
   labels = ['server'],
 )
-
-acceptance_tests(
-  group = 'server-project-flaky',
-  srcs = FLAKY_TEST_CASES,
-  labels = ['server', 'flaky'],
-)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUILD
new file mode 100644
index 0000000..bcf9c9f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUILD
@@ -0,0 +1,16 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+FLAKY_TEST_CASES=['ProjectWatchIT.java']
+
+acceptance_tests(
+  group = 'server_project',
+  srcs = glob(['*IT.java'], exclude=FLAKY_TEST_CASES),
+  labels = ['server'],
+)
+
+acceptance_tests(
+  group = 'server_project_flaky',
+  flaky = 1,
+  srcs = FLAKY_TEST_CASES,
+  labels = ['server', 'flaky'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index 357f268..6f4cc45 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -28,17 +28,25 @@
 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.events.CommentAddedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.Util;
+import com.google.inject.Inject;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 public class CustomLabelIT extends AbstractDaemonTest {
 
+  @Inject
+  private DynamicSet<CommentAddedListener> source;
+
   private final LabelType label = category("CustomLabel",
       value(1, "Positive"),
       value(0, "No score"),
@@ -48,6 +56,9 @@
       value(1, "Positive"),
       value(0, "No score"));
 
+  private RegistrationHandle eventListenerRegistration;
+  private CommentAddedListener.Event lastCommentAddedEvent;
+
   @Before
   public void setUp() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
@@ -58,6 +69,19 @@
     Util.allow(cfg, Permission.forLabel(P.getName()), 0, 1, anonymousUsers,
         "refs/heads/*");
     saveProjectConfig(project, cfg);
+
+    eventListenerRegistration = source.add(new CommentAddedListener() {
+      @Override
+      public void onCommentAdded(Event event) {
+        lastCommentAddedEvent = event;
+      }
+    });
+  }
+
+  @After
+  public void cleanup() {
+    eventListenerRegistration.remove();
+    db.close();
   }
 
   @Test
@@ -124,13 +148,18 @@
         .id(r.getChangeId())
         .addReviewer(in);
 
-    revision(r).review(new ReviewInput().label(P.getName(), 0));
+    ReviewInput input = new ReviewInput().label(P.getName(), 0);
+    input.message = "foo";
+
+    revision(r).review(input);
     ChangeInfo c = get(r.getChangeId());
     LabelInfo q = c.labels.get(P.getName());
     assertThat(q.all).hasSize(2);
     assertThat(q.disliked).isNull();
     assertThat(q.rejected).isNull();
     assertThat(q.blocking).isNull();
+    assertThat(lastCommentAddedEvent.getComment()).isEqualTo(
+        "Patch Set 1:\n\n" + input.message);
   }
 
   @Test
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 564ca23..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
+++ /dev/null
@@ -1,350 +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.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.server.git.MetaDataUpdate;
-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 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 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 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);
-  }
-
-  private void saveProjectConfig(ProjectConfig cfg) throws Exception {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    try {
-      cfg.commit(md);
-    } finally {
-      md.close();
-    }
-  }
-
-  private void merge(PushOneCommit.Result r) throws Exception {
-    revision(r).review(ReviewInput.approve());
-    revision(r).submit();
-    try (Repository repo = repoManager.openRepository(project)) {
-      assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(
-          r.getCommitId());
-    }
-  }
-
-  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 0b86ad9..0b56f43 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
@@ -16,85 +16,41 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+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.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.WatchConfig;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 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;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
-import com.google.inject.Inject;
 
+import com.google.inject.Inject;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.junit.Test;
 
-import java.util.EnumSet;
-import java.util.List;
+import java.util.ArrayList;
 
 @NoHttpd
 public class ProjectWatchIT extends AbstractDaemonTest {
-  @Inject
-  private FakeEmailSender sender;
+  @Inject private WatchConfig.Accessor watchConfig;
 
-  /**
-   * Tests message project watches on new patch sets
-   * <p>
-   * As of 2015-06-21 this test is marked flaky for triggering race
-   * conditions between indexing and project watches filters as
-   * of 2015-06-21.
-   * <p>
-   * The test $SOMETIMES fails, stating that 2 emails instead of only
-   * 1 got sent. The root issue is the inserting of two patch sets
-   * (one shortly after the other), where the first patch set would
-   * not match a user's filter while the second one would.
-   * <p>
-   * The test basically:
-   * <ol>
-   *   <li>Sets up a watch on the text 'sekret' in the commit message.</li>
-   *   <li>Pushes a change without sekret in the commit message (no
-   *     email is expected). (We'll refer to this as PS1)</li>
-   *   <li>Push another patch set to the same change with sekret in the
-   *     commit message (1 email is expected). (We'll refer to this as PS2)</li>
-   *   <li>[...]</li>
-   * </ol>
-   * <p>The expected flow of actions for step 2+3 is:
-   * <pre>
-   *    (i) Write PS1 to the index
-   *   (ii) Send out emails for PS1 after checking project watches from
-   *        fresh ChangeData
-   *  (iii) Write PS2 to the index
-   *   (iv) Send out emails for PS2 after checking project watches from
-   *        fresh ChangeData
-   * </pre>
-   * <p>
-   * But as step (ii) and step (iv) happen on separate threads, steps
-   * (ii) and (iii) might get turned around and become:
-   * <pre>
-   *   * Write PS1 to the index
-   *   * Write PS2 to the index
-   *   * Send out emails for PS1 after checking project watches from
-   *     fresh ChangeData
-   *   * Send out emails for PS2 after checking project watches from
-   *     fresh ChangeData
-   * </pre>
-   * <p>
-   * Hence, the filters for project watches for the emails for PS1 query
-   * the index after PS2 has already been written there. Hence, the
-   * filters for PS1 use the commit message of PS2 when filtering on
-   * 'message:sekret'.
-   * <p>
-   * Since in the ProjectWatchIT test, PS2 contains 'sekret', the filters
-   * for sending out emails for PS1 see a commit message containing
-   * 'sekret', and the watches match for both PS1 and PS2, although they
-   * should only match for PS2.
-   * <p>
-   * This explains why the test is only failing sometimes, and also why it
-   * is more likely to occur when the system is under load.
-   * <p>
-   * A demo exposing the race condition is available at
-   * <a href="https://gerrit-review.googlesource.com/#/c/68719/1">https://gerrit-review.googlesource.com/#/c/68719/1</a>.
-   */
   @Test
   public void newPatchSetsNotifyConfig() throws Exception {
     Address addr = new Address("Watcher", "watcher@example.com");
@@ -132,5 +88,131 @@
     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 {
+    String watchedProject = createProject("watchedProject").get();
+    String otherWatchedProject = createProject("otherWatchedProject").get();
+    setApiUser(user);
+
+    // watch file in project as user
+    watch(watchedProject, "file:a.txt");
+
+    // watch other project as user
+    watch(otherWatchedProject, null);
+
+    // push a change to watched file -> should trigger email notification for
+    // user
+    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();
+
+    // assert email notification for user
+    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");
+    sender.clear();
+
+    // watch project as user2
+    TestAccount user2 = accounts.create("user2", "user2@test.com", "User2");
+    setApiUser(user2);
+    watch(watchedProject, null);
+
+    // push a change to non-watched file -> should not trigger email
+    // notification for user, only for user2
+    r = pushFactory.create(db, admin.getIdent(), watchedRepo,
+        "TRIGGER_USER2", "b.txt", "b1").to("refs/for/master");
+    r.assertOkStatus();
+
+    // assert email notification
+    messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(user2.emailAddress);
+    assertThat(m.body()).contains("Change subject: TRIGGER_USER2\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);
+  }
+
+  @Test
+  public void deleteAllProjectWatches() throws Exception {
+    Map<ProjectWatchKey, Set<NotifyType>> watches = new HashMap<>();
+    watches.put(ProjectWatchKey.create(project, "*"), ImmutableSet.of(NotifyType.ALL));
+    watchConfig.upsertProjectWatches(admin.getId(), watches);
+    assertThat(watchConfig.getProjectWatches(admin.getId())).isNotEmpty();
+
+    watchConfig.deleteAllProjectWatches(admin.getId());
+    assertThat(watchConfig.getProjectWatches(admin.getId())).isEmpty();
+  }
+
+  @Test
+  public void deleteAllProjectWatchesIfWatchConfigIsTheOnlyFileInUserBranch() throws Exception {
+    // Create account that has no files in its refs/users/ branch.
+    Account.Id id = new Account.Id(db.nextAccountId());
+    Account a = new Account(id, TimeUtil.nowTs());
+    db.accounts().insert(Collections.singleton(a));
+
+    // Add a project watch so that a watch.config file in the refs/users/ branch is created.
+    Map<ProjectWatchKey, Set<NotifyType>> watches = new HashMap<>();
+    watches.put(ProjectWatchKey.create(project, "*"), ImmutableSet.of(NotifyType.ALL));
+    watchConfig.upsertProjectWatches(id, watches);
+    assertThat(watchConfig.getProjectWatches(id)).isNotEmpty();
+
+    // Delete all project watches so that the watch.config file in the refs/users/ branch is
+    // deleted.
+    watchConfig.deleteAllProjectWatches(id);
+    assertThat(watchConfig.getProjectWatches(id)).isEmpty();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java
index e07405f..56a56ee 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java
@@ -66,10 +66,10 @@
     if (message != null) {
       command.append(" --message ").append(message);
     }
-    String response = sshSession.exec(command.toString());
+    String response = adminSshSession.exec(command.toString());
     assert_()
-      .withFailureMessage(sshSession.getError())
-      .that(sshSession.hasError())
+      .withFailureMessage(adminSshSession.getError())
+      .that(adminSshSession.hasError())
       .isFalse();
     assertThat(response.toLowerCase(Locale.US)).doesNotContain("error");
   }
@@ -86,4 +86,4 @@
     }
     assertThat(actual).containsExactlyElementsIn(expected);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUILD
new file mode 100644
index 0000000..3c91aa1
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUILD
@@ -0,0 +1,8 @@
+load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests')
+
+acceptance_tests(
+  group = 'ssh',
+  srcs = glob(['*IT.java']),
+  deps = ['//lib/commons:compress'],
+  labels = ['ssh'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
index 3bef84b..025fcfa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
@@ -17,12 +17,13 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static org.eclipse.jgit.transport.RemoteRefUpdate.Status.REJECTED_OTHER_REASON;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.junit.Test;
 
 import java.util.Locale;
@@ -37,13 +38,15 @@
         .create();
 
     String response =
-        sshSession.exec("gerrit ban-commit " + project.get() + " " + c.name());
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isFalse();
+        adminSshSession.exec("gerrit ban-commit " + project.get() + " " + c.name());
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isFalse();
     assertThat(response.toLowerCase(Locale.US)).doesNotContain("error");
 
-    PushResult pushResult = pushHead(testRepo, "refs/heads/master", false);
-    assertThat(pushResult.getRemoteUpdate("refs/heads/master").getMessage())
-        .startsWith("contains banned commit");
+    RemoteRefUpdate u = pushHead(testRepo, "refs/heads/master", false)
+        .getRemoteUpdate("refs/heads/master");
+    assertThat(u).isNotNull();
+    assertThat(u.getStatus()).isEqualTo(REJECTED_OTHER_REASON);
+    assertThat(u.getMessage()).startsWith("contains banned commit");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
index a779136..85d460e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
@@ -28,12 +28,12 @@
   @Test
   public void withValidGroupName() throws Exception {
     String newGroupName = "newGroup";
-    adminSession.put("/groups/" + newGroupName);
+    adminRestSession.put("/groups/" + newGroupName);
     String newProjectName = "newProject";
-    sshSession.exec("gerrit create-project --branch master --owner "
+    adminSshSession.exec("gerrit create-project --branch master --owner "
         + newGroupName + " " + newProjectName);
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isFalse();
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isFalse();
     ProjectState projectState =
         projectCache.get(new Project.NameKey(newProjectName));
     assertThat(projectState).isNotNull();
@@ -42,13 +42,13 @@
   @Test
   public void withInvalidGroupName() throws Exception {
     String newGroupName = "newGroup";
-    adminSession.put("/groups/" + newGroupName);
+    adminRestSession.put("/groups/" + newGroupName);
     String wrongGroupName = "newG";
     String newProjectName = "newProject";
-    sshSession.exec("gerrit create-project --branch master --owner "
+    adminSshSession.exec("gerrit create-project --branch master --owner "
         + wrongGroupName + " " + newProjectName);
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isTrue();
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isTrue();
     ProjectState projectState =
         projectCache.get(new Project.NameKey(newProjectName));
     assertThat(projectState).isNull();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
index b2c4df8..7176254 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GcAssert;
 import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.reviewdb.client.Project;
@@ -59,10 +58,10 @@
   @UseLocalDisk
   public void testGc() throws Exception {
     String response =
-        sshSession.exec("gerrit gc \"" + project.get() + "\" \""
+        adminSshSession.exec("gerrit gc \"" + project.get() + "\" \""
             + project2.get() + "\"");
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isFalse();
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isFalse();
     assertNoError(response);
     gcAssert.assertHasPackFile(project, project2);
     gcAssert.assertHasNoPackFile(allProjects, project3);
@@ -71,20 +70,21 @@
   @Test
   @UseLocalDisk
   public void testGcAll() throws Exception {
-    String response = sshSession.exec("gerrit gc --all");
-    assert_().withFailureMessage(sshSession.getError())
-        .that(sshSession.hasError()).isFalse();
+    String response = adminSshSession.exec("gerrit gc --all");
+    assert_().withFailureMessage(adminSshSession.getError())
+        .that(adminSshSession.hasError()).isFalse();
     assertNoError(response);
     gcAssert.assertHasPackFile(allProjects, project, project2, project3);
   }
 
   @Test
   public void testGcWithoutCapability_Error() throws Exception {
-    SshSession s = new SshSession(server, user);
-    s.exec("gerrit gc --all");
+    userSshSession.exec("gerrit gc --all");
+    assertThat(userSshSession.hasError()).isTrue();
+    String error = userSshSession.getError();
+    assertThat(error).isNotNull();
     assertError("One of the following capabilities is required to access this"
-        + " resource: [runGC, maintainServer]", s.getError());
-    s.close();
+        + " resource: [runGC, maintainServer]", error);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java
deleted file mode 100644
index 7fb99b6..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java
+++ /dev/null
@@ -1,67 +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.ssh;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.GitUtil;
-import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.reviewdb.client.Project;
-
-import org.junit.Ignore;
-import org.junit.Test;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-
-@NoHttpd
-// To see this test failing with 'verify: false', at least in the Jcsh 0.1.51
-// remove bouncycastle libs from the classpath, and run:
-// buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh:JschVerifyFalseBugIT
-public class JschVerifyFalseBugIT extends AbstractDaemonTest {
-
-  @Test
-  @Ignore// we know it works now, so let's not clone a project 500 times ;-)
-  public void test() throws Exception {
-    test(5);
-  }
-
-  private void test(int threads) throws InterruptedException,
-      ExecutionException {
-    Callable<Void> task = new Callable<Void>() {
-      @Override
-      public Void call() throws Exception {
-        for (int i = 1; i < 100; i++) {
-          String p = "p" + i;
-          createProject(p);
-          GitUtil.cloneProject(new Project.NameKey(p), sshSession);
-        }
-        return null;
-      }
-    };
-    List<Callable<Void>> nCopies = Collections.nCopies(threads, task);
-    List<Future<Void>> futures = Executors.newFixedThreadPool(threads)
-        .invokeAll(nCopies);
-    for (Future<Void> future : futures) {
-      future.get();
-    }
-    assertThat(futures).hasSize(threads);
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
index 0e381c1..2865ff87 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
@@ -152,9 +152,9 @@
   public void shouldFailWithFilesWithoutPatchSetsOrCurrentPatchSetsOption()
       throws Exception {
     String changeId = createChange().getChangeId();
-    sshSession.exec("gerrit query --files " + changeId);
-    assertThat(sshSession.hasError()).isTrue();
-    assertThat(sshSession.getError()).contains(
+    adminSshSession.exec("gerrit query --files " + changeId);
+    assertThat(adminSshSession.hasError()).isTrue();
+    assertThat(adminSshSession.getError()).contains(
         "needs --patch-sets or --current-patch-set");
   }
 
@@ -334,7 +334,7 @@
 
   private List<ChangeAttribute> executeSuccessfulQuery(String params)
       throws Exception {
-    return executeSuccessfulQuery(params, sshSession);
+    return executeSuccessfulQuery(params, adminSshSession);
   }
 
   private static List<ChangeAttribute> getChanges(String rawResponse) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
index 88821ce..32a0175 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.ssh;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.Iterables;
@@ -22,17 +23,18 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.testutil.NoteDbMode;
 
 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
 import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
 import org.eclipse.jgit.transport.PacketLineIn;
 import org.eclipse.jgit.transport.PacketLineOut;
 import org.eclipse.jgit.util.IO;
+import org.junit.Before;
 import org.junit.Test;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.io.InputStream;
 import java.util.Set;
 import java.util.TreeSet;
@@ -40,6 +42,13 @@
 @NoHttpd
 public class UploadArchiveIT extends AbstractDaemonTest {
 
+  @Before
+  public void setUp() {
+    // There is some Guice request scoping problem preventing this test from
+    // passing in CHECK mode.
+    assume().that(NoteDbMode.get()).isNotEqualTo(NoteDbMode.CHECK);
+  }
+
   @Test
   @GerritConfig(name = "download.archive", value = "off")
   public void archiveFeatureOff() throws Exception {
@@ -55,11 +64,11 @@
   @Test
   public void zipFormat() throws Exception {
     PushOneCommit.Result r = createChange();
-    String abbreviated = r.getCommitId().abbreviate(8).name();
+    String abbreviated = r.getCommit().abbreviate(8).name();
     String c = command(r, abbreviated);
 
     InputStream out =
-        sshSession.exec2("git-upload-archive " + project.get(),
+        adminSshSession.exec2("git-upload-archive " + project.get(),
             argumentsToInputStream(c));
 
     // Wrap with PacketLineIn to read ACK bytes from output stream
@@ -98,11 +107,11 @@
 
   private void archiveNotPermitted() throws Exception {
     PushOneCommit.Result r = createChange();
-    String abbreviated = r.getCommitId().abbreviate(8).name();
+    String abbreviated = r.getCommit().abbreviate(8).name();
     String c = command(r, abbreviated);
 
     InputStream out =
-        sshSession.exec2("git-upload-archive " + project.get(),
+        adminSshSession.exec2("git-upload-archive " + project.get(),
             argumentsToInputStream(c));
 
     // Wrap with PacketLineIn to read ACK bytes from output stream
@@ -115,7 +124,7 @@
     assertThat(tmp).isEqualTo("fatal: upload-archive not permitted");
   }
 
-  private InputStream argumentsToInputStream(String c) throws IOException {
+  private InputStream argumentsToInputStream(String c) throws Exception {
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     PacketLineOut pctOut = new PacketLineOut(out);
     for (String arg : Splitter.on(' ').split(c)) {
diff --git a/gerrit-acceptance-tests/tests.bzl b/gerrit-acceptance-tests/tests.bzl
new file mode 100644
index 0000000..ff2562d
--- /dev/null
+++ b/gerrit-acceptance-tests/tests.bzl
@@ -0,0 +1,28 @@
+load('//tools/bzl:junit.bzl', 'junit_tests')
+
+BOUNCYCASTLE = [
+  '//lib/bouncycastle:bcpkix-without-neverlink',
+  '//lib/bouncycastle:bcpg-without-neverlink',
+]
+
+def acceptance_tests(
+    group,
+    srcs,
+    flaky = 0,
+    deps = [],
+    labels = [],
+    source_under_test = [], #unused
+    vm_args = ['-Xmx256m']):
+  junit_tests(
+    name = group,
+    srcs = srcs,
+    flaky = flaky,
+    deps = deps + BOUNCYCASTLE + [
+      '//gerrit-acceptance-tests:lib',
+    ],
+    tags = labels + [
+      'acceptance',
+      'slow',
+    ],
+    jvm_flags = vm_args,
+  )
diff --git a/gerrit-acceptance-tests/tests.defs b/gerrit-acceptance-tests/tests.defs
index 940b1cc..85cc78b 100644
--- a/gerrit-acceptance-tests/tests.defs
+++ b/gerrit-acceptance-tests/tests.defs
@@ -1,4 +1,3 @@
-# These are needed as workaround for the 'verify: false' bug in Jcraft SSH library
 BOUNCYCASTLE = [
   '//lib/bouncycastle:bcpkix',
   '//lib/bouncycastle:bcpg',
@@ -11,16 +10,16 @@
     labels = [],
     source_under_test = [],
     vm_args = ['-Xmx256m']):
-  from os import environ, path
-  if not environ.get('NO_BOUNCYCASTLE'):
-    deps = BOUNCYCASTLE + deps
+  from os import path
   if path.exists('/dev/urandom'):
     vm_args = vm_args + ['-Djava.security.egd=file:/dev/./urandom']
 
   java_test(
     name = group,
     srcs = srcs,
-    deps = ['//gerrit-acceptance-tests:lib'] + deps,
+    deps = deps + BOUNCYCASTLE + [
+      '//gerrit-acceptance-tests:lib'
+    ],
     source_under_test = [
       '//gerrit-httpd:httpd',
       '//gerrit-sshd:sshd',
diff --git a/gerrit-antlr/BUILD b/gerrit-antlr/BUILD
new file mode 100644
index 0000000..6c39106
--- /dev/null
+++ b/gerrit-antlr/BUILD
@@ -0,0 +1,32 @@
+load("//tools/bzl:genrule2.bzl", "genrule2")
+
+java_library(
+    name = "query_exception",
+    srcs = ["src/main/java/com/google/gerrit/server/query/QueryParseException.java"],
+    visibility = ["//visibility:public"],
+)
+
+genrule2(
+    name = "query_antlr",
+    srcs = ["src/main/antlr3/com/google/gerrit/server/query/Query.g"],
+    out = "query_antlr.srcjar",
+    cmd = " && ".join([
+        "$(location //lib/antlr:antlr-tool) -o $$TMP $<",
+        "cd $$TMP",
+        "zip -q $$ROOT/$@ $$(find . -type f )",
+    ]),
+    tools = [
+        "//lib/antlr:antlr-tool",
+        "@bazel_tools//tools/zip:zipper",
+    ],
+)
+
+java_library(
+    name = "query_parser",
+    srcs = [":query_antlr"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":query_exception",
+        "//lib/antlr:java_runtime",
+    ],
+)
diff --git a/gerrit-cache-h2/BUCK b/gerrit-cache-h2/BUCK
index 37c8b96..0bc1cb12 100644
--- a/gerrit-cache-h2/BUCK
+++ b/gerrit-cache-h2/BUCK
@@ -8,7 +8,7 @@
     '//lib:guava',
     '//lib:h2',
     '//lib/guice:guice',
-    '//lib/jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib/log:api',
   ],
   visibility = ['PUBLIC'],
diff --git a/gerrit-cache-h2/BUILD b/gerrit-cache-h2/BUILD
new file mode 100644
index 0000000..a70393d
--- /dev/null
+++ b/gerrit-cache-h2/BUILD
@@ -0,0 +1,30 @@
+load('//tools/bzl:junit.bzl', 'junit_tests')
+
+java_library(
+  name = 'cache-h2',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = [
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-server:server',
+    '//lib:guava',
+    '//lib:h2',
+    '//lib/guice:guice',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/log:api',
+  ],
+  visibility = ['//visibility:public'],
+)
+
+junit_tests(
+  name = 'tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = [
+    ':cache-h2',
+    '//gerrit-server:server',
+    '//lib:guava',
+    '//lib:h2',
+    '//lib/guice:guice',
+    '//lib:junit',
+  ],
+)
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 fcacf78..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
@@ -17,7 +17,6 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
-import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -40,6 +39,7 @@
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
@@ -49,7 +49,8 @@
 
 @Singleton
 class H2CacheFactory implements PersistentCacheFactory, LifecycleListener {
-  static final Logger log = LoggerFactory.getLogger(H2CacheFactory.class);
+  private static final Logger log =
+      LoggerFactory.getLogger(H2CacheFactory.class);
 
   private final DefaultCacheFactory defaultFactory;
   private final Config config;
@@ -58,6 +59,8 @@
   private final DynamicMap<Cache<?, ?>> cacheMap;
   private final ExecutorService executor;
   private final ScheduledExecutorService cleanup;
+  private final long h2CacheSize;
+  private final boolean h2AutoServer;
 
   @Inject
   H2CacheFactory(
@@ -68,7 +71,9 @@
     defaultFactory = defaultCacheFactory;
     config = cfg;
     cacheDir = getCacheDir(site, cfg.getString("cache", null, "directory"));
-    caches = Lists.newLinkedList();
+    h2CacheSize = cfg.getLong("cache", null, "h2CacheSize", -1);
+    h2AutoServer = cfg.getBoolean("cache", null, "h2AutoServer", false);
+    caches = new LinkedList<>();
     this.cacheMap = cacheMap;
 
     if (cacheDir != null) {
@@ -220,8 +225,17 @@
       TypeLiteral<K> keyType,
       long maxSize,
       Long expireAfterWrite) {
-    String url = "jdbc:h2:" + cacheDir.resolve(name).toUri();
-    return new SqlStore<>(url, keyType, maxSize,
+    StringBuilder url = new StringBuilder();
+    url.append("jdbc:h2:").append(cacheDir.resolve(name).toUri());
+    if (h2CacheSize >= 0) {
+      url.append(";CACHE_SIZE=");
+      // 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-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 289c8cc..838f42c 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.server.cache.h2;
 
+import com.google.common.base.Throwables;
 import com.google.common.cache.AbstractLoadingCache;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.CacheStats;
 import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.hash.BloomFilter;
 import com.google.common.hash.Funnel;
 import com.google.common.hash.Funnels;
@@ -81,6 +83,9 @@
     PersistentCache {
   private static final Logger log = LoggerFactory.getLogger(H2CacheImpl.class);
 
+  private static final ImmutableSet<String> OLD_CLASS_NAMES = ImmutableSet.of(
+      "com.google.gerrit.server.change.ChangeKind");
+
   private final Executor executor;
   private final SqlStore<K, V> store;
   private final TypeLiteral<K> keyType;
@@ -472,7 +477,9 @@
           c.get.clearParameters();
         }
       } catch (SQLException e) {
-        log.warn("Cannot read cache " + url + " for " + key, e);
+        if (!isOldClassNameError(e)) {
+          log.warn("Cannot read cache " + url + " for " + key, e);
+        }
         c = close(c);
         return null;
       } finally {
@@ -480,6 +487,16 @@
       }
     }
 
+    private static boolean isOldClassNameError(Throwable t) {
+      for (Throwable c : Throwables.getCausalChain(t)) {
+        if (c instanceof ClassNotFoundException
+            && OLD_CLASS_NAMES.contains(c.getMessage())) {
+          return true;
+        }
+      }
+      return false;
+    }
+
     private boolean expired(Timestamp created) {
       if (expireAfterWrite == 0) {
         return false;
@@ -490,7 +507,7 @@
 
     private void touch(SqlHandle c, K key) throws SQLException {
       if (c.touch == null) {
-        c.touch =c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=?");
+        c.touch = c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=?");
       }
       try {
         c.touch.setTimestamp(1, TimeUtil.nowTs());
diff --git a/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java b/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 3b7e436..c999d71 100644
--- a/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -79,4 +79,4 @@
     }));
     assertFalse("did not invoke Callable", called.get());
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-common/BUCK b/gerrit-common/BUCK
index a36a9b7..847fd25 100644
--- a/gerrit-common/BUCK
+++ b/gerrit-common/BUCK
@@ -1,17 +1,11 @@
 SRC = 'src/main/java/com/google/gerrit/'
 
 ANNOTATIONS = [
-  SRC + 'common/Nullable.java',
-  SRC + 'common/audit/Audit.java',
-  SRC + 'common/auth/SignInRequired.java',
-]
-
-EXCLUDES = [
-  SRC + 'common/SiteLibraryLoaderUtil.java',
-  SRC + 'common/PluginData.java',
-  SRC + 'common/FileUtil.java',
-  SRC + 'common/IoUtil.java',
-  SRC + 'common/TimeUtil.java',
+  SRC + x for x in [
+    'common/Nullable.java',
+    'common/audit/Audit.java',
+    'common/auth/SignInRequired.java',
+  ]
 ]
 
 java_library(
@@ -22,13 +16,18 @@
 
 gwt_module(
   name = 'client',
-  srcs = glob([SRC + 'common/**/*.java'], excludes = EXCLUDES),
+  srcs = glob([SRC + 'common/**/*.java']),
   gwt_xml = SRC + 'Common.gwt.xml',
   exported_deps = [
-    '//gerrit-extension-api:client',
+    '//gerrit-extension-api:api',
     '//gerrit-prettify:client',
+    '//lib:guava',
     '//lib:gwtorm_client',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/joda:joda-time',
+    '//lib/log:api',
   ],
+  provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
 )
 
@@ -41,13 +40,14 @@
     '//gerrit-patch-jgit:server',
     '//gerrit-prettify:server',
     '//gerrit-reviewdb:server',
+    '//lib:guava',
     '//lib:gwtjsonrpc',
     '//lib:gwtorm',
-    '//lib:guava',
-    '//lib/jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib/joda:joda-time',
     '//lib/log:api',
   ],
+  provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
 )
 
diff --git a/gerrit-common/BUILD b/gerrit-common/BUILD
new file mode 100644
index 0000000..86ba087
--- /dev/null
+++ b/gerrit-common/BUILD
@@ -0,0 +1,77 @@
+load('//tools/bzl:gwt.bzl', 'gwt_module')
+load('//tools/bzl:junit.bzl', 'junit_tests')
+
+SRC = 'src/main/java/com/google/gerrit/'
+
+ANNOTATIONS = [
+  SRC + x for x in [
+    'common/Nullable.java',
+    'common/audit/Audit.java',
+    'common/auth/SignInRequired.java',
+  ]
+]
+
+java_library(
+  name = 'annotations',
+  srcs = ANNOTATIONS,
+  visibility = ['//visibility:public'],
+)
+
+gwt_module(
+  name = 'client',
+  srcs = glob([SRC + 'common/**/*.java']),
+  gwt_xml = SRC + 'Common.gwt.xml',
+  exported_deps = [
+    '//gerrit-extension-api:api',
+    '//gerrit-prettify:client',
+    '//lib:guava',
+    '//lib:gwtorm_client',
+    '//lib:servlet-api-3_1',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/joda:joda-time',
+    '//lib/log:api',
+  ],
+  visibility = ['//visibility:public'],
+)
+
+java_library(
+  name = 'server',
+  srcs = glob([SRC + 'common/**/*.java'], exclude = ANNOTATIONS),
+  deps = [
+    ':annotations',
+    '//gerrit-extension-api:api',
+    '//gerrit-patch-jgit:server',
+    '//gerrit-prettify:server',
+    '//gerrit-reviewdb:server',
+    '//lib:guava',
+    '//lib:gwtjsonrpc',
+    '//lib:gwtorm',
+    '//lib:servlet-api-3_1',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/joda:joda-time',
+    '//lib/log:api',
+  ],
+  visibility = ['//visibility:public'],
+)
+
+TEST = 'src/test/java/com/google/gerrit/common/'
+AUTO_VALUE_TEST_SRCS = [TEST + 'AutoValueTest.java']
+
+junit_tests(
+  name = 'client_tests',
+  srcs = glob(['src/test/java/**/*.java'], exclude = AUTO_VALUE_TEST_SRCS),
+  deps = [
+    ':client',
+    '//lib:guava',
+    '//lib:junit',
+  ],
+)
+
+junit_tests(
+  name = 'auto_value_tests',
+  srcs = AUTO_VALUE_TEST_SRCS,
+  deps = [
+    '//lib:truth',
+    '//lib/auto:auto-value',
+  ],
+)
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java
index ed28ec0..83dc4d8 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.common;
 
+import com.google.common.annotations.GwtIncompatible;
+
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.IO;
@@ -25,6 +27,7 @@
 import java.nio.file.Path;
 import java.util.Arrays;
 
+@GwtIncompatible("Unemulated classes in java.io, java.nio and JGit")
 public class FileUtil {
   public static boolean modified(FileBasedConfig cfg) throws IOException {
     byte[] curVers;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
index 9bc2ea5..3422a78 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common;
 
+import com.google.common.annotations.GwtIncompatible;
 import com.google.common.collect.Sets;
 
 import java.io.IOException;
@@ -29,6 +30,7 @@
 import java.util.Collections;
 import java.util.Set;
 
+@GwtIncompatible("Unemulated methods in Class and OutputStream")
 public final class IoUtil {
   public static void copyWithThread(final InputStream src,
       final OutputStream dst) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index 28e0d24..43d4441 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common;
 
-import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
@@ -30,6 +29,7 @@
   public static final String SETTINGS_SSHKEYS = "/settings/ssh-keys";
   public static final String SETTINGS_GPGKEYS = "/settings/gpg-keys";
   public static final String SETTINGS_HTTP_PASSWORD = "/settings/http-password";
+  public static final String SETTINGS_OAUTH_TOKEN = "/settings/oauth-token";
   public static final String SETTINGS_WEBIDENT = "/settings/web-identities";
   public static final String SETTINGS_MYGROUPS = "/settings/group-memberships";
   public static final String SETTINGS_AGREEMENTS = "/settings/agreements";
@@ -49,10 +49,7 @@
   public static final String ADMIN_CREATE_PROJECT = "/admin/create-project/";
   public static final String ADMIN_PLUGINS = "/admin/plugins/";
   public static final String MY_GROUPS = "/groups/self";
-
-  public static String toChange(final ChangeInfo c) {
-    return toChange(c.getId());
-  }
+  public static final String DOCUMENTATION = "/Documentation/";
 
   public static String toChangeInEditMode(Change.Id c) {
     return "/c/" + c + ",edit/";
@@ -111,11 +108,6 @@
     return QUERY + KeyUtil.encode(query);
   }
 
-  public static String toChangeQuery(String query, String start) {
-    int s = Integer.parseInt(start);
-    return QUERY + KeyUtil.encode(query) + (s > 0 ? "," + s : "");
-  }
-
   public static String toProjectDashboard(Project.NameKey name, String id) {
     return PROJECTS + name.get() + DASHBOARDS + id;
   }
@@ -132,6 +124,20 @@
       return status(status) + " " + op("project", proj.get());
   }
 
+  public static String topicQuery(Status status, String topic) {
+    switch (status) {
+      case ABANDONED:
+        return toChangeQuery(status(status) + " " + op("topic", topic));
+      case DRAFT:
+      case MERGED:
+      case NEW:
+        return toChangeQuery(op("topic", topic) + " (" +
+            status(Status.NEW) + " OR " +
+            status(Status.MERGED) + ")");
+    }
+    return toChangeQuery(status(status) + " " + op("topic", topic));
+}
+
   public static String toGroup(AccountGroup.UUID uuid) {
     return ADMIN_GROUPS + "uuid-" + uuid;
   }
@@ -140,12 +146,17 @@
     return SETTINGS_EXTENSION + pluginName + "/" + path;
   }
 
+  public static String toDocumentationQuery(String query) {
+    return DOCUMENTATION + KeyUtil.encode(query);
+  }
+
   private static String status(Status status) {
     switch (status) {
       case ABANDONED:
         return "status:abandoned";
       case MERGED:
         return "status:merged";
+      case DRAFT:
       case NEW:
       default:
         return "status:open";
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java b/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
index 27dc639..4645158 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.common;
 
+import com.google.common.annotations.GwtIncompatible;
+
 import java.nio.file.Path;
 import java.util.Objects;
 
+@GwtIncompatible("Unemulated java.nio.file.Path")
 public class PluginData {
   public final String name;
   public final String version;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/RawInputUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/RawInputUtil.java
new file mode 100644
index 0000000..edcd111
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/RawInputUtil.java
@@ -0,0 +1,78 @@
+// 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.common;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.base.Preconditions;
+import com.google.gerrit.extensions.restapi.RawInput;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.servlet.http.HttpServletRequest;
+
+@GwtIncompatible("Unemulated classes in java.io and javax.servlet")
+public class RawInputUtil {
+  public static RawInput create(String content) {
+    return create(content.getBytes(UTF_8));
+  }
+
+  public static RawInput create(final byte[] bytes, final String contentType) {
+    Preconditions.checkNotNull(bytes);
+    Preconditions.checkArgument(bytes.length > 0);
+    return new RawInput() {
+      @Override
+      public InputStream getInputStream() throws IOException {
+        return new ByteArrayInputStream(bytes);
+      }
+
+      @Override
+      public String getContentType() {
+        return contentType;
+      }
+
+      @Override
+      public long getContentLength() {
+        return bytes.length;
+      }
+    };
+  }
+
+  public static RawInput create(final byte[] bytes) {
+    return create(bytes, "application/octet-stream");
+  }
+
+  public static RawInput create(final HttpServletRequest req) {
+    return new RawInput() {
+      @Override
+      public String getContentType() {
+        return req.getContentType();
+      }
+
+      @Override
+      public long getContentLength() {
+        return req.getContentLength();
+      }
+
+      @Override
+      public InputStream getInputStream() throws IOException {
+        return req.getInputStream();
+      }
+    };
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
index 2511a51..bf87d7b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.common.FileUtil.lastModified;
 
+import com.google.common.annotations.GwtIncompatible;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Ordering;
@@ -30,6 +31,7 @@
 import java.nio.file.Path;
 import java.util.List;
 
+@GwtIncompatible("Unemulated classes in java.nio and Guava")
 public final class SiteLibraryLoaderUtil {
   private static final Logger log =
       LoggerFactory.getLogger(SiteLibraryLoaderUtil.class);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java
index 4274b5a..ec91a81 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.common;
 
+import com.google.common.annotations.GwtIncompatible;
+
 import org.joda.time.DateTimeUtils;
 
 import java.sql.Timestamp;
 
 /** Static utility methods for dealing with dates and times. */
+@GwtIncompatible("Unemulated org.joda.time.DateTimeUtils")
 public class TimeUtil {
   public static long nowMs() {
     return DateTimeUtils.currentTimeMillis();
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
index 79c17f4..713fd4d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.common.auth.openid;
 
 public class OpenIdUrls {
-  public static final String OPENID_IDENTIFIER = "openid_identifier";
   public static final String LASTID_COOKIE = "gerrit.last_openid";
 
   public static final String URL_LAUNCHPAD = "https://login.launchpad.net/+openid";
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
index f21f894..e8d1a3b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
@@ -70,9 +70,13 @@
       Permission p = new Permission(name);
       permissions.add(p);
       return p;
-    } else {
-      return null;
     }
+
+    return null;
+  }
+
+  public void addPermission(Permission p) {
+    getPermissions().add(p);
   }
 
   public void remove(Permission permission) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
index 8614be5..e8f9fd5 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
@@ -76,51 +76,4 @@
   public String getUsername() {
     return username;
   }
-
-  /**
-   * Formats an account name.
-   * <p>
-   * If the account has a full name, it returns only the full name. Otherwise it
-   * returns a longer form that includes the email address.
-   */
-  public String getName(String anonymousCowardName) {
-    if (getFullName() != null) {
-      return getFullName();
-    }
-    if (getPreferredEmail() != null) {
-      return getPreferredEmail();
-    }
-    return getNameEmail(anonymousCowardName);
-  }
-
-  /**
-   * Formats an account as a name and an email address.
-   * <p>
-   * Example output:
-   * <ul>
-   * <li>{@code A U. Thor &lt;author@example.com&gt;}: full populated</li>
-   * <li>{@code A U. Thor (12)}: missing email address</li>
-   * <li>{@code Anonymous Coward &lt;author@example.com&gt;}: missing name</li>
-   * <li>{@code Anonymous Coward (12)}: missing name and email address</li>
-   * </ul>
-   */
-  public String getNameEmail(String anonymousCowardName) {
-    String name = getFullName();
-    if (name == null) {
-      name = anonymousCowardName;
-    }
-
-    final StringBuilder b = new StringBuilder();
-    b.append(name);
-    if (getPreferredEmail() != null) {
-      b.append(" <");
-      b.append(getPreferredEmail());
-      b.append(">");
-    } else if (getId() != null) {
-      b.append(" (");
-      b.append(getId().get());
-      b.append(")");
-    }
-    return b.toString();
-  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountProjectWatchInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountProjectWatchInfo.java
deleted file mode 100644
index 581a09b..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountProjectWatchInfo.java
+++ /dev/null
@@ -1,39 +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.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Project;
-
-public final class AccountProjectWatchInfo {
-  protected AccountProjectWatch watch;
-  protected Project project;
-
-  protected AccountProjectWatchInfo() {
-  }
-
-  public AccountProjectWatchInfo(final AccountProjectWatch w, final Project p) {
-    watch = w;
-    project = p;
-  }
-
-  public AccountProjectWatch getWatch() {
-    return watch;
-  }
-
-  public Project getProject() {
-    return project;
-  }
-}
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
index 533dfa2..22482c7 100644
--- 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
@@ -14,48 +14,14 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 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;
 
 @RpcImpl(version = Version.V2_0)
 public interface AccountService extends RemoteJsonService {
   @SignInRequired
-  void myAccount(AsyncCallback<Account> callback);
-
-  @Audit
-  @SignInRequired
-  void changeDiffPreferences(DiffPreferencesInfo diffPref,
-      AsyncCallback<VoidResult> callback);
-
-  @SignInRequired
-  void myProjectWatch(AsyncCallback<List<AccountProjectWatchInfo>> callback);
-
-  @Audit
-  @SignInRequired
-  void addProjectWatch(String projectName, String filter,
-      AsyncCallback<AccountProjectWatchInfo> callback);
-
-  @Audit
-  @SignInRequired
-  void updateProjectWatch(AccountProjectWatch watch,
-      AsyncCallback<VoidResult> callback);
-
-  @Audit
-  @SignInRequired
-  void deleteProjectWatches(Set<AccountProjectWatch.Key> keys,
-      AsyncCallback<VoidResult> callback);
-
-  @SignInRequired
   void myAgreements(AsyncCallback<AgreementInfo> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
deleted file mode 100644
index 6d9c2cd..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
+++ /dev/null
@@ -1,33 +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.audit.Audit;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.reviewdb.client.PatchSet;
-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 ChangeDetailService extends RemoteJsonService {
-  @Audit
-  void patchSetDetail(PatchSet.Id key, AsyncCallback<PatchSetDetail> callback);
-
-  @Audit
-  void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id key,
-      DiffPreferencesInfo diffPrefs, AsyncCallback<PatchSetDetail> callback);
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
deleted file mode 100644
index d0f8cd3..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
+++ /dev/null
@@ -1,113 +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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-
-import java.sql.Timestamp;
-
-public class ChangeInfo {
-  protected Change.Id id;
-  protected Change.Key key;
-  protected Account.Id owner;
-  protected String subject;
-  protected Change.Status status;
-  protected ProjectInfo project;
-  protected String branch;
-  protected String topic;
-  protected boolean starred;
-  protected Timestamp lastUpdatedOn;
-  protected PatchSet.Id patchSetId;
-  protected boolean latest;
-
-  public ChangeInfo() {
-  }
-
-  public ChangeInfo(final Change c, final PatchSet.Id patchId) {
-    set(c, patchId);
-  }
-
-  public void set(final Change c, final PatchSet.Id patchId) {
-    id = c.getId();
-    key = c.getKey();
-    owner = c.getOwner();
-    subject = c.getSubject();
-    status = c.getStatus();
-    project = new ProjectInfo(c.getProject());
-    branch = c.getDest().getShortName();
-    topic = c.getTopic();
-    lastUpdatedOn = c.getLastUpdatedOn();
-    patchSetId = patchId;
-    latest = patchSetId == null || patchSetId.equals(c.currentPatchSetId());
-  }
-
-  public ChangeInfo(final Change c) {
-    this(c, null);
-  }
-
-  public Change.Id getId() {
-    return id;
-  }
-
-  public Change.Key getKey() {
-    return key;
-  }
-
-  public Account.Id getOwner() {
-    return owner;
-  }
-
-  public String getSubject() {
-    return subject;
-  }
-
-  public Change.Status getStatus() {
-    return status;
-  }
-
-  public ProjectInfo getProject() {
-    return project;
-  }
-
-  public String getBranch() {
-    return branch;
-  }
-
-  public String getTopic() {
-    return topic;
-  }
-
-  public boolean isStarred() {
-    return starred;
-  }
-
-  public void setStarred(final boolean s) {
-    starred = s;
-  }
-
-  public PatchSet.Id getPatchSetId() {
-    return patchSetId;
-  }
-
-  public boolean isLatest() {
-    return latest;
-  }
-
-  public Timestamp getLastUpdatedOn() {
-    return lastUpdatedOn;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
index a052f4f..0156b7d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
@@ -39,7 +39,7 @@
   }
 
   public static class Error {
-    public static enum Type {
+    public enum Type {
       /** Git garbage collection was already scheduled for this project */
       GC_ALREADY_SCHEDULED,
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebType.java
index 7cdec2f..0ec7701 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebType.java
@@ -26,7 +26,6 @@
   private String rootTree;
 
   private char pathSeparator = '/';
-  private boolean linkDrafts = true;
   private boolean urlEncode = true;
 
   /** @return name displayed in links. */
@@ -141,20 +140,6 @@
     this.pathSeparator = separator;
   }
 
-  /** @return whether to generate links to draft patch sets. */
-  public boolean getLinkDrafts() {
-    return linkDrafts;
-  }
-
-  /**
-   * Set whether to generate links to draft patch sets.
-   *
-   * @param linkDrafts new value.
-   */
-  public void setLinkDrafts(boolean linkDrafts) {
-    this.linkDrafts = linkDrafts;
-  }
-
   /** @return whether to URL encode path segments. */
   public boolean getUrlEncode() {
     return urlEncode;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java
deleted file mode 100644
index b700853..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (C) 2011 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.reviewdb.client.AccountGroup;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-/** In-memory table of {@link GroupInfo}, indexed by {@code AccountGroup.Id}. */
-public class GroupInfoCache {
-  private static final GroupInfoCache EMPTY;
-  static {
-    EMPTY = new GroupInfoCache();
-    EMPTY.groups = Collections.emptyMap();
-  }
-
-  /** Obtain an empty cache singleton. */
-  public static GroupInfoCache empty() {
-    return EMPTY;
-  }
-
-  protected Map<AccountGroup.UUID, GroupInfo> groups;
-
-  protected GroupInfoCache() {
-  }
-
-  public GroupInfoCache(final Iterable<GroupInfo> list) {
-    groups = new HashMap<>();
-    for (final GroupInfo gi : list) {
-      groups.put(gi.getId(), gi);
-    }
-  }
-
-  /**
-   * Lookup the group summary
-   * <p>
-   * The return value can take on one of three forms:
-   * <ul>
-   * <li>{@code null}, if {@code id == null}.</li>
-   * <li>a valid info block, if {@code id} was loaded.</li>
-   * <li>an anonymous info block, if {@code id} was not loaded.</li>
-   * </ul>
-   *
-   * @param uuid the id desired.
-   * @return info block for the group.
-   */
-  public GroupInfo get(final AccountGroup.UUID uuid) {
-    if (uuid == null) {
-      return null;
-    }
-
-    GroupInfo r = groups.get(uuid);
-    if (r == null) {
-      r = new GroupInfo(uuid);
-      groups.put(uuid, r);
-    }
-    return r;
-  }
-
-  /** Merge the information from another cache into this one. */
-  public void merge(final GroupInfoCache other) {
-    assert this != EMPTY;
-    groups.putAll(other.groups);
-  }
-}
\ No newline at end of file
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/IncludedInDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/IncludedInDetail.java
deleted file mode 100644
index 9365db8..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/IncludedInDetail.java
+++ /dev/null
@@ -1,44 +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.data;
-
-import java.util.Collections;
-import java.util.List;
-
-public class IncludedInDetail {
-  private List<String> branches;
-  private List<String> tags;
-
-  public IncludedInDetail() {
-  }
-
-  public void setBranches(final List<String> b) {
-    Collections.sort(b);
-    branches = b;
-  }
-
-  public List<String> getBranches() {
-    return branches;
-  }
-
-  public void setTags(final List<String> t) {
-    Collections.sort(t);
-    tags = t;
-  }
-
-  public List<String> getTags() {
-    return tags;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
index cf6f756..b1e1243 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
@@ -29,6 +29,7 @@
   public static final boolean DEF_COPY_ALL_SCORES_IF_NO_CHANGE = true;
   public static final boolean DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = false;
   public static final boolean DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = false;
+  public static final boolean DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE = false;
   public static final boolean DEF_COPY_MAX_SCORE = false;
   public static final boolean DEF_COPY_MIN_SCORE = false;
 
@@ -99,6 +100,7 @@
   protected String functionName;
   protected boolean copyMinScore;
   protected boolean copyMaxScore;
+  protected boolean copyAllScoresOnMergeFirstParentUpdate;
   protected boolean copyAllScoresOnTrivialRebase;
   protected boolean copyAllScoresIfNoCodeChange;
   protected boolean copyAllScoresIfNoChange;
@@ -138,6 +140,8 @@
     setCopyAllScoresIfNoChange(DEF_COPY_ALL_SCORES_IF_NO_CHANGE);
     setCopyAllScoresIfNoCodeChange(DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE);
     setCopyAllScoresOnTrivialRebase(DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE);
+    setCopyAllScoresOnMergeFirstParentUpdate(
+        DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE);
     setCopyMaxScore(DEF_COPY_MAX_SCORE);
     setCopyMinScore(DEF_COPY_MIN_SCORE);
   }
@@ -217,6 +221,16 @@
     this.copyMaxScore = copyMaxScore;
   }
 
+  public boolean isCopyAllScoresOnMergeFirstParentUpdate() {
+    return copyAllScoresOnMergeFirstParentUpdate;
+  }
+
+  public void setCopyAllScoresOnMergeFirstParentUpdate(
+      boolean copyAllScoresOnMergeFirstParentUpdate) {
+    this.copyAllScoresOnMergeFirstParentUpdate =
+        copyAllScoresOnMergeFirstParentUpdate;
+  }
+
   public boolean isCopyAllScoresOnTrivialRebase() {
     return copyAllScoresOnTrivialRebase;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
deleted file mode 100644
index d9601f0..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
+++ /dev/null
@@ -1,31 +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.audit.Audit;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-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 PatchDetailService extends RemoteJsonService {
-  @Audit
-  void patchScript(Patch.Key key, PatchSet.Id a, PatchSet.Id b,
-      DiffPreferencesInfo diffPrefs, AsyncCallback<PatchScript> callback);
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index f23afb1..5777396 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -27,38 +27,38 @@
 import java.util.List;
 
 public class PatchScript {
-  public static enum DisplayMethod {
+  public enum DisplayMethod {
     NONE, DIFF, IMG
   }
 
-  public static enum FileMode {
+  public enum FileMode {
     FILE, SYMLINK, GITLINK
   }
 
-  protected Change.Key changeId;
-  protected ChangeType changeType;
-  protected String oldName;
-  protected String newName;
-  protected FileMode oldMode;
-  protected FileMode newMode;
-  protected List<String> header;
-  protected DiffPreferencesInfo diffPrefs;
-  protected SparseFileContent a;
-  protected SparseFileContent b;
-  protected List<Edit> edits;
-  protected DisplayMethod displayMethodA;
-  protected DisplayMethod displayMethodB;
-  protected transient String mimeTypeA;
-  protected transient String mimeTypeB;
-  protected CommentDetail comments;
-  protected List<Patch> history;
-  protected boolean hugeFile;
-  protected boolean intralineDifference;
-  protected boolean intralineFailure;
-  protected boolean intralineTimeout;
-  protected boolean binary;
-  protected transient String commitIdA;
-  protected transient String commitIdB;
+  private Change.Key changeId;
+  private ChangeType changeType;
+  private String oldName;
+  private String newName;
+  private FileMode oldMode;
+  private FileMode newMode;
+  private List<String> header;
+  private DiffPreferencesInfo diffPrefs;
+  private SparseFileContent a;
+  private SparseFileContent b;
+  private List<Edit> edits;
+  private DisplayMethod displayMethodA;
+  private DisplayMethod displayMethodB;
+  private transient String mimeTypeA;
+  private transient String mimeTypeB;
+  private CommentDetail comments;
+  private List<Patch> history;
+  private boolean hugeFile;
+  private boolean intralineDifference;
+  private boolean intralineFailure;
+  private boolean intralineTimeout;
+  private boolean binary;
+  private transient String commitIdA;
+  private transient String commitIdB;
 
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
       final String nn, final FileMode om, final FileMode nm,
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
deleted file mode 100644
index 39f5cb0..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
+++ /dev/null
@@ -1,64 +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.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.Project;
-
-import java.util.List;
-
-public class PatchSetDetail {
-  protected PatchSet patchSet;
-  protected PatchSetInfo info;
-  protected List<Patch> patches;
-  protected Project.NameKey project;
-
-  public PatchSetDetail() {
-  }
-
-  public PatchSet getPatchSet() {
-    return patchSet;
-  }
-
-  public void setPatchSet(final PatchSet ps) {
-    patchSet = ps;
-  }
-
-  public PatchSetInfo getInfo() {
-    return info;
-  }
-
-  public void setInfo(final PatchSetInfo i) {
-    info = i;
-  }
-
-  public List<Patch> getPatches() {
-    return patches;
-  }
-
-  public void setPatches(final List<Patch> p) {
-    patches = p;
-  }
-
-  public Project.NameKey getProject() {
-    return project;
-  }
-
-  public void setProject(final Project.NameKey p) {
-    project = p;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index a041dc6..97f11b4 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -22,6 +22,7 @@
 /** A single permission within an {@link AccessSection} of a project. */
 public class Permission implements Comparable<Permission> {
   public static final String ABANDON = "abandon";
+  public static final String ADD_PATCH_SET = "addPatchSet";
   public static final String CREATE = "create";
   public static final String DELETE_DRAFTS = "deleteDrafts";
   public static final String EDIT_HASHTAGS = "editHashtags";
@@ -53,6 +54,7 @@
     NAMES_LC.add(OWNER.toLowerCase());
     NAMES_LC.add(READ.toLowerCase());
     NAMES_LC.add(ABANDON.toLowerCase());
+    NAMES_LC.add(ADD_PATCH_SET.toLowerCase());
     NAMES_LC.add(CREATE.toLowerCase());
     NAMES_LC.add(FORGE_AUTHOR.toLowerCase());
     NAMES_LC.add(FORGE_COMMITTER.toLowerCase());
@@ -201,9 +203,8 @@
       PermissionRule r = new PermissionRule(group);
       rules.add(r);
       return r;
-    } else {
-      return null;
     }
+    return null;
   }
 
   void mergeFrom(Permission src) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
index ec5ca06..6511d69 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
@@ -17,7 +17,7 @@
 public class PermissionRule implements Comparable<PermissionRule> {
   public static final String FORCE_PUSH = "Force Push";
   public static final String FORCE_EDIT = "Force Edit";
-  public static enum Action {
+  public enum Action {
     ALLOW, DENY, BLOCK,
 
     INTERACTIVE, BATCH
@@ -139,6 +139,10 @@
     switch (a.getAction()) {
       case DENY:
         return 0;
+      case ALLOW:
+      case BATCH:
+      case BLOCK:
+      case INTERACTIVE:
       default:
         return 1 + a.getAction().ordinal();
     }
@@ -260,6 +264,11 @@
     return rule;
   }
 
+  public boolean hasRange() {
+    return (!(getMin() == null || getMin() == 0))
+      || (!(getMax() == null || getMax() == 0));
+  }
+
   public static int parseInt(String value) {
     if (value.startsWith("+")) {
       value = value.substring(1);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectInfo.java
deleted file mode 100644
index dfef806..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectInfo.java
+++ /dev/null
@@ -1,36 +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.reviewdb.client.Project;
-
-public class ProjectInfo {
-  protected Project.NameKey key;
-
-  protected ProjectInfo() {
-  }
-
-  public ProjectInfo(final Project.NameKey key) {
-    this.key = key;
-  }
-
-  public Project.NameKey getKey() {
-    return key;
-  }
-
-  public String getName() {
-    return key.get();
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
deleted file mode 100644
index 76785d8..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
+++ /dev/null
@@ -1,134 +0,0 @@
-// Copyright (C) 2011 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.reviewdb.client.Change;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Result from performing a review (comment, abandon, etc.)
- */
-public class ReviewResult {
-  protected List<Error> errors;
-  protected Change.Id changeId;
-
-  public ReviewResult() {
-    errors = new ArrayList<>();
-  }
-
-  public void addError(final Error e) {
-    errors.add(e);
-  }
-
-  public List<Error> getErrors() {
-    return errors;
-  }
-
-  public Change.Id getChangeId() {
-    return changeId;
-  }
-
-  public void setChangeId(Change.Id changeId) {
-    this.changeId = changeId;
-  }
-
-  public static class Error {
-    public static enum Type {
-      /** Not permitted to abandon this change. */
-      ABANDON_NOT_PERMITTED,
-
-      /** Not permitted to restore this change. */
-      RESTORE_NOT_PERMITTED,
-
-      /** Not permitted to submit this change. */
-      SUBMIT_NOT_PERMITTED,
-
-      /** Approvals or dependencies are lacking for submission. */
-      SUBMIT_NOT_READY,
-
-      /** Review operation invalid because change is closed. */
-      CHANGE_IS_CLOSED,
-
-      /** Review operation invalid because change is not abandoned. */
-      CHANGE_NOT_ABANDONED,
-
-      /** Not permitted to publish this draft patch set */
-      PUBLISH_NOT_PERMITTED,
-
-      /** Not permitted to delete this draft patch set */
-      DELETE_NOT_PERMITTED,
-
-      /** Review operation not permitted by rule. */
-      RULE_ERROR,
-
-      /** Review operation invalid because patch set is not a draft. */
-      NOT_A_DRAFT,
-
-      /** Error writing change to git repository */
-      GIT_ERROR,
-
-      /** The destination branch does not exist */
-      DEST_BRANCH_NOT_FOUND,
-
-      /** Not permitted to edit the topic name */
-      EDIT_TOPIC_NAME_NOT_PERMITTED,
-
-      /** Not permitted to edit the hashtags */
-      EDIT_HASHTAGS_NOT_PERMITTED
-    }
-
-    protected Type type;
-    protected String message;
-
-    protected Error() {
-    }
-
-    public Error(final Type type) {
-      this.type = type;
-      this.message = null;
-    }
-
-    public Error(final Type type, final String message) {
-      this.type = type;
-      this.message = message;
-    }
-
-    public Type getType() {
-      return type;
-    }
-
-    public String getMessage() {
-      return message;
-    }
-
-    public String getMessageOrType() {
-      if (message != null) {
-        return message;
-      }
-      return "" + type;
-    }
-
-    @Override
-    public String toString() {
-      String ret = type + "";
-      if (message != null) {
-        ret += " " + message;
-      }
-      return ret;
-    }
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
index f4fe963..9dccf0c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -23,7 +23,7 @@
  * Describes the state required to submit a change.
  */
 public class SubmitRecord {
-  public static enum Status {
+  public enum Status {
     /** The change is ready for submission. */
     OK,
 
@@ -33,6 +33,9 @@
     /** The change has been closed. */
     CLOSED,
 
+    /** The change was submitted bypassing submit rules. */
+    FORCED,
+
     /**
      * An internal server error occurred preventing computation.
      * <p>
@@ -46,7 +49,7 @@
   public String errorMessage;
 
   public static class Label {
-    public static enum Status {
+    public enum Status {
       /**
        * This label provides what is necessary for submission.
        * <p>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
index 5a79d08..b6ce797 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
@@ -20,7 +20,7 @@
  * Describes the submit type for a change.
  */
 public class SubmitTypeRecord {
-  public static enum Status {
+  public enum Status {
     /** The type was computed successfully */
     OK,
 
@@ -32,15 +32,33 @@
   }
 
   public static SubmitTypeRecord OK(SubmitType type) {
-    SubmitTypeRecord r = new SubmitTypeRecord();
-    r.status = Status.OK;
-    r.type = type;
-    return r;
+    return new SubmitTypeRecord(Status.OK, type, null);
   }
 
-  public Status status;
-  public SubmitType type;
-  public String errorMessage;
+  public static SubmitTypeRecord error(String err) {
+    return new SubmitTypeRecord(SubmitTypeRecord.Status.RULE_ERROR, null, err);
+  }
+
+  /** Status enum value of the record. */
+  public final Status status;
+
+  /** Submit type of the record; never null if {@link #status} is {@code OK}. */
+  public final SubmitType type;
+
+  /**
+   * Submit type of the record; always null if {@link #status} is {@code OK}.
+   */
+  public final String errorMessage;
+
+  private SubmitTypeRecord(Status status, SubmitType type, String errorMessage) {
+    this.status = status;
+    this.type = type;
+    this.errorMessage = errorMessage;
+  }
+
+  public boolean isOk() {
+    return status == Status.OK;
+  }
 
   @Override
   public String toString() {
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
new file mode 100644
index 0000000..3fdc331
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
@@ -0,0 +1,111 @@
+// 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.common.data;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+
+import org.eclipse.jgit.transport.RefSpec;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/** Portion of a {@link Project} describing superproject subscription rules. */
+@GwtIncompatible("Unemulated org.eclipse.jgit.transport.RefSpec")
+public class SubscribeSection {
+
+  private final List<RefSpec> multiMatchRefSpecs;
+  private final List<RefSpec> matchingRefSpecs;
+  private final Project.NameKey project;
+
+  public SubscribeSection(Project.NameKey p) {
+    project = p;
+    matchingRefSpecs = new ArrayList<>();
+    multiMatchRefSpecs = new ArrayList<>();
+  }
+
+  public void addMatchingRefSpec(RefSpec spec) {
+    matchingRefSpecs.add(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() {
+    return project;
+  }
+
+  /**
+   * Determines if the <code>branch</code> could trigger a
+   * superproject update as allowed via this subscribe section.
+   *
+   * @param branch the branch to check
+   * @return if the branch could trigger a superproject update
+   */
+  public boolean appliesTo(Branch.NameKey branch) {
+    for (RefSpec r : matchingRefSpecs) {
+      if (r.matchSource(branch.get())) {
+        return true;
+      }
+    }
+    for (RefSpec r : multiMatchRefSpecs) {
+      if (r.matchSource(branch.get())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public Collection<RefSpec> getMatchingRefSpecs() {
+    return Collections.unmodifiableCollection(matchingRefSpecs);
+  }
+
+  public Collection<RefSpec> getMultiMatchRefSpecs() {
+    return Collections.unmodifiableCollection(multiMatchRefSpecs);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder ret = new StringBuilder();
+    ret.append("[SubscribeSection, project=");
+    ret.append(project);
+    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/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
deleted file mode 100644
index 7b25a23..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
+++ /dev/null
@@ -1,29 +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.reviewdb.client.Project;
-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 java.util.List;
-
-@RpcImpl(version = Version.V2_0)
-public interface SuggestService extends RemoteJsonService {
-  void suggestAccountGroupForProject(Project.NameKey project, String query,
-      int limit, AsyncCallback<List<GroupReference>> callback);
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/CorruptEntityException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/CorruptEntityException.java
deleted file mode 100644
index f2b3e98..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/CorruptEntityException.java
+++ /dev/null
@@ -1,28 +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.errors;
-
-import com.google.gwtorm.client.Key;
-
-/** Error indicating the entity's database records are invalid. */
-public class CorruptEntityException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE_PREFIX = "Corrupt Database Entity: ";
-
-  public CorruptEntityException(final Key<?> key) {
-    super(MESSAGE_PREFIX + key);
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InactiveAccountException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InactiveAccountException.java
deleted file mode 100644
index 6ae5eb6..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InactiveAccountException.java
+++ /dev/null
@@ -1,26 +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.errors;
-
-/** Error indicating the account is currently inactive. */
-public class InactiveAccountException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Account Inactive: ";
-
-  public InactiveAccountException(String who) {
-    super(MESSAGE + who);
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java
index 53792e4..4a66a416a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java
@@ -18,10 +18,6 @@
 public class InvalidQueryException extends Exception {
   private static final long serialVersionUID = 1L;
 
-  public InvalidQueryException(String message) {
-    super(message);
-  }
-
   public InvalidQueryException(String message, String query) {
     super("Invalid query: " + query + "\n\n" + message);
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidRevisionException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidRevisionException.java
deleted file mode 100644
index b4b54c1..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidRevisionException.java
+++ /dev/null
@@ -1,26 +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.common.errors;
-
-/** Error indicating the revision is invalid as supplied. */
-public class InvalidRevisionException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Invalid Revision";
-
-  public InvalidRevisionException() {
-    super(MESSAGE);
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidUserNameException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidUserNameException.java
deleted file mode 100644
index f1c35a8..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidUserNameException.java
+++ /dev/null
@@ -1,29 +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.common.errors;
-
-import com.google.gerrit.reviewdb.client.Account;
-
-/** Error indicating the SSH user name does not match {@link Account#USER_NAME_PATTERN} pattern. */
-public class InvalidUserNameException extends Exception {
-
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Invalid user name.";
-
-  public InvalidUserNameException() {
-    super(MESSAGE);
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
index ade0d98..8b740c3 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
@@ -38,10 +38,6 @@
     super(MESSAGE + key.toString(), why);
   }
 
-  public NoSuchGroupException(final AccountGroup.NameKey k) {
-    this(k, null);
-  }
-
   public NoSuchGroupException(final AccountGroup.NameKey k, final Throwable why) {
     super(MESSAGE + k.toString(), why);
   }
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index 67bfdc1..61cd406 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -1,3 +1,6 @@
+include_defs('//lib/JGIT_VERSION')
+include_defs('//lib/GUAVA_VERSION')
+
 SRC = 'src/main/java/com/google/gerrit/extensions/'
 SRCS = glob([SRC + '**/*.java'])
 
@@ -27,6 +30,7 @@
   name = 'lib',
   exported_deps = [
     ':api',
+    '//lib:guava',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
@@ -38,7 +42,11 @@
 java_library(
   name = 'api',
   srcs = glob([SRC + '**/*.java']),
+  deps = [
+    '//gerrit-common:annotations',
+  ],
   provided_deps = [
+    '//lib:guava',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
   ],
@@ -69,9 +77,13 @@
   paths = ['src/main/java'],
   srcs = SRCS,
   deps = [
+    '//lib:guava',
     '//lib/guice:javax-inject',
     '//lib/guice:guice_library',
     '//lib/guice:guice-assistedinject',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//gerrit-common:annotations',
   ],
   visibility = ['PUBLIC'],
+  external_docs = [JGIT_DOC_URL, GUAVA_DOC_URL],
 )
diff --git a/gerrit-extension-api/BUILD b/gerrit-extension-api/BUILD
new file mode 100644
index 0000000..4a5cfe3
--- /dev/null
+++ b/gerrit-extension-api/BUILD
@@ -0,0 +1,46 @@
+load('//tools/bzl:gwt.bzl', 'gwt_module')
+
+SRC = 'src/main/java/com/google/gerrit/extensions/'
+SRCS = glob([SRC + '**/*.java'])
+
+EXT_API_SRCS = glob([SRC + 'client/*.java'])
+
+gwt_module(
+  name = 'client',
+  srcs = EXT_API_SRCS,
+  gwt_xml = SRC + 'Extensions.gwt.xml',
+  visibility = ['//visibility:public'],
+)
+
+java_binary(
+  name = 'extension-api',
+  main_class = 'Dummy',
+  runtime_deps = [':lib'],
+  visibility = ['//visibility:public'],
+)
+
+java_library(
+  name = 'lib',
+  exports = [
+    ':api',
+    '//lib:guava',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice-servlet',
+    '//lib:servlet-api-3_1',
+  ],
+  visibility = ['//visibility:public'],
+)
+
+#TODO(davido): There is no provided_deps argument to java_library rule
+java_library(
+  name = 'api',
+  srcs = glob([SRC + '**/*.java']),
+  deps = [
+    '//gerrit-common:annotations',
+    '//lib:guava',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+  ],
+  visibility = ['//visibility:public'],
+)
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 287e634..083e644 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.12.9</version>
+  <version>2.13.14</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
@@ -23,15 +23,24 @@
 
   <developers>
     <developer>
+      <name>Andrew Bonventre</name>
+    </developer>
+    <developer>
       <name>Dave Borowitz</name>
     </developer>
     <developer>
+      <name>David Ostrovsky</name>
+    </developer>
+    <developer>
       <name>David Pursehouse</name>
     </developer>
     <developer>
       <name>Edwin Kempin</name>
     </developer>
     <developer>
+      <name>Hugo Arès</name>
+    </developer>
+    <developer>
       <name>Martin Fick</name>
     </developer>
     <developer>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
index 8b9f520..c2e3787 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
@@ -22,17 +22,17 @@
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 
 public interface GerritApi {
-  public Accounts accounts();
-  public Changes changes();
-  public Config config();
-  public Groups groups();
-  public Projects projects();
+  Accounts accounts();
+  Changes changes();
+  Config config();
+  Groups groups();
+  Projects projects();
 
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements GerritApi {
+  class NotImplemented implements GerritApi {
     @Override
     public Accounts accounts() {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/AccessSectionInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/AccessSectionInfo.java
new file mode 100644
index 0000000..7c47ec5
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/AccessSectionInfo.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.api.access;
+
+import java.util.Map;
+import java.util.Objects;
+
+public class AccessSectionInfo {
+
+  public Map<String, PermissionInfo> permissions;
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof AccessSectionInfo) {
+      return Objects.equals(permissions, ((AccessSectionInfo) obj).permissions);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(permissions);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PermissionInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PermissionInfo.java
new file mode 100644
index 0000000..c4808a5
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PermissionInfo.java
@@ -0,0 +1,44 @@
+// 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.access;
+
+import java.util.Map;
+import java.util.Objects;
+
+public class PermissionInfo {
+  public String label;
+  public Boolean exclusive;
+  public Map<String, PermissionRuleInfo> rules;
+
+  public PermissionInfo(String label, Boolean exclusive) {
+    this.label = label;
+    this.exclusive = exclusive;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof PermissionInfo) {
+      PermissionInfo p = (PermissionInfo) obj;
+      return Objects.equals(label, p.label)
+          && Objects.equals(exclusive, p.exclusive)
+          && Objects.equals(rules, p.rules);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(label, exclusive, rules);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PermissionRuleInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PermissionRuleInfo.java
new file mode 100644
index 0000000..f979039
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PermissionRuleInfo.java
@@ -0,0 +1,53 @@
+// 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.access;
+
+import java.util.Objects;
+
+public class PermissionRuleInfo {
+  public enum Action {
+    ALLOW,
+    DENY,
+    BLOCK,
+    INTERACTIVE,
+    BATCH
+  }
+
+  public Action action;
+  public Boolean force;
+  public Integer min;
+  public Integer max;
+
+  public PermissionRuleInfo(Action action, Boolean force) {
+    this.action = action;
+    this.force = force;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof PermissionRuleInfo) {
+      PermissionRuleInfo p = (PermissionRuleInfo) obj;
+      return Objects.equals(action, p.action)
+          && Objects.equals(force, p.force)
+          && Objects.equals(min, p.min)
+          && Objects.equals(max, p.max);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(action, force, min, max);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/ProjectAccessInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/ProjectAccessInfo.java
new file mode 100644
index 0000000..3274313a
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/ProjectAccessInfo.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.api.access;
+
+import com.google.gerrit.extensions.common.ProjectInfo;
+
+import java.util.Map;
+import java.util.Set;
+
+public class ProjectAccessInfo {
+  public String revision;
+  public ProjectInfo inheritsFrom;
+  public Map<String, AccessSectionInfo> local;
+  public Boolean isOwner;
+  public Set<String> ownerOf;
+  public Boolean canUpload;
+  public Boolean canAdd;
+  public Boolean configVisible;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/ProjectAccessInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/ProjectAccessInput.java
new file mode 100644
index 0000000..39a5209
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/ProjectAccessInput.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.api.access;
+
+import java.util.Map;
+
+public class ProjectAccessInput {
+  public Map<String, AccessSectionInfo> remove;
+  public Map<String, AccessSectionInfo> add;
+  public String parent;
+  public String message;
+}
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 a356ab6..8f7f93c 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
@@ -14,43 +14,157 @@
 
 package com.google.gerrit.extensions.api.accounts;
 
+import com.google.gerrit.extensions.api.changes.StarsInput;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+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;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 import java.util.List;
 import java.util.Map;
+import java.util.SortedSet;
 
 public interface AccountApi {
   AccountInfo get() throws RestApiException;
 
-  void starChange(String id) throws RestApiException;
-  void unstarChange(String id) throws RestApiException;
+  String getAvatarUrl(int size) throws RestApiException;
+
+  GeneralPreferencesInfo getPreferences() throws RestApiException;
+  GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in)
+      throws RestApiException;
+
+  DiffPreferencesInfo getDiffPreferences() throws RestApiException;
+  DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in)
+      throws RestApiException;
+
+  EditPreferencesInfo getEditPreferences() throws RestApiException;
+  EditPreferencesInfo setEditPreferences(EditPreferencesInfo in)
+      throws RestApiException;
+
+  List<ProjectWatchInfo> getWatchedProjects() throws RestApiException;
+  List<ProjectWatchInfo> setWatchedProjects(List<ProjectWatchInfo> in)
+      throws RestApiException;
+  void deleteWatchedProjects(List<ProjectWatchInfo> in)
+      throws RestApiException;
+
+  void starChange(String changeId) throws RestApiException;
+  void unstarChange(String changeId) throws RestApiException;
+  void setStars(String changeId, StarsInput input) throws RestApiException;
+  SortedSet<String> getStars(String changeId) throws RestApiException;
+  List<ChangeInfo> getStarredChanges() throws RestApiException;
+
   void addEmail(EmailInput input) throws RestApiException;
 
+  List<SshKeyInfo> listSshKeys() throws RestApiException;
+  SshKeyInfo addSshKey(String key) throws RestApiException;
+  void deleteSshKey(int seq) throws RestApiException;
+
   Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException;
   Map<String, GpgKeyInfo> putGpgKeys(List<String> add, List<String> remove)
       throws RestApiException;
   GpgKeyApi gpgKey(String id) throws RestApiException;
 
+  List<AgreementInfo> listAgreements() throws RestApiException;
+  void signAgreement(String agreementName) throws RestApiException;
+
+  void index() throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements AccountApi {
+  class NotImplemented implements AccountApi {
     @Override
     public AccountInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void starChange(String id) throws RestApiException {
+    public String getAvatarUrl(int size) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void unstarChange(String id) throws RestApiException {
+    public GeneralPreferencesInfo getPreferences() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public DiffPreferencesInfo getDiffPreferences() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public EditPreferencesInfo getEditPreferences() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public EditPreferencesInfo setEditPreferences(EditPreferencesInfo in)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<ProjectWatchInfo> getWatchedProjects()
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<ProjectWatchInfo> setWatchedProjects(
+        List<ProjectWatchInfo> in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void deleteWatchedProjects(List<ProjectWatchInfo> in)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void starChange(String changeId) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void unstarChange(String changeId) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void setStars(String changeId, StarsInput input)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public SortedSet<String> getStars(String changeId) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<ChangeInfo> getStarredChanges() throws RestApiException {
       throw new NotImplementedException();
     }
 
@@ -60,6 +174,21 @@
     }
 
     @Override
+    public List<SshKeyInfo> listSshKeys() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public SshKeyInfo addSshKey(String key) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void deleteSshKey(int seq) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public Map<String, GpgKeyInfo> putGpgKeys(List<String> add,
         List<String> remove) throws RestApiException {
       throw new NotImplementedException();
@@ -74,5 +203,20 @@
     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();
+    }
+
+    @Override
+    public void index() 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 32f8488..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
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.extensions.api.accounts;
 
+import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
+import java.util.Arrays;
+import java.util.EnumSet;
 import java.util.List;
 
 public interface Accounts {
@@ -38,12 +41,23 @@
   AccountApi id(String id) throws RestApiException;
 
   /**
+   * @see #id(String)
+   */
+  AccountApi id(int id) throws RestApiException;
+
+  /**
    * Look up the account of the current in-scope user.
    *
    * @see #id(String)
    */
   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>
@@ -65,12 +79,31 @@
     throws RestApiException;
 
   /**
+   * Queries users.
+   * <p>
+   * Example code:
+   * {@code query().withQuery("name:John email:example.com").withLimit(5).get()}
+   *
+   * @return API for setting parameters and getting result.
+   */
+  QueryRequest query() throws RestApiException;
+
+  /**
+   * Queries users.
+   * <p>
+   * Shortcut API for {@code query().withQuery(String)}.
+   *
+   * @see #query()
+   */
+  QueryRequest query(String query) throws RestApiException;
+
+  /**
    * API for setting parameters and getting result.
    * Used for {@code suggestAccounts()}.
    *
    * @see #suggestAccounts()
    */
-  public abstract class SuggestAccountsRequest {
+  abstract class SuggestAccountsRequest {
     private String query;
     private int limit;
 
@@ -108,21 +141,114 @@
   }
 
   /**
+   * API for setting parameters and getting result.
+   * Used for {@code query()}.
+   *
+   * @see #query()
+   */
+  abstract class QueryRequest {
+    private String query;
+    private int limit;
+    private int start;
+    private EnumSet<ListAccountsOption> options =
+        EnumSet.noneOf(ListAccountsOption.class);
+
+    /**
+     * Executes query and returns a list of accounts.
+     */
+    public abstract List<AccountInfo> get() throws RestApiException;
+
+    /**
+     * Set query.
+     *
+     * @param query needs to be in human-readable form.
+     */
+    public QueryRequest withQuery(String query) {
+      this.query = query;
+      return this;
+    }
+
+    /**
+     * Set limit for returned list of accounts.
+     * Optional; server-default is used when not provided.
+     */
+    public QueryRequest withLimit(int limit) {
+      this.limit = limit;
+      return this;
+    }
+
+    /**
+     * Set number of accounts to skip.
+     * Optional; no accounts are skipped when not provided.
+     */
+    public QueryRequest withStart(int start) {
+      this.start = start;
+      return this;
+    }
+
+    public QueryRequest withOption(ListAccountsOption options) {
+      this.options.add(options);
+      return this;
+    }
+
+    public QueryRequest withOptions(ListAccountsOption... options) {
+      this.options.addAll(Arrays.asList(options));
+      return this;
+    }
+
+    public QueryRequest withOptions(EnumSet<ListAccountsOption> options) {
+      this.options = options;
+      return this;
+    }
+
+    public String getQuery() {
+      return query;
+    }
+
+    public int getLimit() {
+      return limit;
+    }
+
+    public int getStart() {
+      return start;
+    }
+
+    public EnumSet<ListAccountsOption> getOptions() {
+      return options;
+    }
+  }
+
+  /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements Accounts {
+  class NotImplemented implements Accounts {
     @Override
     public AccountApi id(String id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
+    public AccountApi id(int id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public AccountApi self() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @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();
     }
@@ -132,5 +258,15 @@
       throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public QueryRequest query() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public QueryRequest query(String query) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
index ffdcf87..6f87e8b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
@@ -26,7 +26,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    */
-  public class NotImplemented implements GpgKeyApi {
+  class NotImplemented implements GpgKeyApi {
     @Override
     public GpgKeyInfo get() 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..4c535d4 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,23 @@
 
 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 NotifyHandling notify;
 
   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/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index ce07098..f656c2d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -58,12 +58,28 @@
    */
   RevisionApi revision(String id) throws RestApiException;
 
+  /**
+   * Look up the reviewer of the change.
+   * <p>
+   * @param id ID of the account, can be a string of the format
+   *     "Full Name &lt;mail@example.com&gt;", just the email address, a full name
+   *     if it is unique, an account ID, a user name or 'self' for the
+   *     calling user.
+   * @return API for accessing the reviewer.
+   * @throws RestApiException if id is not account ID or is a user that isn't
+   *     known to be a reviewer for this change.
+   */
+  ReviewerApi reviewer(String id) throws RestApiException;
+
   void abandon() throws RestApiException;
   void abandon(AbandonInput in) throws RestApiException;
 
   void restore() throws RestApiException;
   void restore(RestoreInput in) throws RestApiException;
 
+  void move(String destination) throws RestApiException;
+  void move(MoveInput in) throws RestApiException;
+
   /**
    * Create a new change that reverts this change.
    *
@@ -79,6 +95,18 @@
   ChangeApi revert(RevertInput in) throws RestApiException;
 
   List<ChangeInfo> submittedTogether() throws RestApiException;
+  SubmittedTogetherInfo submittedTogether(
+      EnumSet<SubmittedTogetherOption> options) throws RestApiException;
+
+  /**
+   * Publishes a draft change.
+   */
+  void publish() throws RestApiException;
+
+  /**
+   * Deletes a draft change.
+   */
+  void delete() throws RestApiException;
 
   String topic() throws RestApiException;
   void topic(String topic) throws RestApiException;
@@ -130,8 +158,9 @@
 
   ChangeInfo check() throws RestApiException;
   ChangeInfo check(FixInput fix) throws RestApiException;
+  void index() throws RestApiException;
 
-  public abstract class SuggestedReviewersRequest {
+  abstract class SuggestedReviewersRequest {
     private String query;
     private int limit;
 
@@ -160,7 +189,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements ChangeApi {
+  class NotImplemented implements ChangeApi {
     @Override
     public String id() {
       throw new NotImplementedException();
@@ -177,6 +206,11 @@
     }
 
     @Override
+    public ReviewerApi reviewer(String id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public RevisionApi revision(String id) throws RestApiException {
       throw new NotImplementedException();
     }
@@ -202,6 +236,16 @@
     }
 
     @Override
+    public void move(String destination) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void move(MoveInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public ChangeApi revert() throws RestApiException {
       throw new NotImplementedException();
     }
@@ -212,6 +256,16 @@
     }
 
     @Override
+    public void publish() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void delete() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public String topic() throws RestApiException {
       throw new NotImplementedException();
     }
@@ -292,8 +346,19 @@
     }
 
     @Override
+    public void index() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public List<ChangeInfo> submittedTogether() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public SubmittedTogetherInfo submittedTogether(
+        EnumSet<SubmittedTogetherOption> options) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
index da8aeb2..aa67473 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
@@ -58,12 +59,12 @@
   ChangeApi id(String project, String branch, String id)
       throws RestApiException;
 
-  ChangeApi create(ChangeInfo in) throws RestApiException;
+  ChangeApi create(ChangeInput in) throws RestApiException;
 
   QueryRequest query();
   QueryRequest query(String query);
 
-  public abstract class QueryRequest {
+  abstract class QueryRequest {
     private String query;
     private int limit;
     private int start;
@@ -139,7 +140,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements Changes {
+  class NotImplemented implements Changes {
     @Override
     public ChangeApi id(int id) throws RestApiException {
       throw new NotImplementedException();
@@ -156,7 +157,7 @@
     }
 
     @Override
-    public ChangeApi create(ChangeInfo in) throws RestApiException {
+    public ChangeApi create(ChangeInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
index d0c5633..adac284 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
@@ -25,7 +25,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements CommentApi {
+  class NotImplemented implements CommentApi {
     @Override
     public CommentInfo get() throws RestApiException {
       throw new NotImplementedException();
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
new file mode 100644
index 0000000..671f43e
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.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.api.changes;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+/** Input passed to {@code DELETE /changes/[id]/reviewers/[id]/votes/[label]}. */
+public class DeleteVoteInput {
+  @DefaultInput
+  public String label;
+
+  /** Who to send email notifications to after vote is deleted. */
+  public NotifyHandling notify = NotifyHandling.ALL;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java
index 80a71f8..50335db 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java
@@ -26,7 +26,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented extends CommentApi.NotImplemented
+  class NotImplemented extends CommentApi.NotImplemented
       implements DraftApi {
     @Override
     public CommentInfo update(DraftInput in) throws RestApiException {
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 dd8f488..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,5 +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 f5f087c..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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -34,10 +35,66 @@
   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.
+   */
+  DiffRequest diffRequest() throws RestApiException;
+
+  abstract class DiffRequest {
+    private String base;
+    private Integer context;
+    private Boolean intraline;
+    private Whitespace whitespace;
+
+    public abstract DiffInfo get() throws RestApiException;
+
+    public DiffRequest withBase(String base) {
+      this.base = base;
+      return this;
+    }
+
+    public DiffRequest withContext(int context) {
+      this.context = context;
+      return this;
+    }
+
+    public DiffRequest withIntraline(boolean intraline) {
+      this.intraline = intraline;
+      return this;
+    }
+
+    public DiffRequest withWhitespace(Whitespace whitespace) {
+      this.whitespace = whitespace;
+      return this;
+    }
+
+    public String getBase() {
+      return base;
+    }
+
+    public Integer getContext() {
+      return context;
+    }
+
+    public Boolean getIntraline() {
+      return intraline;
+    }
+
+    public Whitespace getWhitespace() {
+      return whitespace;
+    }
+  }
+
+  /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements FileApi {
+  class NotImplemented implements FileApi {
     @Override
     public BinaryResult content() throws RestApiException {
       throw new NotImplementedException();
@@ -52,5 +109,15 @@
     public DiffInfo diff(String base) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @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/MoveInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/MoveInput.java
new file mode 100644
index 0000000..795642a
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/MoveInput.java
@@ -0,0 +1,20 @@
+// 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.extensions.api.changes;
+
+public class MoveInput {
+  public String message;
+  public String destinationBranch;
+}
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 ee043eb..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;
@@ -26,6 +30,8 @@
   @DefaultInput
   public String message;
 
+  public String tag;
+
   public Map<String, Short> labels;
   public Map<String, List<CommentInput>> comments;
 
@@ -66,7 +72,12 @@
    */
   public String onBehalfOf;
 
-  public static enum DraftHandling {
+  /**
+   * Reviewers that should be added to this change.
+   */
+  public List<AddReviewerInput> reviewers;
+
+  public enum DraftHandling {
     /** Delete pending drafts on this revision only. */
     DELETE,
 
@@ -80,10 +91,6 @@
     PUBLISH_ALL_REVISIONS
   }
 
-  public static enum NotifyHandling {
-    NONE, OWNER, OWNER_REVIEWERS, ALL
-  }
-
   public static class CommentInput extends Comment {
   }
 
@@ -114,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/ReviewerApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
new file mode 100644
index 0000000..d1f09e8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
@@ -0,0 +1,54 @@
+// 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.extensions.api.changes;
+
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+import java.util.Map;
+
+public interface ReviewerApi {
+
+  Map<String, Short> votes() throws RestApiException;
+  void deleteVote(String label) throws RestApiException;
+  void deleteVote(DeleteVoteInput input) throws RestApiException;
+  void remove() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  class NotImplemented implements ReviewerApi {
+    @Override
+    public Map<String, Short> votes() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void deleteVote(String label) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void deleteVote(DeleteVoteInput input) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void remove() throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
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 44c2ba4..2731476 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
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.common.MergeableInfo;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -28,6 +30,7 @@
 
 public interface RevisionApi {
   void delete() throws RestApiException;
+
   void review(ReviewInput in) throws RestApiException;
 
   void submit() throws RestApiException;
@@ -36,13 +39,14 @@
   ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
   ChangeApi rebase() throws RestApiException;
   ChangeApi rebase(RebaseInput in) throws RestApiException;
-  boolean canRebase();
+  boolean canRebase() throws RestApiException;
 
   void setReviewed(String path, boolean reviewed) throws RestApiException;
   Set<String> reviewed() throws RestApiException;
 
   Map<String, FileInfo> files() throws RestApiException;
   Map<String, FileInfo> files(String base) throws RestApiException;
+  Map<String, FileInfo> files(int parentNum) throws RestApiException;
   FileApi file(String path);
   MergeableInfo mergeable() throws RestApiException;
   MergeableInfo mergeableOtherBranches() throws RestApiException;
@@ -65,11 +69,14 @@
 
   Map<String, ActionInfo> actions() throws RestApiException;
 
+  SubmitType submitType() throws RestApiException;
+  SubmitType testSubmitType(TestSubmitRuleInput in) throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements RevisionApi {
+  class NotImplemented implements RevisionApi {
     @Override
     public void delete() throws RestApiException {
       throw new NotImplementedException();
@@ -141,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();
     }
@@ -194,5 +206,16 @@
     public Map<String, ActionInfo> actions() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public SubmitType submitType() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public SubmitType testSubmitType(TestSubmitRuleInput in)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/StarsInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/StarsInput.java
new file mode 100644
index 0000000..d3dff98
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/StarsInput.java
@@ -0,0 +1,34 @@
+// 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 java.util.Set;
+
+public class StarsInput {
+  public Set<String> add;
+  public Set<String> remove;
+
+  public StarsInput() {
+  }
+
+  public StarsInput(Set<String> add) {
+    this.add = add;
+  }
+
+  public StarsInput(Set<String> add, Set<String> remove) {
+    this.add = add;
+    this.remove = remove;
+  }
+}
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 4e08f8d..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
@@ -15,7 +15,11 @@
 package com.google.gerrit.extensions.api.changes;
 
 public class SubmitInput {
+  /** Not used anymore, kept for backward compatibility */
   @Deprecated
   public boolean waitForMerge;
+
   public String onBehalfOf;
+
+  public NotifyHandling notify = NotifyHandling.ALL;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherInfo.java
new file mode 100644
index 0000000..52b6904
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherInfo.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.api.changes;
+
+import com.google.gerrit.extensions.common.ChangeInfo;
+
+import java.util.List;
+
+public class SubmittedTogetherInfo {
+  public List<ChangeInfo> changes;
+  public int nonVisibleChanges;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherOption.java
new file mode 100644
index 0000000..8649e91f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherOption.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.extensions.api.changes;
+
+/** Output options available for submitted_together requests. */
+public enum SubmittedTogetherOption {
+  NON_VISIBLE_CHANGES(0);
+
+  private final int value;
+
+  SubmittedTogetherOption(int v) {
+    value = v;
+  }
+
+  public int getValue() {
+    return value;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Config.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Config.java
index 348cf4b..3f4971e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Config.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Config.java
@@ -26,7 +26,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements Config {
+  class NotImplemented implements Config {
     @Override
     public Server server() {
       throw new NotImplementedException();
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 c093b18..a43c29f 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
@@ -14,6 +14,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.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
@@ -23,14 +25,45 @@
    */
   String getVersion() throws RestApiException;
 
+  GeneralPreferencesInfo getDefaultPreferences() throws RestApiException;
+  GeneralPreferencesInfo setDefaultPreferences(GeneralPreferencesInfo in)
+      throws RestApiException;
+  DiffPreferencesInfo getDefaultDiffPreferences() throws RestApiException;
+  DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in)
+      throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements Server {
+  class NotImplemented implements Server {
     @Override
     public String getVersion() 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();
+    }
+
+    @Override
+    public DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
index f5a89c0..d3f4463 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.common.GroupAuditEventInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 import java.util.List;
@@ -141,4 +142,102 @@
    * @throws RestApiException
    */
   List<? extends GroupAuditEventInfo> auditLog() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  class NotImplemented implements GroupApi {
+    @Override
+    public GroupInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GroupInfo detail() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public String name() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void name(String name) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GroupInfo owner() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void owner(String owner) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public String description() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void description(String description) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GroupOptionsInfo options() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void options(GroupOptionsInfo options) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<AccountInfo> members() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<AccountInfo> members(boolean recursive)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void addMembers(String... members) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void removeMembers(String... members) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<GroupInfo> includedGroups() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void addGroups(String... groups) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void removeGroups(String... groups) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<? extends GroupAuditEventInfo> auditLog()
+        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/api/groups/Groups.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
index b909f31..b58009d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 import java.util.ArrayList;
@@ -51,7 +52,7 @@
   /** @return new request for listing groups. */
   ListRequest list();
 
-  public abstract class ListRequest {
+  abstract class ListRequest {
     private final EnumSet<ListGroupsOption> options =
         EnumSet.noneOf(ListGroupsOption.class);
     private final List<String> projects = new ArrayList<>();
@@ -114,6 +115,11 @@
       return this;
     }
 
+    public ListRequest withOwned(boolean owned) {
+      this.owned = owned;
+      return this;
+    }
+
     public ListRequest withLimit(int limit) {
       this.limit = limit;
       return this;
@@ -174,4 +180,30 @@
       return suggest;
     }
   }
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  class NotImplemented implements Groups {
+    @Override
+    public GroupApi id(String id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GroupApi create(String name) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GroupApi create(GroupInput input) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ListRequest list() {
+      throw new NotImplementedException();
+    }
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
index 91cb70e..222248d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
@@ -25,10 +26,15 @@
   void delete() throws RestApiException;
 
   /**
+   * Returns the content of a file from the HEAD revision.
+   */
+  BinaryResult file(String path) throws RestApiException;
+
+  /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements BranchApi {
+  class NotImplemented implements BranchApi {
     @Override
     public BranchApi create(BranchInput in) throws RestApiException {
       throw new NotImplementedException();
@@ -43,5 +49,10 @@
     public void delete() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public BinaryResult file(String path) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInput.java
index cfe9a99..506bcd4 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInput.java
@@ -19,4 +19,5 @@
 public class BranchInput {
   @DefaultInput
   public String revision;
+  public String ref;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
index a930f0d..3bffac0 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
@@ -26,7 +26,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements ChildProjectApi {
+  class NotImplemented implements ChildProjectApi {
     @Override
     public ProjectInfo get() throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommentLinkInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommentLinkInfo.java
new file mode 100644
index 0000000..64ad6086
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommentLinkInfo.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.api.projects;
+
+public class CommentLinkInfo {
+  public String match;
+  public String link;
+  public String html;
+  public Boolean enabled; // null means true
+
+  public transient String name;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
new file mode 100644
index 0000000..81c999bc
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -0,0 +1,69 @@
+// 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.projects;
+
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ActionInfo;
+
+import java.util.List;
+import java.util.Map;
+
+public class ConfigInfo {
+  public String description;
+  public InheritedBooleanInfo useContributorAgreements;
+  public InheritedBooleanInfo useContentMerge;
+  public InheritedBooleanInfo useSignedOffBy;
+  public InheritedBooleanInfo createNewChangeForAllNotInTarget;
+  public InheritedBooleanInfo requireChangeId;
+  public InheritedBooleanInfo enableSignedPush;
+  public InheritedBooleanInfo requireSignedPush;
+  public InheritedBooleanInfo rejectImplicitMerges;
+  public MaxObjectSizeLimitInfo maxObjectSizeLimit;
+  public SubmitType submitType;
+  public ProjectState state;
+  public Map<String, Map<String, ConfigParameterInfo>> pluginConfig;
+  public Map<String, ActionInfo> actions;
+
+  public Map<String, CommentLinkInfo> commentlinks;
+  public ThemeInfo theme;
+
+  public static class InheritedBooleanInfo {
+    public Boolean value;
+    public InheritableBoolean configuredValue;
+    public Boolean inheritedValue;
+  }
+
+  public static class MaxObjectSizeLimitInfo {
+    public String value;
+    public String configuredValue;
+    public String inheritedValue;
+  }
+
+  public static class ConfigParameterInfo {
+    public String displayName;
+    public String description;
+    public String warning;
+    public ProjectConfigEntryType type;
+    public String value;
+    public Boolean editable;
+    public Boolean inheritable;
+    public String configuredValue;
+    public String inheritedValue;
+    public List<String> permittedValues;
+    public List<String> values;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
new file mode 100644
index 0000000..8ab13f6
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
@@ -0,0 +1,37 @@
+// 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.projects;
+
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.client.SubmitType;
+
+import java.util.Map;
+
+public class ConfigInput {
+  public String description;
+  public InheritableBoolean useContributorAgreements;
+  public InheritableBoolean useContentMerge;
+  public InheritableBoolean useSignedOffBy;
+  public InheritableBoolean createNewChangeForAllNotInTarget;
+  public InheritableBoolean requireChangeId;
+  public InheritableBoolean enableSignedPush;
+  public InheritableBoolean requireSignedPush;
+  public InheritableBoolean rejectImplicitMerges;
+  public String maxObjectSizeLimit;
+  public SubmitType submitType;
+  public ProjectState state;
+  public Map<String, Map<String, ConfigValue>> pluginConfigValues;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigValue.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigValue.java
new file mode 100644
index 0000000..5d6d2b0
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigValue.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.api.projects;
+
+import java.util.List;
+
+public class ConfigValue {
+  public String value;
+  public List<String> values;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DeleteBranchesInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DeleteBranchesInput.java
new file mode 100644
index 0000000..e8108a5
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DeleteBranchesInput.java
@@ -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.
+
+package com.google.gerrit.extensions.api.projects;
+
+import java.util.List;
+
+public class DeleteBranchesInput {
+  public List<String> branches;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DescriptionInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DescriptionInput.java
new file mode 100644
index 0000000..d329510
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DescriptionInput.java
@@ -0,0 +1,23 @@
+// 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.extensions.api.projects;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+public class DescriptionInput {
+  @DefaultInput
+  public String description;
+  public String commitMessage;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index e3eb4be..e111291 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
+import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -26,12 +28,20 @@
   ProjectInfo get() throws RestApiException;
 
   String description() throws RestApiException;
-  void description(PutDescriptionInput in) throws RestApiException;
+  void description(DescriptionInput in) throws RestApiException;
+
+  ProjectAccessInfo access() throws RestApiException;
+  ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException;
+
+  ConfigInfo config() throws RestApiException;
+  ConfigInfo config(ConfigInput in) throws RestApiException;
 
   ListRefsRequest<BranchInfo> branches();
   ListRefsRequest<TagInfo> tags();
 
-  public abstract class ListRefsRequest<T extends RefInfo> {
+  void deleteBranches(DeleteBranchesInput in) throws RestApiException;
+
+  abstract class ListRefsRequest<T extends RefInfo> {
     protected int limit;
     protected int start;
     protected String substring;
@@ -108,7 +118,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements ProjectApi {
+  class NotImplemented implements ProjectApi {
     @Override
     public ProjectApi create() throws RestApiException {
       throw new NotImplementedException();
@@ -130,7 +140,28 @@
     }
 
     @Override
-    public void description(PutDescriptionInput in)
+    public ProjectAccessInfo access() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ConfigInfo config() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ConfigInfo config(ConfigInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ProjectAccessInfo access(ProjectAccessInput p)
+      throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void description(DescriptionInput in)
         throws RestApiException {
       throw new NotImplementedException();
     }
@@ -169,5 +200,10 @@
     public TagApi tag(String ref) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public void deleteBranches(DeleteBranchesInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectConfigEntryType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectConfigEntryType.java
new file mode 100644
index 0000000..bc4674f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectConfigEntryType.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.projects;
+
+public enum ProjectConfigEntryType {
+  STRING, INT, LONG, BOOLEAN, LIST, ARRAY
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
index 27bdf16..1cbf54c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
@@ -36,9 +36,4 @@
   public InheritableBoolean createNewChangeForAllNotInTarget;
   public String maxObjectSizeLimit;
   public Map<String, Map<String, ConfigValue>> pluginConfigValues;
-
-  public static class ConfigValue {
-    public String value;
-    public List<String> values;
-  }
-}
\ No newline at end of file
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
index 0e848b9..fdbadb2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -60,8 +60,8 @@
 
   ListRequest list();
 
-  public abstract class ListRequest {
-    public static enum FilterType {
+  abstract class ListRequest {
+    public enum FilterType {
       CODE, PARENT_CANDIDATES, PERMISSIONS, ALL
     }
 
@@ -175,7 +175,7 @@
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements Projects {
+  class NotImplemented implements Projects {
     @Override
     public ProjectApi name(String name) throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/PutDescriptionInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/PutDescriptionInput.java
deleted file mode 100644
index 7ea9fb6..0000000
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/PutDescriptionInput.java
+++ /dev/null
@@ -1,23 +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.extensions.api.projects;
-
-import com.google.gerrit.extensions.restapi.DefaultInput;
-
-public class PutDescriptionInput {
-  @DefaultInput
-  public String description;
-  public String commitMessage;
-}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
index 6cc1ba4..4348daf 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
@@ -18,13 +18,20 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 public interface TagApi {
+  TagApi create(TagInput input) throws RestApiException;
+
   TagInfo get() throws RestApiException;
 
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
-  public class NotImplemented implements TagApi {
+  class NotImplemented implements TagApi {
+    @Override
+    public TagApi create(TagInput input) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
     @Override
     public TagInfo get() throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInput.java
new file mode 100644
index 0000000..929d12e
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInput.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.api.projects;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+public class TagInput {
+  @DefaultInput
+  public String ref;
+  public String revision;
+  public String message;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ThemeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ThemeInfo.java
new file mode 100644
index 0000000..d5d520f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ThemeInfo.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 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.projects;
+
+public class ThemeInfo {
+  public static final ThemeInfo INHERIT = new ThemeInfo(null, null, null);
+
+  public final String css;
+  public final String header;
+  public final String footer;
+
+  public ThemeInfo(String css, String header, String footer) {
+    this.css = css;
+    this.header = header;
+    this.footer = footer;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthLoginProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthLoginProvider.java
new file mode 100644
index 0000000..3fa7bb2
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthLoginProvider.java
@@ -0,0 +1,42 @@
+// 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.extensions.auth.oauth;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.io.IOException;
+
+@ExtensionPoint
+public interface OAuthLoginProvider {
+
+  /**
+   * Performs a login with an OAuth2 provider for Git over HTTP
+   * communication.
+   *
+   * An implementation of this interface must transmit the given
+   * user name and secret, which can be either an OAuth2 access token
+   * or a password, to the OAuth2 backend for verification.
+   *
+   * @param username the user's identifier.
+   * @param secret the secret to verify, e.g. a previously received
+   * access token or a password.
+   *
+   * @return information about the logged in user, at least
+   * external id, user name and email address.
+   *
+   * @throws IOException if the login failed.
+   */
+  OAuthUserInfo login(String username, String secret) throws IOException;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java
index 901951e..788f420 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java
@@ -14,17 +14,40 @@
 
 package com.google.gerrit.extensions.auth.oauth;
 
+import java.io.Serializable;
+
 /* OAuth token */
-public class OAuthToken {
+public class OAuthToken implements Serializable {
+
+  private static final long serialVersionUID = 1L;
 
   private final String token;
   private final String secret;
   private final String raw;
 
+  /**
+   * Time of expiration of this token, or {@code Long#MAX_VALUE} if this
+   * token never expires, or time of expiration is unknown.
+   */
+  private final long expiresAt;
+
+  /**
+   * The identifier of the OAuth provider that issued this token
+   * in the form <tt>"plugin-name:provider-name"</tt>, or {@code null}.
+   */
+  private final String providerId;
+
   public OAuthToken(String token, String secret, String raw) {
+    this(token, secret, raw, Long.MAX_VALUE, null);
+  }
+
+  public OAuthToken(String token, String secret, String raw,
+      long expiresAt, String providerId) {
     this.token = token;
     this.secret = secret;
     this.raw = raw;
+    this.expiresAt = expiresAt;
+    this.providerId = providerId;
   }
 
   public String getToken() {
@@ -38,4 +61,16 @@
   public String getRaw() {
     return raw;
   }
+
+  public long getExpiresAt() {
+    return expiresAt;
+  }
+
+  public boolean isExpired() {
+    return System.currentTimeMillis() > expiresAt;
+  }
+
+  public String getProviderId() {
+    return providerId;
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthTokenEncrypter.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthTokenEncrypter.java
new file mode 100644
index 0000000..b2f4262
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthTokenEncrypter.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.auth.oauth;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+@ExtensionPoint
+public interface OAuthTokenEncrypter {
+
+  /**
+   * Encrypts the secret parts of the given OAuth access token.
+   *
+   * @param unencrypted a raw OAuth access token.
+   */
+  OAuthToken encrypt(OAuthToken unencrypted);
+
+  /**
+   * Decrypts the secret parts of the given OAuth access token.
+   *
+   * @param encrypted an encryppted OAuth access token.
+   */
+  OAuthToken decrypt(OAuthToken encrypted);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeKind.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeKind.java
new file mode 100644
index 0000000..e58e005
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeKind.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2013 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;
+
+/** Operation performed by a change relative to its parent. */
+public enum ChangeKind {
+  /** Nontrivial content changes. */
+  REWORK,
+
+  /** Conflict-free merge between the new parent and the prior patch set. */
+  TRIVIAL_REBASE,
+
+  /** Conflict-free change of first (left) parent of a merge commit. */
+  MERGE_FIRST_PARENT_UPDATE,
+
+  /** Same tree and same parent tree. */
+  NO_CODE_CHANGE,
+
+  /** Same tree, parent tree, same commit message. */
+  NO_CHANGE
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
index 56ebf9d..661d253 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
@@ -42,12 +42,12 @@
    * the uploader publishes the change, it becomes a NEW change.
    * Publishing is a one-way action, a change cannot return to DRAFT status.
    * Draft changes are only visible to the uploader and those explicitly
-   * added as reviewers.
+   * added as reviewers. Note that currently draft changes cannot be abandoned.
    *
    * <p>
    * Changes in the DRAFT state can be moved to:
    * <ul>
-   * <li>{@link #NEW} - when the change is published, it becomes a new change;
+   * <li>{@link #NEW} - when the change is published, it becomes a new change.
    * </ul>
    */
   DRAFT,
@@ -69,6 +69,12 @@
    * Once a change has been abandoned, it cannot be further modified by adding
    * a replacement patch set, and it cannot be merged. Draft comments however
    * may be published, permitting reviewers to send constructive feedback.
+   *
+   * <p>
+   * Changes in the ABANDONED state can be moved to:
+   * <ul>
+   * <li>{@link #NEW} - when the Restore action is used.
+   * </ul>
    */
   ABANDONED
-}
\ No newline at end of file
+}
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/DiffPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
index 18555cf..d246996 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
@@ -32,7 +32,7 @@
   public static final short[] CONTEXT_CHOICES =
       {3, 10, 25, 50, 75, 100, WHOLE_FILE_CONTEXT};
 
-  public static enum Whitespace {
+  public enum Whitespace {
     IGNORE_NONE,
     IGNORE_TRAILING,
     IGNORE_LEADING_AND_TRAILING,
@@ -61,6 +61,7 @@
   public Whitespace ignoreWhitespace;
   public Boolean retainHeader;
   public Boolean skipDeleted;
+  public Boolean skipUnchanged;
   public Boolean skipUncommented;
 
   public static DiffPreferencesInfo defaults() {
@@ -79,6 +80,7 @@
     i.showTabs = true;
     i.showWhitespaceErrors = true;
     i.skipDeleted = false;
+    i.skipUnchanged = false;
     i.skipUncommented = false;
     i.syntaxHighlighting = true;
     i.hideTopMenu = false;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
index fe11b32..84c61b7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
@@ -18,6 +18,7 @@
 public class EditPreferencesInfo {
   public Integer tabSize;
   public Integer lineLength;
+  public Integer indentUnit;
   public Integer cursorBlinkRate;
   public Boolean hideTopMenu;
   public Boolean showTabs;
@@ -27,6 +28,7 @@
   public Boolean matchBrackets;
   public Boolean lineWrapping;
   public Boolean autoCloseBrackets;
+  public Boolean showBase;
   public Theme theme;
   public KeyMapType keyMapType;
 
@@ -34,6 +36,7 @@
     EditPreferencesInfo i = new EditPreferencesInfo();
     i.tabSize = 8;
     i.lineLength = 100;
+    i.indentUnit = 2;
     i.cursorBlinkRate = 0;
     i.hideTopMenu = false;
     i.showTabs = true;
@@ -43,6 +46,7 @@
     i.matchBrackets = true;
     i.lineWrapping = false;
     i.autoCloseBrackets = false;
+    i.showBase = false;
     i.theme = Theme.DEFAULT;
     i.keyMapType = KeyMapType.DEFAULT;
     return i;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
new file mode 100644
index 0000000..9754f12
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -0,0 +1,185 @@
+// 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.extensions.client;
+
+import java.util.List;
+import java.util.Map;
+
+/** Preferences about a single user. */
+public class GeneralPreferencesInfo {
+
+  /** Default number of items to display per page. */
+  public static final int DEFAULT_PAGESIZE = 25;
+
+  /** Valid choices for the page size. */
+  public static final int[] PAGESIZE_CHOICES = {10, 25, 50, 100};
+
+  /** Preferred method to download a change. */
+  public enum DownloadCommand {
+    REPO_DOWNLOAD, PULL, CHECKOUT, CHERRY_PICK, FORMAT_PATCH
+  }
+
+  public enum DateFormat {
+    /** US style dates: Apr 27, Feb 14, 2010 */
+    STD("MMM d", "MMM d, yyyy"),
+
+    /** US style dates: 04/27, 02/14/10 */
+    US("MM/dd", "MM/dd/yy"),
+
+    /** ISO style dates: 2010-02-14 */
+    ISO("MM-dd", "yyyy-MM-dd"),
+
+    /** European style dates: 27. Apr, 27.04.2010 */
+    EURO("d. MMM", "dd.MM.yyyy"),
+
+    /** UK style dates: 27/04, 27/04/2010 */
+    UK("dd/MM", "dd/MM/yyyy");
+
+    private final String shortFormat;
+    private final String longFormat;
+
+    DateFormat(String shortFormat, String longFormat) {
+      this.shortFormat = shortFormat;
+      this.longFormat = longFormat;
+    }
+
+    public String getShortFormat() {
+      return shortFormat;
+    }
+
+    public String getLongFormat() {
+      return longFormat;
+    }
+  }
+
+  public enum ReviewCategoryStrategy {
+    NONE,
+    NAME,
+    EMAIL,
+    USERNAME,
+    ABBREV
+  }
+
+  public enum DiffView {
+    SIDE_BY_SIDE,
+    UNIFIED_DIFF
+  }
+
+  public enum EmailStrategy {
+    ENABLED,
+    CC_ON_OWN_COMMENTS,
+    DISABLED
+  }
+
+  public enum TimeFormat {
+    /** 12-hour clock: 1:15 am, 2:13 pm */
+    HHMM_12("h:mm a"),
+
+    /** 24-hour clock: 01:15, 14:13 */
+    HHMM_24("HH:mm");
+
+    private final String format;
+
+    TimeFormat(String format) {
+      this.format = format;
+    }
+
+    public String getFormat() {
+      return format;
+    }
+  }
+
+  /** Number of changes to show in a screen. */
+  public Integer changesPerPage;
+  /** Should the site header be displayed when logged in ? */
+  public Boolean showSiteHeader;
+  /** Should the Flash helper movie be used to copy text to the clipboard? */
+  public Boolean useFlashClipboard;
+  /** Type of download URL the user prefers to use. */
+  public String downloadScheme;
+  /** Type of download command the user prefers to use. */
+  public DownloadCommand downloadCommand;
+  public DateFormat dateFormat;
+  public TimeFormat timeFormat;
+  public Boolean relativeDateInChangeTable;
+  public DiffView diffView;
+  public Boolean sizeBarInChangeTable;
+  public Boolean legacycidInChangeTable;
+  public ReviewCategoryStrategy reviewCategoryStrategy;
+  public Boolean muteCommonPathPrefixes;
+  public Boolean signedOffBy;
+  public List<MenuItem> my;
+  public Map<String, String> urlAliases;
+  public EmailStrategy emailStrategy;
+
+  public boolean isShowInfoInReviewCategory() {
+    return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE;
+  }
+
+  public DateFormat getDateFormat() {
+    if (dateFormat == null) {
+      return DateFormat.STD;
+    }
+    return dateFormat;
+  }
+
+  public TimeFormat getTimeFormat() {
+    if (timeFormat == null) {
+      return TimeFormat.HHMM_12;
+    }
+    return timeFormat;
+  }
+
+  public ReviewCategoryStrategy getReviewCategoryStrategy() {
+    if (reviewCategoryStrategy == null) {
+      return ReviewCategoryStrategy.NONE;
+    }
+    return reviewCategoryStrategy;
+  }
+
+  public DiffView getDiffView() {
+    if (diffView == null) {
+      return DiffView.SIDE_BY_SIDE;
+    }
+    return diffView;
+  }
+
+  public EmailStrategy getEmailStrategy() {
+    if (emailStrategy == null) {
+      return EmailStrategy.ENABLED;
+    }
+    return emailStrategy;
+  }
+
+  public static GeneralPreferencesInfo defaults() {
+    GeneralPreferencesInfo p = new GeneralPreferencesInfo();
+    p.changesPerPage = DEFAULT_PAGESIZE;
+    p.showSiteHeader = true;
+    p.useFlashClipboard = true;
+    p.emailStrategy = EmailStrategy.ENABLED;
+    p.reviewCategoryStrategy = ReviewCategoryStrategy.NONE;
+    p.downloadScheme = null;
+    p.downloadCommand = DownloadCommand.CHECKOUT;
+    p.dateFormat = DateFormat.STD;
+    p.timeFormat = TimeFormat.HHMM_12;
+    p.relativeDateInChangeTable = false;
+    p.diffView = DiffView.SIDE_BY_SIDE;
+    p.sizeBarInChangeTable = true;
+    p.legacycidInChangeTable = false;
+    p.muteCommonPathPrefixes = true;
+    p.signedOffBy = false;
+    return p;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GerritTopMenu.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GerritTopMenu.java
index 81d6149..0a5b033 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GerritTopMenu.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GerritTopMenu.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.extensions.client;
 
 public enum GerritTopMenu {
-  ALL, MY, DIFFERENCES, PROJECTS, PEOPLE, PLUGINS, DOCUMENTATION;
+  ALL, MY, PROJECTS, PEOPLE, PLUGINS, DOCUMENTATION;
 
   public final String menuName;
 
-  private GerritTopMenu() {
+  GerritTopMenu() {
     menuName = name().substring(0, 1) + name().substring(1).toLowerCase();
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GitBasicAuthPolicy.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GitBasicAuthPolicy.java
new file mode 100644
index 0000000..6450b0d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GitBasicAuthPolicy.java
@@ -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.
+
+package com.google.gerrit.extensions.client;
+
+public enum GitBasicAuthPolicy {
+  HTTP,
+  LDAP,
+  HTTP_LDAP
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/InheritableBoolean.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/InheritableBoolean.java
index 57d4849..ce6464d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/InheritableBoolean.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/InheritableBoolean.java
@@ -18,4 +18,4 @@
   TRUE,
   FALSE,
   INHERIT
-}
\ No newline at end of file
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
index 261168d..9c85ac7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
@@ -17,5 +17,6 @@
 public enum KeyMapType {
   DEFAULT,
   EMACS,
+  SUBLIME,
   VIM
-}
\ No newline at end of file
+}
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
new file mode 100644
index 0000000..b5e9004
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListAccountsOption.java
@@ -0,0 +1,62 @@
+// 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;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+/** Output options available for retrieval of account details. */
+public enum ListAccountsOption {
+  /** Return detailed account properties. */
+  DETAILS(0),
+
+  /** Return all secondary emails. */
+  ALL_EMAILS(1);
+
+  private final int value;
+
+  ListAccountsOption(int v) {
+    this.value = v;
+  }
+
+  public int getValue() {
+    return value;
+  }
+
+  public static EnumSet<ListAccountsOption> fromBits(int v) {
+    EnumSet<ListAccountsOption> r = EnumSet.noneOf(ListAccountsOption.class);
+    for (ListAccountsOption o : ListAccountsOption.values()) {
+      if ((v & (1 << o.value)) != 0) {
+        r.add(o);
+        v &= ~(1 << o.value);
+      }
+      if (v == 0) {
+        return r;
+      }
+    }
+    if (v != 0) {
+      throw new IllegalArgumentException("unknown " + Integer.toHexString(v));
+    }
+    return r;
+  }
+
+  public static int toBits(Set<ListAccountsOption> set) {
+    int r = 0;
+    for (ListAccountsOption o : set) {
+      r |= 1 << o.value;
+    }
+    return r;
+  }
+}
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 4c336f7..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,11 +66,14 @@
   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;
 
-  private ListChangesOption(int v) {
+  ListChangesOption(int v) {
     this.value = v;
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListGroupsOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListGroupsOption.java
index d87f73e..e95570f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListGroupsOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListGroupsOption.java
@@ -26,7 +26,7 @@
 
   private final int value;
 
-  private ListGroupsOption(int v) {
+  ListGroupsOption(int v) {
     this.value = v;
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/MenuItem.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/MenuItem.java
new file mode 100644
index 0000000..25377a5
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/MenuItem.java
@@ -0,0 +1,42 @@
+// 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.extensions.client;
+
+public class MenuItem {
+  public final String url;
+  public final String name;
+  public final String target;
+  public final String id;
+
+  // Needed for GWT
+  public MenuItem() {
+    this(null, null, null, null);
+  }
+
+  public MenuItem(String name, String url) {
+    this(name, url, "_blank");
+  }
+
+  public MenuItem(String name, String url, String target) {
+    this(name, url, target, null);
+  }
+
+  public MenuItem(String name, String url, String target, String id) {
+    this.url = url;
+    this.name = name;
+    this.target = target;
+    this.id = id;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java
index 6f4190d..3114cb9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java
@@ -18,4 +18,4 @@
   ACTIVE,
   READ_ONLY,
   HIDDEN
-}
\ No newline at end of file
+}
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
new file mode 100644
index 0000000..556dddc
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
@@ -0,0 +1,76 @@
+// 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;
+
+import java.util.Objects;
+
+public class ProjectWatchInfo {
+  public String project;
+  public String filter;
+
+  public Boolean notifyNewChanges;
+  public Boolean notifyNewPatchSets;
+  public Boolean notifyAllComments;
+  public Boolean notifySubmittedChanges;
+  public Boolean notifyAbandonedChanges;
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof ProjectWatchInfo) {
+      ProjectWatchInfo w = (ProjectWatchInfo) obj;
+      return Objects.equals(project, w.project)
+          && Objects.equals(filter, w.filter)
+          && Objects.equals(notifyNewChanges, w.notifyNewChanges)
+          && Objects.equals(notifyNewPatchSets, w.notifyNewPatchSets)
+          && Objects.equals(notifyAllComments, w.notifyAllComments)
+          && Objects.equals(notifySubmittedChanges, w.notifySubmittedChanges)
+          && Objects.equals(notifyAbandonedChanges, w.notifyAbandonedChanges);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects
+        .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/ReviewerState.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ReviewerState.java
new file mode 100644
index 0000000..a58c959
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ReviewerState.java
@@ -0,0 +1,26 @@
+// 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.extensions.client;
+
+public enum ReviewerState {
+  /** The user has contributed at least one nonzero vote on the change. */
+  REVIEWER,
+
+  /** The reviewer was added to the change, but has not voted. */
+  CC,
+
+  /** The user was previously a reviewer on the change, but was removed. */
+  REMOVED;
+}
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/SubmitType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java
index 2b916f1..fcfeb01 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java
@@ -20,4 +20,4 @@
   REBASE_IF_NECESSARY,
   MERGE_ALWAYS,
   CHERRY_PICK
-}
\ No newline at end of file
+}
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 39730b7..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
@@ -17,23 +17,103 @@
 public enum Theme {
   // Light themes
   DEFAULT,
+  DAY_3024,
+  BASE16_LIGHT,
   ECLIPSE,
   ELEGANT,
+  MDN_LIKE,
   NEAT,
+  NEO,
+  PARAISO_LIGHT,
+  SOLARIZED,
+  TTCN,
+  XQ_LIGHT,
+  YETI,
 
   // Dark themes
+  NIGHT_3024,
+  ABCDEF,
+  AMBIANCE,
+  BASE16_DARK,
+  BESPIN,
+  BLACKBOARD,
+  COBALT,
+  COLORFORTH,
+  DRACULA,
+  ERLANG_DARK,
+  HOPSCOTCH,
+  ICECODER,
+  ISOTOPE,
+  LESSER_DARK,
+  LIQUIBYTE,
+  MATERIAL,
+  MBO,
   MIDNIGHT,
+  MONOKAI,
   NIGHT,
-  TWILIGHT;
+  PARAISO_DARK,
+  PASTEL_ON_DARK,
+  RAILSCASTS,
+  RUBYBLUE,
+  SETI,
+  THE_MATRIX,
+  TOMORROW_NIGHT_BRIGHT,
+  TOMORROW_NIGHT_EIGHTIES,
+  TWILIGHT,
+  VIBRANT_INK,
+  XQ_DARK,
+  ZENBURN;
 
   public boolean isDark() {
     switch (this) {
+      case NIGHT_3024:
+      case ABCDEF:
+      case AMBIANCE:
+      case BASE16_DARK:
+      case BESPIN:
+      case BLACKBOARD:
+      case COBALT:
+      case COLORFORTH:
+      case DRACULA:
+      case ERLANG_DARK:
+      case HOPSCOTCH:
+      case ICECODER:
+      case ISOTOPE:
+      case LESSER_DARK:
+      case LIQUIBYTE:
+      case MATERIAL:
+      case MBO:
       case MIDNIGHT:
+      case MONOKAI:
       case NIGHT:
+      case PARAISO_DARK:
+      case PASTEL_ON_DARK:
+      case RAILSCASTS:
+      case RUBYBLUE:
+      case SETI:
+      case THE_MATRIX:
+      case TOMORROW_NIGHT_BRIGHT:
+      case TOMORROW_NIGHT_EIGHTIES:
       case TWILIGHT:
+      case VIBRANT_INK:
+      case XQ_DARK:
+      case ZENBURN:
         return true;
+      case DEFAULT:
+      case DAY_3024:
+      case BASE16_LIGHT:
+      case ECLIPSE:
+      case ELEGANT:
+      case MDN_LIKE:
+      case NEAT:
+      case NEO:
+      case PARAISO_LIGHT:
+      case SOLARIZED:
+      case TTCN:
+      case XQ_LIGHT:
+      case YETI:
       default:
         return false;
     }
   }
-}
\ No newline at end of file
+}
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 39d98de..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,8 +20,10 @@
   public Integer _accountId;
   public String name;
   public String email;
+  public List<String> secondaryEmails;
   public String username;
   public List<AvatarInfo> avatars;
+  public Boolean _moreAccounts;
 
   public AccountInfo(Integer id) {
     this._accountId = id;
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..6ec5b1d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInfo.java
@@ -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.
+
+package com.google.gerrit.extensions.common;
+
+public class AgreementInfo {
+  public String name;
+  public String description;
+  public String url;
+}
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/ApprovalInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
index ce120cd..6d28dbc 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -17,6 +17,7 @@
 import java.sql.Timestamp;
 
 public class ApprovalInfo extends AccountInfo {
+  public String tag;
   public Integer value;
   public Timestamp date;
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/BlameInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/BlameInfo.java
new file mode 100644
index 0000000..df3f373
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/BlameInfo.java
@@ -0,0 +1,43 @@
+// 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.extensions.common;
+
+import java.util.List;
+
+public class BlameInfo {
+  public String author;
+  public String id;
+  public int time;
+  public String commitMsg;
+  public List<RangeInfo> ranges;
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    BlameInfo blameInfo = (BlameInfo) o;
+
+    return id.equals(blameInfo.id);
+  }
+
+  @Override
+  public int hashCode() {
+    return id.hashCode();
+  }
+}
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 cdfe0c6..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
@@ -15,6 +15,8 @@
 package com.google.gerrit.extensions.common;
 
 import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.client.SubmitType;
 
 import java.sql.Timestamp;
 import java.util.Collection;
@@ -32,14 +34,16 @@
   public ChangeStatus status;
   public Timestamp created;
   public Timestamp updated;
+  public Timestamp submitted;
   public Boolean starred;
+  public Collection<String> stars;
   public Boolean reviewed;
+  public SubmitType submitType;
   public Boolean mergeable;
   public Boolean submittable;
   public Integer insertions;
   public Integer deletions;
 
-  public String baseChange;
   public int _number;
 
   public AccountInfo owner;
@@ -48,6 +52,8 @@
   public Map<String, LabelInfo> labels;
   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
new file mode 100644
index 0000000..88c3ea8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.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.common;
+
+import com.google.gerrit.extensions.client.ChangeStatus;
+
+public class ChangeInput {
+  public String project;
+  public String branch;
+  public String subject;
+
+  public String topic;
+  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/ChangeMessageInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
index 7ad40b5..e79918f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
@@ -18,6 +18,7 @@
 
 public class ChangeMessageInfo {
   public String id;
+  public String tag;
   public AccountInfo author;
   public Timestamp date;
   public String message;
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 b1f8183..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,6 +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/DiffInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
index 58b2d39..3df4b86 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
@@ -35,7 +35,7 @@
   // Binary file
   public Boolean binary;
 
-  public static enum IntraLineStatus {
+  public enum IntraLineStatus {
     OK,
     TIMEOUT,
     FAILURE
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java
index 00d0c18..a812908 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java
@@ -21,4 +21,5 @@
   public Integer linesInserted;
   public Integer linesDeleted;
   public long sizeDelta;
+  public long size;
 }
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/ProblemInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
index d04b346..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,8 +14,10 @@
 
 package com.google.gerrit.extensions.common;
 
+import java.util.Objects;
+
 public class ProblemInfo {
-  public static enum Status {
+  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/RangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RangeInfo.java
new file mode 100644
index 0000000..5268825
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RangeInfo.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 RangeInfo {
+  public int start;
+  public int end;
+
+  public RangeInfo(int start, int end) {
+    this.start = start;
+    this.end = end;
+  }
+}
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/RevisionInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
index 025c623..34a1e63 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.extensions.common;
 
+import com.google.gerrit.extensions.client.ChangeKind;
+
 import java.sql.Timestamp;
 import java.util.Map;
 
 public class RevisionInfo {
   public transient boolean isCurrent;
   public Boolean draft;
+  public ChangeKind kind;
   public int _number;
   public Timestamp created;
   public AccountInfo uploader;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshKeyInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshKeyInfo.java
new file mode 100644
index 0000000..a20bcbf
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshKeyInfo.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;
+
+public class SshKeyInfo {
+  public Integer seq;
+  public String sshPublicKey;
+  public String encodedKey;
+  public String algorithm;
+  public String comment;
+  public Boolean valid;
+}
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/TestSubmitRuleInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TestSubmitRuleInput.java
new file mode 100644
index 0000000..96a1626
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TestSubmitRuleInput.java
@@ -0,0 +1,27 @@
+// 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.extensions.common;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+public class TestSubmitRuleInput {
+  public enum Filters {
+    RUN, SKIP
+  }
+
+  @DefaultInput
+  public String rule;
+  public Filters filters;
+}
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/AccountIndexedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AccountIndexedListener.java
new file mode 100644
index 0000000..3263e70
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AccountIndexedListener.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Notified whenever an account is indexed
+ */
+@ExtensionPoint
+public interface AccountIndexedListener {
+/**
+ * Invoked when an account is indexed
+ * @param id of the account
+ */
+  void onAccountIndexed(int id);
+}
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
new file mode 100644
index 0000000..5abfc38
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AgreementSignupListener.java
@@ -0,0 +1,29 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+/** Notified whenever a user signed up for a Contributor License Agreement. */
+@ExtensionPoint
+public interface AgreementSignupListener {
+  interface Event extends GerritEvent {
+    AccountInfo getAccount();
+    String getAgreementName();
+  }
+
+  void onAgreementSignup(Event e);
+}
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
new file mode 100644
index 0000000..40b84a3
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java
@@ -0,0 +1,30 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+/** Notified whenever a Change is abandoned. */
+@ExtensionPoint
+public interface ChangeAbandonedListener {
+  interface Event extends RevisionEvent {
+    @Deprecated
+    AccountInfo getAbandoner();
+    String getReason();
+  }
+
+  void onChangeAbandoned(Event event);
+}
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
new file mode 100644
index 0000000..f012710
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeEvent.java
@@ -0,0 +1,27 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+
+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/ChangeIndexedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeIndexedListener.java
new file mode 100644
index 0000000..fd8dac8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeIndexedListener.java
@@ -0,0 +1,27 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/** Notified whenever a change is indexed or deleted from the index. */
+@ExtensionPoint
+public interface ChangeIndexedListener {
+  /** Invoked when a change is indexed. */
+  void onChangeIndexed(int id);
+
+  /** Invoked when a change is deleted from the index. */
+  void onChangeDeleted(int id);
+}
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
new file mode 100644
index 0000000..d0ca6d6
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java
@@ -0,0 +1,34 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+/** Notified whenever a Change is merged. */
+@ExtensionPoint
+public interface ChangeMergedListener {
+  interface Event extends RevisionEvent {
+    @Deprecated
+    AccountInfo getMerger();
+    /**
+     * Represents the merged Revision when the submit strategy is cherry-pick or
+     * rebase-if-necessary.
+     */
+    String getNewRevisionId();
+  }
+
+  void onChangeMerged(Event event);
+}
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
new file mode 100644
index 0000000..e5f3330
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java
@@ -0,0 +1,30 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+/** Notified whenever a Change is restored. */
+@ExtensionPoint
+public interface ChangeRestoredListener {
+  interface Event extends RevisionEvent {
+    @Deprecated
+    AccountInfo getRestorer();
+    String getReason();
+  }
+
+  void onChangeRestored(Event event);
+}
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
new file mode 100644
index 0000000..6c82034
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java
@@ -0,0 +1,35 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+
+import java.util.Map;
+
+/** Notified whenever a comment is added to a change. */
+@ExtensionPoint
+public interface CommentAddedListener {
+  interface Event extends RevisionEvent {
+    @Deprecated
+    AccountInfo getAuthor();
+    String getComment();
+    Map<String, ApprovalInfo> getApprovals();
+    Map<String, ApprovalInfo> getOldApprovals();
+  }
+
+  void onCommentAdded(Event event);
+}
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
new file mode 100644
index 0000000..3857468
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java
@@ -0,0 +1,29 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+/** Notified whenever a Draft is published. */
+@ExtensionPoint
+public interface DraftPublishedListener {
+  interface Event extends RevisionEvent {
+    @Deprecated
+    AccountInfo getPublisher();
+  }
+
+  void onDraftPublished(Event event);
+}
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 89d836d..f15dd4d 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,14 +23,11 @@
  */
 @ExtensionPoint
 public interface GarbageCollectorListener {
-  public 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.
-     * @see <a href="http://download.eclipse.org/jgit/site/3.7.0.201502260915-r/apidocs/org/eclipse/jgit/api/GarbageCollectCommand.html#call%28%29">GarbageCollectCommand</a>
+     * @see org.eclipse.jgit.api.GarbageCollectCommand#call()
      */
     Properties getStatistics();
   }
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 a838baf..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
@@ -14,20 +14,24 @@
 
 package com.google.gerrit.extensions.events;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountInfo;
 
 /** Notified when one or more references are modified. */
 @ExtensionPoint
 public interface GitReferenceUpdatedListener {
-
-  public interface Event {
-    String getProjectName();
+  interface Event extends ProjectEvent {
     String getRefName();
     String getOldObjectId();
     String getNewObjectId();
     boolean isCreate();
     boolean isDelete();
     boolean isNonFastForward();
+    /**
+     * The updater, could be null if it's the server.
+     */
+    @Nullable AccountInfo getUpdater();
   }
 
   void onGitReferenceUpdated(Event event);
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
new file mode 100644
index 0000000..c49b0f3
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java
@@ -0,0 +1,34 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+import java.util.Collection;
+
+/** Notified whenever a Change's Hashtags are edited. */
+@ExtensionPoint
+public interface HashtagsEditedListener {
+  interface Event extends ChangeEvent {
+    @Deprecated
+    AccountInfo getEditor();
+    Collection<String> getHashtags();
+    Collection<String> getAddedHashtags();
+    Collection<String> getRemovedHashtags();
+  }
+
+  void onHashtagsEdited(Event event);
+}
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 5961d6f..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 {
-  public 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/LifecycleListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
index 93da347..b3ed37b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
@@ -21,9 +21,9 @@
 /** Listener interested in server startup and shutdown events. */
 @ExtensionPoint
 public interface LifecycleListener extends EventListener {
-  /** Invoke when the server is starting. */
-  public void start();
+  /** Invoked when the server is starting. */
+  void start();
 
   /** Invoked when the server is stopping. */
-  public void stop();
+  void stop();
 }
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 7eed7d4..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 {
-  public 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
new file mode 100644
index 0000000..dfcbdee
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/PluginEventListener.java
@@ -0,0 +1,26 @@
+// 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.extensions.events;
+
+/** Notified when a plugin fires an event. */
+public interface PluginEventListener {
+  interface Event extends GerritEvent {
+    String pluginName();
+    String getType();
+    String getData();
+  }
+
+  void onPluginEvent(Event e);
+}
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 b03f99c..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 {
-  public 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
new file mode 100644
index 0000000..3cc3fdc
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java
@@ -0,0 +1,28 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+/** Notified whenever a Reviewer is added to a change. */
+@ExtensionPoint
+public interface ReviewerAddedListener {
+  interface Event extends ChangeEvent {
+    AccountInfo getReviewer();
+  }
+
+  void onReviewerAdded(Event event);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerDeletedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerDeletedListener.java
new file mode 100644
index 0000000..3c2f723
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerDeletedListener.java
@@ -0,0 +1,34 @@
+// 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.AccountInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+
+import java.util.Map;
+
+/** Notified whenever a Reviewer is removed from a change. */
+@ExtensionPoint
+public interface ReviewerDeletedListener {
+  interface Event extends ChangeEvent {
+    AccountInfo getReviewer();
+    String getComment();
+    Map<String, ApprovalInfo> getNewApprovals();
+    Map<String, ApprovalInfo> getOldApprovals();
+  }
+
+  void onReviewerDeleted(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
new file mode 100644
index 0000000..5e4e095
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java
@@ -0,0 +1,29 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+/** Notified whenever a Change Revision is created. */
+@ExtensionPoint
+public interface RevisionCreatedListener {
+  interface Event extends RevisionEvent {
+    @Deprecated
+    AccountInfo getUploader();
+  }
+
+  void onRevisionCreated(Event event);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionEvent.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionEvent.java
new file mode 100644
index 0000000..27d1067
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionEvent.java
@@ -0,0 +1,23 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.common.RevisionInfo;
+
+/** Interface to be extended by Events with a Revision. */
+public interface RevisionEvent extends ChangeEvent {
+  RevisionInfo getRevision();
+}
+
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
new file mode 100644
index 0000000..68ba22c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java
@@ -0,0 +1,30 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+/** Notified whenever a Change Topic is changed. */
+@ExtensionPoint
+public interface TopicEditedListener {
+  interface Event extends ChangeEvent {
+    @Deprecated
+    AccountInfo getEditor();
+    String getOldTopic();
+  }
+
+  void onTopicEdited(Event event);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/UsageDataPublishedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/UsageDataPublishedListener.java
index 365d056..35d49b1 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/UsageDataPublishedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/UsageDataPublishedListener.java
@@ -23,22 +23,22 @@
 @ExtensionPoint
 public interface UsageDataPublishedListener {
 
-  public interface Event {
+  interface Event {
     MetaData getMetaData();
     Timestamp getInstant();
     List<Data> getData();
   }
 
-  public interface Data {
+  interface Data {
     long getValue();
     String getProjectName();
   }
 
-  public interface MetaData {
-    public String getName();
-    public String getUnitName();
-    public String getUnitSymbol();
-    public String getDescription();
+  interface MetaData {
+    String getName();
+    String getUnitName();
+    String getUnitSymbol();
+    String getDescription();
   }
 
   void onUsageDataPublished(Event event);
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/registration/DynamicItem.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
index da7db17..52be977 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
@@ -74,15 +74,33 @@
    * @param member type of entry to store.
    */
   public static <T> void itemOf(Binder binder, TypeLiteral<T> member) {
-    @SuppressWarnings("unchecked")
-    Key<DynamicItem<T>> key = (Key<DynamicItem<T>>) Key.get(
-        Types.newParameterizedType(DynamicItem.class, member.getType()));
+    Key<DynamicItem<T>> key = keyFor(member);
     binder.bind(key)
       .toProvider(new DynamicItemProvider<>(member, key))
       .in(Scopes.SINGLETON);
   }
 
   /**
+   * Construct a single {@code DynamicItem<T>} with a fixed value.
+   * <p>
+   * Primarily useful for passing {@code DynamicItem}s to constructors in tests.
+   *
+   * @param member type of item.
+   * @param item item to store.
+   */
+  public static <T> DynamicItem<T> itemOf(Class<T> member, T item) {
+    return new DynamicItem<>(
+        keyFor(TypeLiteral.get(member)),
+        Providers.of(item), "gerrit");
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T> Key<DynamicItem<T>> keyFor(TypeLiteral<T> member) {
+    return (Key<DynamicItem<T>>) Key.get(
+        Types.newParameterizedType(DynamicItem.class, member.getType()));
+  }
+
+  /**
    * Bind one implementation as the item using a unique annotation.
    *
    * @param binder a new binder created in the module.
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
index 28052ef..6eb11bc 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -53,6 +53,7 @@
    * @param member type of entry in the set.
    */
   public static <T> void setOf(Binder binder, Class<T> member) {
+    binder.disableCircularProxies();
     setOf(binder, TypeLiteral.get(member));
   }
 
@@ -71,6 +72,7 @@
     @SuppressWarnings("unchecked")
     Key<DynamicSet<T>> key = (Key<DynamicSet<T>>) Key.get(
         Types.newParameterizedType(DynamicSet.class, member.getType()));
+    binder.disableCircularProxies();
     binder.bind(key)
       .toProvider(new DynamicSetProvider<>(member))
       .in(Scopes.SINGLETON);
@@ -84,6 +86,7 @@
    * @return a binder to continue configuring the new set member.
    */
   public static <T> LinkedBindingBuilder<T> bind(Binder binder, Class<T> type) {
+    binder.disableCircularProxies();
     return bind(binder, TypeLiteral.get(type));
   }
 
@@ -95,6 +98,7 @@
    * @return a binder to continue configuring the new set member.
    */
   public static <T> LinkedBindingBuilder<T> bind(Binder binder, TypeLiteral<T> type) {
+    binder.disableCircularProxies();
     return binder.bind(type).annotatedWith(UniqueAnnotations.create());
   }
 
@@ -110,6 +114,7 @@
   public static <T> LinkedBindingBuilder<T> bind(Binder binder,
       Class<T> type,
       Named name) {
+    binder.disableCircularProxies();
     return bind(binder, TypeLiteral.get(type));
   }
 
@@ -125,6 +130,7 @@
   public static <T> LinkedBindingBuilder<T> bind(Binder binder,
       TypeLiteral<T> type,
       Named name) {
+    binder.disableCircularProxies();
     return binder.bind(type).annotatedWith(name);
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
index 2243786..4688f7c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
@@ -17,5 +17,5 @@
 /** Handle for registered information. */
 public interface RegistrationHandle {
   /** Delete this registration. */
-  public void remove();
+  void remove();
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
index 7284296..ef38303 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
@@ -18,7 +18,7 @@
 import com.google.inject.Provider;
 
 public interface ReloadableRegistrationHandle<T> extends RegistrationHandle {
-  public Key<T> getKey();
+  Key<T> getKey();
 
-  public RegistrationHandle replace(Key<T> key, Provider<T> item);
+  RegistrationHandle replace(Key<T> key, Provider<T> item);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index a21c2d4..068d9a0 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -62,7 +62,7 @@
   private Charset characterEncoding;
   private long contentLength = -1;
   private boolean gzip = true;
-  private boolean base64 = false;
+  private boolean base64;
   private String attachmentName;
 
   /** @return the MIME type of the result, for HTTP clients. */
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ETagView.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ETagView.java
index f95161d..3b32829 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ETagView.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ETagView.java
@@ -18,5 +18,5 @@
  * A view which may change, although the underlying resource did not change
  */
 public interface ETagView<R extends RestResource> extends RestReadView<R> {
-  public String getETag(R rsrc);
+  String getETag(R rsrc);
 }
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 4345076..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
@@ -36,6 +36,11 @@
     return new Impl<>(201, value);
   }
 
+  /** HTTP 202 Accepted: accepted as background task. */
+  public static Accepted accepted(String location) {
+    return new Accepted(location);
+  }
+
   /** HTTP 204 No Content: typically used when the resource is deleted. */
   @SuppressWarnings("unchecked")
   public static <T> Response<T> none() {
@@ -47,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) {
@@ -168,4 +178,33 @@
       return String.format("[302 Redirect] %s", location);
     }
   }
+
+  /** Accepted as task for asynchronous execution. */
+  public static final class Accepted {
+    private final String location;
+
+    private Accepted(String url) {
+      this.location = url;
+    }
+
+    public String location() {
+      return location;
+    }
+
+    @Override
+    public int hashCode() {
+      return location.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return o instanceof Accepted
+        && ((Accepted) o).location.equals(location);
+    }
+
+    @Override
+    public String toString() {
+      return String.format("[202 Accepted] %s", location);
+    }
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
index 8032531..29c824f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
@@ -30,11 +30,11 @@
      * @return time for the Last-Modified header. HTTP truncates the header
      *         value to seconds.
      */
-    public Timestamp getLastModified();
+    Timestamp getLastModified();
   }
 
   /** A resource with an ETag. */
   public interface HasETag {
-    public String getETag();
+    String getETag();
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
index c7deedb..45aec57 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
@@ -17,7 +17,7 @@
 /** Exports current server information to an extension. */
 public interface ServerInformation {
   /** Current state of the server. */
-  public enum State {
+  enum State {
     /**
      * The server is starting up, and network connections are not yet being
      * accepted. Plugins or extensions starting during this time are starting
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ParentWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ParentWebLink.java
new file mode 100644
index 0000000..648dff8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ParentWebLink.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.webui;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+
+@ExtensionPoint
+public interface ParentWebLink extends WebLink {
+
+  /**
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo}
+   * describing a link from a parent revision to an external service.
+   *
+   * <p>In order for the web link to be visible
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo#url}
+   * and {@link com.google.gerrit.extensions.common.WebLinkInfo#name}
+   * must be set.<p>
+   *
+   * @param projectName Name of the project
+   * @param commit Commit sha1 of the parent revision
+   * @return WebLinkInfo that links to parent commit in external service,
+   * null if there should be no link.
+   */
+  WebLinkInfo getParentWebLink(String projectName, String commit);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
index ad74849..b9004f2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.extensions.common.WebLinkInfo;
 
 @ExtensionPoint
-public interface PatchSetWebLink extends WebLink{
+public interface PatchSetWebLink extends WebLink {
 
   /**
    * {@link com.google.gerrit.extensions.common.WebLinkInfo}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java
index ead7c31..7ad12cd 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java
@@ -16,12 +16,13 @@
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.extensions.client.GerritTopMenu;
+import com.google.gerrit.extensions.client.MenuItem;
 
 import java.util.List;
 
 @ExtensionPoint
 public interface TopMenu {
-  public class MenuEntry {
+  class MenuEntry {
     public final String name;
     public final List<MenuItem> items;
 
@@ -35,27 +36,5 @@
     }
   }
 
-  public class MenuItem {
-    public final String url;
-    public final String name;
-    public final String target;
-    public final String id;
-
-    public MenuItem(String name, String url) {
-      this(name, url, "_blank");
-    }
-
-    public MenuItem(String name, String url, String target) {
-      this(name, url, target, null);
-    }
-
-    public MenuItem(String name, String url, String target, String id) {
-      this.url = url;
-      this.name = name;
-      this.target = target;
-      this.id = id;
-    }
-  }
-
   List<MenuEntry> getEntries();
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
index 63172ce..1807673 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
@@ -29,10 +29,10 @@
    *         assumed unavailable and not presented. This is usually the same as
    *         {@code setVisible(false)}.
    */
-  public Description getDescription(R resource);
+  Description getDescription(R resource);
 
   /** Describes an action invokable through the web interface. */
-  public static class Description {
+  class Description {
     private String method;
     private String id;
     private String label;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
index e497f7d..fd677ca 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
@@ -24,7 +24,7 @@
   /**
    * Class that holds target defaults for WebLink anchors.
    */
-  public static class Target {
+  class Target {
     /**
      * Opens the link in a new window or tab
      */
diff --git a/gerrit-gpg/BUCK b/gerrit-gpg/BUCK
index 2b258a9..73d9f04 100644
--- a/gerrit-gpg/BUCK
+++ b/gerrit-gpg/BUCK
@@ -8,7 +8,7 @@
   '//lib/guice:guice',
   '//lib/guice:guice-assistedinject',
   '//lib/guice:guice-servlet',
-  '//lib/jgit:jgit',
+  '//lib/jgit/org.eclipse.jgit:jgit',
   '//lib/log:api',
 ]
 
@@ -45,12 +45,12 @@
     ':gpg',
     ':testutil',
     '//gerrit-cache-h2:cache-h2',
-    '//gerrit-lucene:lucene',  
+    '//gerrit-lucene:lucene',
     '//gerrit-server:testutil',
     '//lib:truth',
     '//lib/bouncycastle:bcpg',
     '//lib/bouncycastle:bcprov',
-    '//lib/jgit:junit',
+    '//lib/jgit/org.eclipse.jgit.junit:junit',
   ],
   source_under_test = [':gpg'],
   visibility = ['//tools/eclipse:classpath'],
diff --git a/gerrit-gpg/BUILD b/gerrit-gpg/BUILD
new file mode 100644
index 0000000..79f50b1
--- /dev/null
+++ b/gerrit-gpg/BUILD
@@ -0,0 +1,58 @@
+load('//tools/bzl:junit.bzl', 'junit_tests')
+
+DEPS = [
+  '//gerrit-common:server',
+  '//gerrit-extension-api:api',
+  '//gerrit-reviewdb:server',
+  '//gerrit-server:server',
+  '//lib:guava',
+  '//lib:gwtorm',
+  '//lib/guice:guice',
+  '//lib/guice:guice-assistedinject',
+  '//lib/guice:guice-servlet',
+  '//lib/jgit/org.eclipse.jgit:jgit',
+  '//lib/log:api',
+]
+
+java_library(
+  name = 'gpg',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = DEPS + [
+    '//lib/bouncycastle:bcpg',
+    '//lib/bouncycastle:bcprov',
+  ],
+  visibility = ['//visibility:public'],
+)
+
+TESTUTIL_SRCS = glob(['src/test/**/testutil/**/*.java'])
+
+java_library(
+  name = 'testutil',
+  srcs = TESTUTIL_SRCS,
+  deps = DEPS + [
+    ':gpg',
+    '//lib/bouncycastle:bcpg-without-neverlink',
+    '//lib/bouncycastle:bcprov-without-neverlink',
+  ],
+  visibility = ['//visibility:public'],
+)
+
+junit_tests(
+  name = 'gpg_tests',
+  srcs = glob(
+    ['src/test/java/**/*.java'],
+    exclude = TESTUTIL_SRCS,
+  ),
+  deps = DEPS + [
+    ':gpg',
+    ':testutil',
+    '//gerrit-cache-h2:cache-h2',
+    '//gerrit-lucene:lucene',
+    '//gerrit-server:testutil',
+    '//lib:truth',
+    '//lib/jgit/org.eclipse.jgit.junit:junit',
+    '//lib/bouncycastle:bcpg-without-neverlink',
+    '//lib/bouncycastle:bcprov-without-neverlink',
+  ],
+  visibility = ['//visibility:public'],
+)
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 c3c886f..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);
@@ -83,7 +93,7 @@
         Map<Long, Fingerprint> fps =
             Maps.newHashMapWithExpectedSize(strs.length);
         for (String str : strs) {
-          str = CharMatcher.WHITESPACE.removeFrom(str).toUpperCase();
+          str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
           Fingerprint fp = new Fingerprint(BaseEncoding.base16().decode(str));
           fps.put(fp.getId(), fp);
         }
@@ -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) {
@@ -139,9 +153,8 @@
     try {
       if (depth == 0 && expectedUser != null) {
         return checkIdsForExpectedUser(key);
-      } else {
-        return checkIdsForArbitraryUser(key);
       }
+      return checkIdsForArbitraryUser(key);
     } catch (PGPException | OrmException e) {
       String msg = "Error checking user IDs for key";
       log.warn(msg + " " + keyIdToString(key.getKeyID()), e);
@@ -164,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(db, 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/PublicKeyStore.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
index 3d939a1..b45bce5 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -340,6 +340,13 @@
         toAdd.clear();
         toRemove.clear();
         break;
+      case FORCED:
+      case IO_FAILURE:
+      case LOCK_FAILURE:
+      case NOT_ATTEMPTED:
+      case REJECTED:
+      case REJECTED_CURRENT_BRANCH:
+      case RENAMED:
       default:
         break;
     }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
index cdc3c62..59157bd 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
@@ -88,10 +88,9 @@
       // key is not ultimately trusted. Assume anyone with Submit permission to
       // the branch is able to verify during review that the code is legitimate.
       return result.isOk();
-    } else {
-      // Directly updating one or more refs: require a trusted key.
-      return result.isTrusted();
     }
+    // Directly updating one or more refs: require a trusted key.
+    return result.isTrusted();
   }
 
   private static boolean onlyMagicBranches(Iterable<ReceiveCommand> commands) {
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 baac714..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());
@@ -80,6 +85,14 @@
         case NO_CHANGE:
         case FAST_FORWARD:
           break;
+        case FORCED:
+        case IO_FAILURE:
+        case LOCK_FAILURE:
+        case NEW:
+        case NOT_ATTEMPTED:
+        case REJECTED:
+        case REJECTED_CURRENT_BRANCH:
+        case RENAMED:
         default:
           throw new ResourceConflictException(
               "Failed to delete public key: " + saveResult);
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
index a136007..49657c6 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -40,6 +40,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.CurrentUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -70,16 +71,19 @@
 
   private final DynamicMap<RestView<GpgKey>> views;
   private final Provider<ReviewDb> db;
+  private final Provider<CurrentUser> self;
   private final Provider<PublicKeyStore> storeProvider;
   private final GerritPublicKeyChecker.Factory checkerFactory;
 
   @Inject
   GpgKeys(DynamicMap<RestView<GpgKey>> views,
       Provider<ReviewDb> db,
+      Provider<CurrentUser> self,
       Provider<PublicKeyStore> storeProvider,
       GerritPublicKeyChecker.Factory checkerFactory) {
     this.views = views;
     this.db = db;
+    this.self = self;
     this.storeProvider = storeProvider;
     this.checkerFactory = checkerFactory;
   }
@@ -87,7 +91,6 @@
   @Override
   public ListGpgKeys list()
       throws ResourceNotFoundException, AuthException {
-    checkEnabled();
     return new ListGpgKeys();
   }
 
@@ -95,8 +98,8 @@
   public GpgKey parse(AccountResource parent, IdString id)
       throws ResourceNotFoundException, PGPException, OrmException,
       IOException {
-    checkEnabled();
-    String str = CharMatcher.WHITESPACE.removeFrom(id.get()).toUpperCase();
+    checkVisible(self, parent);
+    String str = CharMatcher.whitespace().removeFrom(id.get()).toUpperCase();
     if ((str.length() != 8 && str.length() != 40)
         || !CharMatcher.anyOf("0123456789ABCDEF").matchesAllOf(str)) {
       throw new ResourceNotFoundException(id);
@@ -119,7 +122,7 @@
   static byte[] parseFingerprint(String str,
       Iterable<AccountExternalId> existingExtIds)
       throws ResourceNotFoundException {
-    str = CharMatcher.WHITESPACE.removeFrom(str).toUpperCase();
+    str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
     if ((str.length() != 8 && str.length() != 40)
         || !CharMatcher.anyOf("0123456789ABCDEF").matchesAllOf(str)) {
       throw new ResourceNotFoundException(str);
@@ -151,7 +154,9 @@
   public class ListGpgKeys implements RestReadView<AccountResource> {
     @Override
     public Map<String, GpgKeyInfo> apply(AccountResource rsrc)
-        throws OrmException, PGPException, IOException {
+        throws OrmException, PGPException, IOException,
+        ResourceNotFoundException {
+      checkVisible(self, rsrc);
       Map<String, GpgKeyInfo> keys = new HashMap<>();
       try (PublicKeyStore store = storeProvider.get()) {
         for (AccountExternalId extId : getGpgExtIds(rsrc)) {
@@ -225,10 +230,14 @@
     return NB.decodeInt64(fp, fp.length - 8);
   }
 
-  static void checkEnabled() throws ResourceNotFoundException {
+  static void checkVisible(Provider<CurrentUser> self, AccountResource rsrc)
+      throws ResourceNotFoundException {
     if (!BouncyCastleUtil.havePGP()) {
       throw new ResourceNotFoundException("GPG not enabled");
     }
+    if (self.get() != rsrc.getUser()) {
+      throw new ResourceNotFoundException();
+    }
   }
 
   public static GpgKeyInfo toJson(PGPPublicKey key, CheckResult checkResult)
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 91c4494..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,12 +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;
@@ -80,28 +86,40 @@
   private final Logger log = LoggerFactory.getLogger(getClass());
   private final Provider<PersonIdent> serverIdent;
   private final Provider<ReviewDb> db;
+  private final Provider<CurrentUser> self;
   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,
       Provider<ReviewDb> db,
+      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
   public Map<String, GpgKeyInfo> apply(AccountResource rsrc, Input input)
       throws ResourceNotFoundException, BadRequestException,
       ResourceConflictException, PGPException, OrmException, IOException {
-    GpgKeys.checkEnabled();
+    GpgKeys.checkVisible(self, rsrc);
 
     List<AccountExternalId> existingExtIds =
         GpgKeys.getGpgExtIds(db.get(), rsrc.getUser().getAccountId()).toList();
@@ -114,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));
+          }
         }
       }
 
@@ -137,6 +168,7 @@
               return toExtIdKey(fp.get());
             }
           }));
+      accountCache.evict(rsrc.getUser().getAccountId());
       return toJson(newKeys, toRemove, store, rsrc.getUser());
     }
   }
@@ -228,6 +260,12 @@
           break;
         case NO_CHANGE:
           break;
+        case IO_FAILURE:
+        case LOCK_FAILURE:
+        case NOT_ATTEMPTED:
+        case REJECTED:
+        case REJECTED_CURRENT_BRANCH:
+        case RENAMED:
         default:
           // TODO(dborowitz): Backoff and retry on LOCK_FAILURE.
           throw new ResourceConflictException(
@@ -236,12 +274,34 @@
     }
   }
 
-  private final AccountExternalId.Key toExtIdKey(byte[] fp) {
+  private AccountExternalId.Key toExtIdKey(byte[] fp) {
     return new AccountExternalId.Key(
         AccountExternalId.SCHEME_GPGKEY,
         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/GerritPublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index 4df9d37..e39c8ae 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -46,6 +46,7 @@
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryModule;
+import com.google.gerrit.testutil.TestNotesMigration;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -65,6 +66,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -107,7 +109,8 @@
     cfg.setStringList("receive", null, "trustedKey", ImmutableList.of(
         Fingerprint.toString(keyB().getPublicKey().getFingerprint()),
         Fingerprint.toString(keyD().getPublicKey().getFingerprint())));
-    Injector injector = Guice.createInjector(new InMemoryModule(cfg));
+    Injector injector = Guice.createInjector(
+        new InMemoryModule(cfg, new TestNotesMigration()));
 
     lifecycle = new LifecycleManager();
     lifecycle.add(injector);
@@ -149,12 +152,12 @@
   private IdentifiedUser addUser(String name) throws Exception {
     AuthRequest req = AuthRequest.forUser(name);
     Account.Id id = accountManager.authenticate(req).getAccountId();
-    return userFactory.create(Providers.of(db), id);
+    return userFactory.create(id);
   }
 
-  private IdentifiedUser reloadUser() {
+  private IdentifiedUser reloadUser() throws IOException {
     accountCache.evict(userId);
-    user = userFactory.create(Providers.of(db), userId);
+    user = userFactory.create(userId);
     return user;
   }
 
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
index 99e96a2..bd71bc5 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
@@ -339,6 +339,13 @@
       case FAST_FORWARD:
       case FORCED:
         break;
+      case IO_FAILURE:
+      case LOCK_FAILURE:
+      case NOT_ATTEMPTED:
+      case NO_CHANGE:
+      case REJECTED:
+      case REJECTED_CURRENT_BRANCH:
+      case RENAMED:
       default:
         throw new AssertionError(result);
     }
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-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java b/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java
deleted file mode 100644
index 8072d75..0000000
--- a/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java
+++ /dev/null
@@ -1,541 +0,0 @@
-/*
- * Copyright 2011 Google Inc.
- *
- * 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.gwt.dev.codeserver;
-
-import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.core.ext.TreeLogger.Type;
-import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.dev.codeserver.CompileDir.PolicyFile;
-import com.google.gwt.dev.codeserver.Pages.ErrorPage;
-import com.google.gwt.dev.json.JsonObject;
-
-import org.eclipse.jetty.http.MimeTypes;
-import org.eclipse.jetty.server.HttpConnection;
-import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
-import org.eclipse.jetty.servlets.GzipFilter;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Date;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import javax.servlet.DispatcherType;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * The web server for Super Dev Mode, also known as the code server. The URLs handled include:
- * <ul>
- *   <li>HTML pages for the front page and module pages</li>
- *   <li>JavaScript that implementing the bookmarklets</li>
- *   <li>The web API for recompiling a GWT app</li>
- *   <li>The output files and log files from the GWT compiler</li>
- *   <li>Java source code (for source-level debugging)</li>
- * </ul>
- *
- * <p>EXPERIMENTAL. There is no authentication, encryption, or XSS protection, so this server is
- * only safe to run on localhost.</p>
- */
-// This file was copied from GWT project and adjusted to run against
-// Jetty 9.2.2. The original diff can be found here:
-// https://gwt-review.googlesource.com/#/c/7857/13/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
-public class WebServer {
-
-  private static final Pattern SAFE_DIRECTORY =
-      Pattern.compile("([a-zA-Z0-9_-]+\\.)*[a-zA-Z0-9_-]+"); // no extension needed
-
-  private static final Pattern SAFE_FILENAME =
-      Pattern.compile("([a-zA-Z0-9_-]+\\.)+[a-zA-Z0-9_-]+"); // an extension is required
-
-  private static final Pattern SAFE_MODULE_PATH =
-      Pattern.compile("/(" + SAFE_DIRECTORY + ")/$");
-
-  static final Pattern SAFE_DIRECTORY_PATH =
-      Pattern.compile("/(" + SAFE_DIRECTORY + "/)+$");
-
-  /* visible for testing */
-  static final Pattern SAFE_FILE_PATH =
-      Pattern.compile("/(" + SAFE_DIRECTORY + "/)+" + SAFE_FILENAME + "$");
-
-  static final Pattern STRONG_NAME = Pattern.compile("[\\dA-F]{32}");
-
-  private static final Pattern CACHE_JS_FILE = Pattern.compile("/(" + STRONG_NAME + ").cache.js$");
-
-  private static final MimeTypes MIME_TYPES = new MimeTypes();
-
-  private static final String TIME_IN_THE_PAST = "Fri, 01 Jan 1990 00:00:00 GMT";
-
-  private final SourceHandler handler;
-  private final JsonExporter jsonExporter;
-  private final OutboxTable outboxes;
-  private final JobRunner runner;
-  private final JobEventTable eventTable;
-
-  private final String bindAddress;
-  private final int port;
-
-  private Server server;
-
-  WebServer(SourceHandler handler, JsonExporter jsonExporter, OutboxTable outboxes,
-      JobRunner runner, JobEventTable eventTable, String bindAddress, int port) {
-    this.handler = handler;
-    this.jsonExporter = jsonExporter;
-    this.outboxes = outboxes;
-    this.runner = runner;
-    this.eventTable = eventTable;
-    this.bindAddress = bindAddress;
-    this.port = port;
-  }
-
-  @SuppressWarnings("serial")
-  void start(final TreeLogger logger) throws UnableToCompleteException {
-
-    Server newServer = new Server();
-    ServerConnector connector = new ServerConnector(newServer);
-    connector.setHost(bindAddress);
-    connector.setPort(port);
-    connector.setReuseAddress(false);
-    connector.setSoLingerTime(0);
-
-    newServer.addConnector(connector);
-
-    ServletContextHandler newHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
-    newHandler.setContextPath("/");
-    newHandler.addServlet(new ServletHolder(new HttpServlet() {
-      @Override
-      protected void doGet(HttpServletRequest request, HttpServletResponse response)
-          throws ServletException, IOException {
-        handleRequest(request.getPathInfo(), request, response, logger);
-      }
-    }), "/*");
-    newHandler.addFilter(GzipFilter.class, "/*", EnumSet.allOf(DispatcherType.class));
-    newServer.setHandler(newHandler);
-    try {
-      newServer.start();
-    } catch (Exception e) {
-      logger.log(TreeLogger.ERROR, "cannot start web server", e);
-      throw new UnableToCompleteException();
-    }
-    this.server = newServer;
-  }
-
-  public int getPort() {
-    return port;
-  }
-
-  public void stop() throws Exception {
-    server.stop();
-    server = null;
-  }
-
-  /**
-   * Returns the location of the compiler output. (Changes after every recompile.)
-   * @param outputModuleName the module name that the GWT compiler used in its output.
-   */
-  public File getCurrentWarDir(String outputModuleName) {
-    return outboxes.findByOutputModuleName(outputModuleName).getWarDir();
-  }
-
-  private void handleRequest(String target, HttpServletRequest request,
-      HttpServletResponse response, TreeLogger parentLogger)
-      throws IOException {
-
-    if (request.getMethod().equalsIgnoreCase("get")) {
-
-      TreeLogger logger = parentLogger.branch(Type.TRACE, "GET " + target);
-
-      Response page = doGet(target, request, logger);
-      if (page == null) {
-        logger.log(Type.WARN, "not handled: " + target);
-        return;
-      }
-
-      setHandled(request);
-      if (!target.endsWith(".cache.js")) {
-        // Make sure IE9 doesn't cache any pages.
-        // (Nearly all pages may change on server restart.)
-        response.setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
-        response.setHeader("Pragma", "no-cache");
-        response.setHeader("Expires", TIME_IN_THE_PAST);
-        response.setDateHeader("Date", new Date().getTime());
-      }
-      page.send(request, response, logger);
-    }
-  }
-
-  /**
-   * Returns the page that should be sent in response to a GET request, or null for no response.
-   */
-  private Response doGet(String target, HttpServletRequest request, TreeLogger logger)
-      throws IOException {
-
-    if (target.equals("/")) {
-      JsonObject json = jsonExporter.exportFrontPageVars();
-      return Pages.newHtmlPage("config", json, "frontpage.html");
-    }
-
-    if (target.equals("/dev_mode_on.js")) {
-      JsonObject json = jsonExporter.exportDevModeOnVars();
-      return Responses.newJavascriptResponse("__gwt_codeserver_config", json,
-          "dev_mode_on.js");
-    }
-
-    // Recompile on request from the bookmarklet.
-    // This is a GET because a bookmarklet can call it from a different origin (JSONP).
-    if (target.startsWith("/recompile/")) {
-      String moduleName = target.substring("/recompile/".length());
-      Outbox box = outboxes.findByOutputModuleName(moduleName);
-      if (box == null) {
-        return new ErrorPage("No such module: " + moduleName);
-      }
-
-      // We are passing properties from an unauthenticated GET request directly to the compiler.
-      // This should be safe, but only because these are binding properties. For each binding
-      // property, you can only choose from a set of predefined values. So all an attacker can do is
-      // cause a spurious recompile, resulting in an unexpected permutation being loaded later.
-      //
-      // It would be unsafe to allow a configuration property to be changed.
-      Job job = box.makeJob(getBindingProperties(request), logger);
-      runner.submit(job);
-      Job.Result result = job.waitForResult();
-      JsonObject json = jsonExporter.exportRecompileResponse(result);
-      return Responses.newJsonResponse(json);
-    }
-
-    if (target.startsWith("/log/")) {
-      String moduleName = target.substring("/log/".length());
-      Outbox box = outboxes.findByOutputModuleName(moduleName);
-      if (box == null) {
-        return new ErrorPage("No such module: " + moduleName);
-      } else if (box.containsStubCompile()) {
-        return new ErrorPage("This module hasn't been compiled yet.");
-      } else {
-        return makeLogPage(box);
-      }
-    }
-
-    if (target.equals("/favicon.ico")) {
-      InputStream faviconStream = getClass().getResourceAsStream("favicon.ico");
-      if (faviconStream == null) {
-        return new ErrorPage("icon not found");
-      }
-      // IE8 will not load the favicon in an img tag with the default MIME type,
-      // so use "image/x-icon" instead.
-      return Responses.newBinaryStreamResponse("image/x-icon", faviconStream);
-    }
-
-    if (target.equals("/policies/")) {
-      return makePolicyIndexPage();
-    }
-
-    if (target.equals("/progress")) {
-      // TODO: return a list of progress objects here, one for each job.
-      JobEvent event = eventTable.getCompilingJobEvent();
-
-      JsonObject json;
-      if (event == null) {
-        json = new JsonObject();
-        json.put("status", "idle");
-      } else {
-        json = jsonExporter.exportProgressResponse(event);
-      }
-      return Responses.newJsonResponse(json);
-    }
-
-    Matcher matcher = SAFE_MODULE_PATH.matcher(target);
-    if (matcher.matches()) {
-      return makeModulePage(matcher.group(1));
-    }
-
-    matcher = SAFE_DIRECTORY_PATH.matcher(target);
-    if (matcher.matches() && SourceHandler.isSourceMapRequest(target)) {
-      return handler.handle(target, request, logger);
-    }
-
-    matcher = SAFE_FILE_PATH.matcher(target);
-    if (matcher.matches()) {
-      if (SourceHandler.isSourceMapRequest(target)) {
-        return handler.handle(target, request, logger);
-      }
-      if (target.startsWith("/policies/")) {
-        return makePolicyFilePage(target);
-      }
-      return makeCompilerOutputPage(target);
-    }
-
-    logger.log(TreeLogger.WARN, "ignored get request: " + target);
-    return null; // not handled
-  }
-
-  /**
-   * Returns a file that the compiler wrote to its war directory.
-   */
-  private Response makeCompilerOutputPage(String target) {
-
-    int secondSlash = target.indexOf('/', 1);
-    String moduleName = target.substring(1, secondSlash);
-    Outbox box = outboxes.findByOutputModuleName(moduleName);
-    if (box == null) {
-      return new ErrorPage("No such module: " + moduleName);
-    }
-
-    final String contentEncoding;
-    File file = box.getOutputFile(target);
-    if (!file.isFile()) {
-      // perhaps it's compressed
-      file = box.getOutputFile(target + ".gz");
-      if (!file.isFile()) {
-        return new ErrorPage("not found: " + file.toString());
-      }
-      contentEncoding = "gzip";
-    } else {
-      contentEncoding = null;
-    }
-
-    final String sourceMapUrl;
-    Matcher match = CACHE_JS_FILE.matcher(target);
-    if (match.matches()) {
-      String strongName = match.group(1);
-      String template = SourceHandler.sourceMapLocationTemplate(moduleName);
-      sourceMapUrl = template.replace("__HASH__", strongName);
-    } else {
-      sourceMapUrl = null;
-    }
-
-    String mimeType = guessMimeType(target);
-    final Response barePage = Responses.newFileResponse(mimeType, file);
-
-    // Wrap the response to send the extra headers.
-    return new Response() {
-      @Override
-      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
-          throws IOException {
-        // TODO: why do we need this? Looks like Ray added it a long time ago.
-        response.setHeader("Access-Control-Allow-Origin", "*");
-
-        if (sourceMapUrl != null) {
-          response.setHeader("X-SourceMap", sourceMapUrl);
-          response.setHeader("SourceMap", sourceMapUrl);
-        }
-
-        if (contentEncoding != null) {
-          if (!request.getHeader("Accept-Encoding").contains("gzip")) {
-            response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
-            logger.log(TreeLogger.WARN, "client doesn't accept gzip; bailing");
-            return;
-          }
-          response.setHeader("Content-Encoding", "gzip");
-        }
-
-        barePage.send(request, response, logger);
-      }
-    };
-  }
-
-  private Response makeModulePage(String moduleName) {
-    Outbox box = outboxes.findByOutputModuleName(moduleName);
-    if (box == null) {
-      return new ErrorPage("No such module: " + moduleName);
-    }
-
-    JsonObject json = jsonExporter.exportModulePageVars(box);
-    return Pages.newHtmlPage("config", json, "modulepage.html");
-  }
-
-  private Response makePolicyIndexPage() {
-
-    return new Response() {
-
-      @Override
-      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
-          throws IOException {
-        response.setContentType("text/html");
-
-        HtmlWriter out = new HtmlWriter(response.getWriter());
-
-        out.startTag("html").nl();
-        out.startTag("head").nl();
-        out.startTag("title").text("Policy Files").endTag("title").nl();
-        out.endTag("head");
-        out.startTag("body");
-
-        out.startTag("h1").text("Policy Files").endTag("h1").nl();
-
-        for (Outbox box : outboxes.getOutboxes()) {
-          List<PolicyFile> policies = box.readRpcPolicyManifest();
-          if (!policies.isEmpty()) {
-            out.startTag("h2").text(box.getOutputModuleName()).endTag("h2").nl();
-
-            out.startTag("table").nl();
-            for (PolicyFile policy : policies) {
-
-              out.startTag("tr");
-
-              out.startTag("td");
-
-              out.startTag("a", "href=", policy.getServiceSourceUrl());
-              out.text(policy.getServiceName());
-              out.endTag("a");
-
-              out.endTag("td");
-
-              out.startTag("td");
-
-              out.startTag("a", "href=", policy.getUrl());
-              out.text(policy.getName());
-              out.endTag("a");
-
-              out.endTag("td");
-
-              out.endTag("tr").nl();
-            }
-            out.endTag("table").nl();
-          }
-        }
-
-        out.endTag("body").nl();
-        out.endTag("html").nl();
-      }
-    };
-  }
-
-  private Response makePolicyFilePage(String target) {
-
-    int secondSlash = target.indexOf('/', 1);
-    if (secondSlash < 1) {
-      return new ErrorPage("invalid URL for policy file: " + target);
-    }
-
-    String rest = target.substring(secondSlash + 1);
-    if (rest.contains("/") || !rest.endsWith(".gwt.rpc")) {
-      return new ErrorPage("invalid name for policy file: " + rest);
-    }
-
-    File fileToSend = outboxes.findPolicyFile(rest);
-    if (fileToSend == null) {
-      return new ErrorPage("Policy file not found: " + rest);
-    }
-
-    return Responses.newFileResponse("text/plain", fileToSend);
-  }
-
-  /**
-   * Sends the log file as html with errors highlighted in red.
-   */
-  private Response makeLogPage(final Outbox box) {
-    final File file = box.getCompileLog();
-    if (!file.isFile()) {
-      return new ErrorPage("log file not found");
-    }
-
-    return new Response() {
-
-      @Override
-      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
-          throws IOException {
-        BufferedReader reader = new BufferedReader(new FileReader(file));
-
-        response.setStatus(HttpServletResponse.SC_OK);
-        response.setContentType("text/html");
-        response.setHeader("Content-Style-Type", "text/css");
-
-        HtmlWriter out = new HtmlWriter(response.getWriter());
-        out.startTag("html").nl();
-        out.startTag("head").nl();
-        out.startTag("title").text(box.getOutputModuleName() + " compile log").endTag("title").nl();
-        out.startTag("style").nl();
-        out.text(".error { color: red; font-weight: bold; }").nl();
-        out.endTag("style").nl();
-        out.endTag("head").nl();
-        out.startTag("body").nl();
-        sendLogAsHtml(reader, out);
-        out.endTag("body").nl();
-        out.endTag("html").nl();
-      }
-    };
-  }
-
-  private static final Pattern ERROR_PATTERN = Pattern.compile("\\[ERROR\\]");
-
-  /**
-   * Copies in to out line by line, escaping each line for html characters and highlighting
-   * error lines. Closes <code>in</code> when done.
-   */
-  private static void sendLogAsHtml(BufferedReader in, HtmlWriter out) throws IOException {
-    try {
-      out.startTag("pre").nl();
-      String line = in.readLine();
-      while (line != null) {
-        Matcher m = ERROR_PATTERN.matcher(line);
-        boolean error = m.find();
-        if (error) {
-          out.startTag("span", "class=", "error");
-        }
-        out.text(line);
-        if (error) {
-          out.endTag("span");
-        }
-        out.nl(); // the readLine doesn't include the newline.
-        line = in.readLine();
-      }
-      out.endTag("pre").nl();
-    } finally {
-      in.close();
-    }
-  }
-
-  /* visible for testing */
-  static String guessMimeType(String filename) {
-    String mimeType = MIME_TYPES.getMimeByExtension(filename);
-    return mimeType != null ? mimeType : "";
-  }
-
-  /**
-   * Returns the binding properties from the web page where dev mode is being used. (As passed in
-   * by dev_mode_on.js in a JSONP request to "/recompile".)
-   */
-  private Map<String, String> getBindingProperties(HttpServletRequest request) {
-    Map<String, String> result = new HashMap<>();
-    for (Object key : request.getParameterMap().keySet()) {
-      String propName = (String) key;
-      if (!propName.equals("_callback")) {
-        result.put(propName, request.getParameter(propName));
-      }
-    }
-    return result;
-  }
-
-  private static void setHandled(HttpServletRequest request) {
-    Request baseRequest = (request instanceof Request) ? (Request) request :
-        HttpConnection.getCurrentConnection().getHttpChannel().getRequest();
-    baseRequest.setHandled(true);
-  }
-}
diff --git a/gerrit-gwtexpui/BUCK b/gerrit-gwtexpui/BUCK
index 4b2cb03..79a97a9 100644
--- a/gerrit-gwtexpui/BUCK
+++ b/gerrit-gwtexpui/BUCK
@@ -7,7 +7,7 @@
   resources = [
     SRC + 'clippy/client/clippy.css',
     SRC + 'clippy/client/clippy.swf',
-    SRC + 'clippy/client/clipboard-16.png',
+    SRC + 'clippy/client/page_white_copy.png',
     SRC + 'clippy/client/CopyableLabelText.properties',
   ],
   provided_deps = ['//lib/gwt:user'],
@@ -15,7 +15,7 @@
     ':SafeHtml',
     ':UserAgent',
     '//lib:LICENSE-clippy',
-    '//lib:LICENSE-drifty',
+    '//lib:LICENSE-silk_icons',
   ],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-gwtexpui/BUILD b/gerrit-gwtexpui/BUILD
new file mode 100644
index 0000000..d3b03ef
--- /dev/null
+++ b/gerrit-gwtexpui/BUILD
@@ -0,0 +1,114 @@
+load('//tools/bzl:gwt.bzl', 'gwt_module')
+load('//tools/bzl:junit.bzl', 'junit_tests')
+
+SRC = 'src/main/java/com/google/gwtexpui/'
+
+gwt_module(
+  name = 'Clippy',
+  srcs = glob([SRC + 'clippy/client/*.java']),
+  gwt_xml = SRC + 'clippy/Clippy.gwt.xml',
+  resources = [
+    SRC + 'clippy/client/clippy.css',
+    SRC + 'clippy/client/clippy.swf',
+    SRC + 'clippy/client/page_white_copy.png',
+    SRC + 'clippy/client/CopyableLabelText.properties',
+  ],
+  deps = [
+    ':SafeHtml',
+    ':UserAgent',
+    '//lib/gwt:user',
+  ],
+  visibility = ['//visibility:public'],
+)
+
+java_library(
+  name = 'CSS',
+  srcs = glob([SRC + 'css/rebind/*.java']),
+  resources = [SRC + 'css/CSS.gwt.xml'],
+  deps = ['//lib/gwt:dev'],
+  visibility = ['//visibility:public'],
+)
+
+gwt_module(
+  name = 'GlobalKey',
+  srcs = glob([SRC + 'globalkey/client/*.java']),
+  gwt_xml = SRC + 'globalkey/GlobalKey.gwt.xml',
+  resources = [
+    SRC + 'globalkey/client/KeyConstants.properties',
+    SRC + 'globalkey/client/key.css',
+  ],
+  deps = [
+    ':SafeHtml',
+    ':UserAgent',
+    '//lib/gwt:user',
+  ],
+  visibility = ['//visibility:public'],
+)
+
+java_library(
+  name = 'linker_server',
+  srcs = glob([SRC + 'linker/server/*.java']),
+  deps = ['//lib:servlet-api-3_1'],
+  visibility = ['//visibility:public'],
+)
+
+gwt_module(
+  name = 'Progress',
+  srcs = glob([SRC + 'progress/client/*.java']),
+  gwt_xml = SRC + 'progress/Progress.gwt.xml',
+  resources = [SRC + 'progress/client/progress.css'],
+  deps = ['//lib/gwt:user'],
+  visibility = ['//visibility:public'],
+)
+
+gwt_module(
+  name = 'SafeHtml',
+  srcs = glob([SRC + 'safehtml/client/*.java']),
+  gwt_xml = SRC + 'safehtml/SafeHtml.gwt.xml',
+  resources = [SRC + 'safehtml/client/safehtml.css'],
+  deps = ['//lib/gwt:user'],
+  visibility = ['//visibility:public'],
+)
+
+junit_tests(
+  name = 'SafeHtml_tests',
+  srcs = glob([
+    'src/test/java/com/google/gwtexpui/safehtml/client/**/*.java',
+  ]),
+  deps = [
+    ':SafeHtml',
+    '//lib:truth',
+    '//lib/gwt:user',
+    '//lib/gwt:dev',
+  ],
+)
+
+gwt_module(
+  name = 'UserAgent',
+  srcs = glob([SRC + 'user/client/*.java']),
+  gwt_xml = SRC + 'user/User.gwt.xml',
+  resources = [SRC + 'user/client/tooltip.css'],
+  deps = ['//lib/gwt:user'],
+  visibility = ['//visibility:public'],
+)
+
+java_library(
+  name = 'server',
+  srcs = glob([SRC + 'server/*.java']),
+  deps = ['//lib:servlet-api-3_1'],
+  visibility = ['//visibility:public'],
+)
+
+java_library(
+  name = 'client-src-lib',
+  srcs = [],
+  resources = glob(
+    [SRC + n for n in [
+      'clippy/**/*',
+      'globalkey/**/*',
+      'safehtml/**/*',
+      'user/**/*',
+    ]]
+  ),
+  visibility = ['//visibility:public'],
+)
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
index dd3cc18..a97b392 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
@@ -21,7 +21,7 @@
 import com.google.gwt.resources.client.ImageResource;
 
 public interface ClippyResources extends ClientBundle {
-  public static final ClippyResources I = GWT.create(ClippyResources.class);
+  ClippyResources I = GWT.create(ClippyResources.class);
 
   @Source("clippy.css")
   ClippyCss css();
@@ -30,6 +30,6 @@
   @DoNotEmbed
   DataResource swf();
 
-  @Source("clipboard-16.png")
+  @Source("page_white_copy.png")
   ImageResource clipboard();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java
index 4d1b837..8e4d090 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java
@@ -18,7 +18,7 @@
 import com.google.gwt.i18n.client.Constants;
 
 interface CopyableLabelText extends Constants {
-  static final CopyableLabelText I = GWT.create(CopyableLabelText.class);
+  CopyableLabelText I = GWT.create(CopyableLabelText.class);
 
   String tooltip();
   String copied();
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clipboard-16.png b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clipboard-16.png
deleted file mode 100644
index 9c6e10a..0000000
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clipboard-16.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/page_white_copy.png b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/page_white_copy.png
new file mode 100644
index 0000000..a9f31a2
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/page_white_copy.png
Binary files differ
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
index 05f41d4..4b67260 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
@@ -15,5 +15,5 @@
 package com.google.gwtexpui.globalkey.client;
 
 public interface KeyCommandFilter {
-  public boolean include(KeyCommand key);
+  boolean include(KeyCommand key);
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
index d26ca8c..2b5984d 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
@@ -18,7 +18,7 @@
 import com.google.gwt.i18n.client.Constants;
 
 public interface KeyConstants extends Constants {
-  public static final KeyConstants I = GWT.create(KeyConstants.class);
+  KeyConstants I = GWT.create(KeyConstants.class);
 
   String applicationSection();
   String showHelp();
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
index a52ca2a..562e12d 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
@@ -18,7 +18,7 @@
 import com.google.gwt.resources.client.ClientBundle;
 
 public interface KeyResources extends ClientBundle {
-  public static final KeyResources I = GWT.create(KeyResources.class);
+  KeyResources I = GWT.create(KeyResources.class);
 
   @Source("key.css")
   KeyCss css();
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
index 38a488e..fd0da74 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
@@ -14,7 +14,6 @@
 
 package com.google.gwtexpui.globalkey.client;
 
-import com.google.gwt.dom.client.Element;
 import com.google.gwt.user.client.ui.TextArea;
 
 public class NpTextArea extends TextArea {
@@ -22,11 +21,6 @@
     addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
   }
 
-  public NpTextArea(final Element element) {
-    super(element);
-    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
-  }
-
   public void setSpellCheck(boolean spell) {
     getElement().setPropertyBoolean("spellcheck", spell);
   }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
index 6c820a8..a33f605 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
@@ -30,7 +30,7 @@
  * Ported from JavaScript in {@code com.google.gwt.user.UserAgent.gwt.xml}.
  */
 public class UserAgentRule {
-  private static final Pattern msie = compile(".*msie ([0-9]+)\\.([0-9]+).*");
+  private static final Pattern msie = compile(".*msie ([0-11]+)\\.([0-11]+).*");
   private static final Pattern gecko = compile(".*rv:([0-9]+)\\.([0-9]+).*");
 
   public String getName() {
@@ -58,6 +58,9 @@
       Matcher m = msie.matcher(ua);
       if (m.matches() && m.groupCount() == 2) {
         int v = makeVersion(m);
+        if (v >= 11000) {
+          return "ie11";
+        }
         if (v >= 10000) {
           return "ie10";
         }
@@ -70,6 +73,8 @@
       }
       return null;
 
+    } else if (ua.contains("edge")) {
+      return "edge";
     } else if (ua.contains("gecko")) {
       Matcher m = gecko.matcher(ua);
       if (m.matches() && m.groupCount() == 2) {
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
index 0276e9a..6bcf2c4 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
@@ -18,7 +18,7 @@
 import com.google.gwt.resources.client.ClientBundle;
 
 public interface ProgressResources extends ClientBundle {
-  public static final ProgressResources I = GWT.create(ProgressResources.class);
+  ProgressResources I = GWT.create(ProgressResources.class);
 
   @Source("progress.css")
   ProgressCss css();
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
index 6eaa7fd..a84835e 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
@@ -102,7 +102,7 @@
     }
   }
 
-  private static interface Tag {
+  private interface Tag {
     void assertSafe(String name, String value);
   }
 
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java
index f7bc907..487e613 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java
@@ -22,7 +22,7 @@
    * @return regular expression to match substrings with; should be treated as
    *     immutable.
    */
-  public RegExp pattern();
+  RegExp pattern();
 
   /**
    * Find and replace a single instance of this pattern in an input.
@@ -36,5 +36,5 @@
    * @return result of regular expression replacement.
    * @throws IllegalArgumentException if the input could not be safely sanitized.
    */
-  public String replace(String input);
+  String replace(String input);
 }
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..cf5a445 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,59 @@
         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>");
+      StringBuilder pattern = new StringBuilder();
+      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.
+
+        if (pattern.length() > 0) {
+          pattern.append("|");
+        }
+        pattern.append(qterm);
+      }
+
+      ds = sgi(ds, "(" + pattern.toString() + ")", "<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/main/java/com/google/gwtexpui/server/CacheHeaders.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
index 950400a..d94f243 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
@@ -129,9 +129,8 @@
   }
 
   public static boolean hasCacheHeader(HttpServletResponse res) {
-    return res.getHeader("Cache-Control") != null
-        || res.getHeader("Expires") != null
-        || "no-cache".equals(res.getHeader("Pragma"));
+    return res.containsHeader("Cache-Control")
+        || res.containsHeader("Expires");
   }
 
   private static void cache(HttpServletResponse res,
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/Tooltip.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/Tooltip.java
index e3ab034..7939697 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/Tooltip.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/Tooltip.java
@@ -23,7 +23,7 @@
 /** Displays custom tooltip message below an element. */
 public class Tooltip {
   interface Resources extends ClientBundle {
-    static final Resources I = GWT.create(Resources.class);
+    Resources I = GWT.create(Resources.class);
 
     @Source("tooltip.css")
     Css css();
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
index 2070200..bbef449 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
@@ -152,6 +152,25 @@
   private static native void bustOutOfIFrame(String newloc)
   /*-{ top.location.href = newloc }-*/;
 
+  /**
+   * Test if Gerrit is running on a mobile browser. This check could be
+   * incomplete, but should cover most cases. Regexes shamelessly borrowed from
+   * CodeMirror.
+   */
+  public static native boolean isMobile() /*-{
+    var ua = $wnd.navigator.userAgent;
+    var ios = /AppleWebKit/.test(ua) && /Mobile\/\w+/.test(ua);
+    return ios
+        || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(ua);
+  }-*/;
+
+  /**
+   * Check if the height of the browser view is greater than its width.
+   */
+  public static boolean isPortrait() {
+    return Window.getClientHeight() > Window.getClientWidth();
+  }
+
   private UserAgent() {
   }
 }
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/BUCK b/gerrit-gwtui-common/BUCK
index 436714a..ef78d98 100644
--- a/gerrit-gwtui-common/BUCK
+++ b/gerrit-gwtui-common/BUCK
@@ -41,7 +41,7 @@
   binary_jar = ':diffy_image_files_ln',
   deps = [
     '//lib:LICENSE-diffy',
-    '//lib:LICENSE-CC-BY3.0',
+    '//lib:LICENSE-CC-BY3.0-unported',
   ],
   visibility = ['PUBLIC'],
 )
@@ -64,7 +64,7 @@
     ':client',
     '//lib:junit',
     '//lib/gwt:user',
-    '//lib/jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit:jgit',
   ],
   source_under_test = [':client'],
   vm_args = ['-Xmx512m'],
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/CommonConstants.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java
index 032c47c..5d0b93f 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java
@@ -18,7 +18,7 @@
 import com.google.gwt.i18n.client.Constants;
 
 public interface CommonConstants extends Constants {
-  public static final CommonConstants C = GWT.create(CommonConstants.class);
+  CommonConstants C = GWT.create(CommonConstants.class);
 
   String inTheFuture();
   String month();
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java
index aa5e3cf..5a5b4a3 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java
@@ -18,7 +18,7 @@
 import com.google.gwt.i18n.client.Messages;
 
 public interface CommonMessages extends Messages {
-  public static final CommonMessages M = GWT.create(CommonMessages.class);
+  CommonMessages M = GWT.create(CommonMessages.class);
 
   String secondsAgo(long seconds);
   String minutesAgo(long minutes);
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java
index b6af366..b357737 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.client;
 
-import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gerrit.client.info.GeneralPreferences;
 import com.google.gwt.i18n.client.DateTimeFormat;
 
 import java.util.Date;
@@ -28,7 +28,7 @@
   private final DateTimeFormat mDate;
   private final DateTimeFormat dtfmt;
 
-  public DateFormatter(AccountPreferencesInfo prefs) {
+  public DateFormatter(GeneralPreferences prefs) {
     String fmt_sTime = prefs.timeFormat().getFormat();
     String fmt_sDate = prefs.dateFormat().getShortFormat();
     String fmt_mDate = prefs.dateFormat().getLongFormat();
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 61f73c0..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
@@ -20,6 +20,8 @@
   CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS,
   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,
@@ -34,6 +36,6 @@
   PROJECT_INFO_SCREEN_TOP, PROJECT_INFO_SCREEN_BOTTOM;
 
   public enum Key {
-    ACCOUNT_INFO, CHANGE_INFO, PROJECT_NAME
+    ACCOUNT_INFO, CHANGE_INFO, PROJECT_NAME, REVISION_INFO
   }
 }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
index eb4b0ba..2165db2 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
@@ -51,9 +51,8 @@
       long seconds = round(ageMillis, SECOND_IN_MILLIS);
       if (seconds == 1) {
         return C.oneSecondAgo();
-      } else {
-        return M.secondsAgo(seconds);
       }
+      return M.secondsAgo(seconds);
     }
 
     // minutes
@@ -61,9 +60,8 @@
       long minutes = round(ageMillis, MINUTE_IN_MILLIS);
       if (minutes == 1) {
         return C.oneMinuteAgo();
-      } else {
-        return M.minutesAgo(minutes);
       }
+      return M.minutesAgo(minutes);
     }
 
     // hours
@@ -71,9 +69,8 @@
       long hours = round(ageMillis, HOUR_IN_MILLIS);
       if (hours == 1) {
         return C.oneHourAgo();
-      } else {
-        return M.hoursAgo(hours);
       }
+      return M.hoursAgo(hours);
     }
 
     // up to 14 days use days
@@ -81,9 +78,8 @@
       long days = round(ageMillis, DAY_IN_MILLIS);
       if (days == 1) {
         return C.oneDayAgo();
-      } else {
-        return M.daysAgo(days);
       }
+      return M.daysAgo(days);
     }
 
     // up to 10 weeks use weeks
@@ -91,9 +87,8 @@
       long weeks = round(ageMillis, WEEK_IN_MILLIS);
       if (weeks == 1) {
         return C.oneWeekAgo();
-      } else {
-        return M.weeksAgo(weeks);
       }
+      return M.weeksAgo(weeks);
     }
 
     // months
@@ -101,9 +96,8 @@
       long months = round(ageMillis, MONTH_IN_MILLIS);
       if (months == 1) {
         return C.oneMonthAgo();
-      } else {
-        return M.monthsAgo(months);
       }
+      return M.monthsAgo(months);
     }
 
     // up to 5 years use "year, months" rounded to months
@@ -114,18 +108,16 @@
       String monthLabel = (months > 1) ? C.months() : (months == 1 ? C.month() : "");
       if (months == 0) {
         return M.years0MonthsAgo(years, yearLabel);
-      } else {
-        return M.yearsMonthsAgo(years, yearLabel, months, monthLabel);
       }
+      return M.yearsMonthsAgo(years, yearLabel, months, monthLabel);
     }
 
     // years
     long years = round(ageMillis, YEAR_IN_MILLIS);
     if (years == 1) {
       return C.oneYearAgo();
-    } else {
-      return M.yearsAgo(years);
     }
+    return M.yearsAgo(years);
   }
 
   private static long upperLimit(long unit) {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
index a5a02cd..95751fa 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
@@ -18,93 +18,115 @@
 import com.google.gwt.resources.client.ImageResource;
 
 public interface Resources extends ClientBundle {
-  @Source("addFileComment.png")
-  public ImageResource addFileComment();
+  /**
+   * silk icons (CC-BY3.0): http://famfamfam.com/lab/icons/silk/
+   */
+  @Source("note_add.png")
+  ImageResource addFileComment();
 
-  @Source("arrowDown.png")
-  public ImageResource arrowDown();
+  @Source("tag_blue_add.png")
+  ImageResource addHashtag();
 
-  @Source("arrowRight.png")
-  public ImageResource arrowRight();
+  @Source("user_add.png")
+  ImageResource addUser();
 
-  @Source("arrowUp.png")
-  public ImageResource arrowUp();
+  // derived from resultset_next.png
+  @Source("resultset_down_gray.png")
+  ImageResource arrowDown();
 
-  @Source("deleteHover.png")
-  public ImageResource deleteHover();
+  // derived from resultset_next.png
+  @Source("resultset_next_gray.png")
+  ImageResource arrowRight();
 
-  @Source("deleteNormal.png")
-  public ImageResource deleteNormal();
+  // derived from resultset_next.png
+  @Source("resultset_up_gray.png")
+  ImageResource arrowUp();
 
-  @Source("diffy26.png")
-  public ImageResource gerritAvatar26();
+  @Source("lightbulb.png")
+  ImageResource blame();
 
-  @Source("downloadIcon.png")
-  public ImageResource downloadIcon();
+  @Source("page_white_put.png")
+  ImageResource downloadIcon();
 
-  @Source("draftComments.png")
-  public ImageResource draftComments();
+  // derived from comment.png
+  @Source("comment_draft.png")
+  ImageResource draftComments();
 
-  @Source("editText.png")
-  public ImageResource edit();
+  @Source("page_edit.png")
+  ImageResource edit();
 
-  @Source("editUndo.png")
-  public ImageResource editUndo();
+  @Source("arrow_undo.png")
+  ImageResource editUndo();
 
-  @Source("gear.png")
-  public ImageResource gear();
+  @Source("cog.png")
+  ImageResource gear();
 
+  @Source("tick.png")
+  ImageResource greenCheck();
+
+  @Source("tag_blue.png")
+  ImageResource hashtag();
+
+  @Source("lightbulb.png")
+  ImageResource info();
+
+  @Source("find.png")
+  ImageResource queryIcon();
+
+  @Source("lock.png")
+  ImageResource readOnly();
+
+  @Source("cross.png")
+  ImageResource redNot();
+
+  @Source("disk.png")
+  ImageResource save();
+
+  @Source("star.png")
+  ImageResource starFilled();
+
+  // derived from star.png
+  @Source("star-open.png")
+  ImageResource starOpen();
+
+  @Source("exclamation.png")
+  ImageResource warning();
+
+  @Source("help.png")
+  ImageResource question();
+
+  /**
+   * tango icon library (public domain):
+   * http://tango.freedesktop.org/Tango_Icon_Library
+   */
   @Source("goNext.png")
-  public ImageResource goNext();
+  ImageResource goNext();
 
   @Source("goPrev.png")
-  public ImageResource goPrev();
+  ImageResource goPrev();
 
   @Source("goUp.png")
-  public ImageResource goUp();
-
-  @Source("greenCheck.png")
-  public ImageResource greenCheck();
-
-  @Source("info.png")
-  public ImageResource info();
+  ImageResource goUp();
 
   @Source("listAdd.png")
-  public ImageResource listAdd();
+  ImageResource listAdd();
 
-  @Source("mediaFloppy.png")
-  public ImageResource save();
-
+  // derived from important.png
   @Source("merge.png")
-  public ImageResource merge();
+  ImageResource merge();
 
-  @Source("queryIcon.png")
-  public ImageResource queryIcon();
-
-  @Source("readOnly.png")
-  public ImageResource readOnly();
-
-  @Source("redNot.png")
-  public ImageResource redNot();
-
+  /**
+   * contributed by the artist under Apache2.0
+   */
   @Source("sideBySideDiff.png")
-  public ImageResource sideBySideDiff();
-
-  @Source("starFilled.png")
-  public ImageResource starFilled();
-
-  @Source("starOpen.png")
-  public ImageResource starOpen();
-
-  @Source("undoNormal.png")
-  public ImageResource undoNormal();
+  ImageResource sideBySideDiff();
 
   @Source("unifiedDiff.png")
-  public ImageResource unifiedDiff();
+  ImageResource unifiedDiff();
 
-  @Source("warning.png")
-  public ImageResource warning();
-
-  @Source("question.png")
-  public ImageResource question();
+  /**
+   * contributed by the artist under CC-BY3.0
+   */
+  @Source("diffy26.png")
+  ImageResource gerritAvatar26();
 }
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 6ac0404..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() {
@@ -40,9 +43,9 @@
     return ts;
   }
 
-  private final native String registeredOnRaw() /*-{ return this.registered_on; }-*/;
-  private final native Timestamp _getRegisteredOn() /*-{ return this._cts; }-*/;
-  private final native void _setRegisteredOn(Timestamp ts) /*-{ this._cts = ts; }-*/;
+  private native String registeredOnRaw() /*-{ return this.registered_on; }-*/;
+  private native Timestamp _getRegisteredOn() /*-{ return this._cts; }-*/;
+  private native void _setRegisteredOn(Timestamp ts) /*-{ this._cts = ts; }-*/;
 
   /**
    * @return true if the server supplied avatar information about this account.
@@ -64,7 +67,7 @@
     return null;
   }
 
-  private final native JsArray<AvatarInfo> avatars()
+  private native JsArray<AvatarInfo> avatars()
   /*-{ return this.avatars }-*/;
 
   public final native void name(String n) /*-{ this.name = n }-*/;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java
deleted file mode 100644
index 11a1b6a..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java
+++ /dev/null
@@ -1,219 +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.client.info;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class AccountPreferencesInfo extends JavaScriptObject {
-  public static AccountPreferencesInfo create() {
-    return createObject().cast();
-  }
-
-  public static AccountPreferencesInfo createDefault() {
-    AccountGeneralPreferences defaultPrefs =
-        AccountGeneralPreferences.createDefault();
-    AccountPreferencesInfo p = createObject().cast();
-    p.changesPerPage(defaultPrefs.getMaximumPageSize());
-    p.showSiteHeader(defaultPrefs.isShowSiteHeader());
-    p.useFlashClipboard(defaultPrefs.isUseFlashClipboard());
-    p.downloadScheme(defaultPrefs.getDownloadUrl());
-    p.downloadCommand(defaultPrefs.getDownloadCommand());
-    p.copySelfOnEmail(defaultPrefs.isCopySelfOnEmails());
-    p.dateFormat(defaultPrefs.getDateFormat());
-    p.timeFormat(defaultPrefs.getTimeFormat());
-    p.relativeDateInChangeTable(defaultPrefs.isRelativeDateInChangeTable());
-    p.sizeBarInChangeTable(defaultPrefs.isSizeBarInChangeTable());
-    p.legacycidInChangeTable(defaultPrefs.isLegacycidInChangeTable());
-    p.muteCommonPathPrefixes(defaultPrefs.isMuteCommonPathPrefixes());
-    p.reviewCategoryStrategy(defaultPrefs.getReviewCategoryStrategy());
-    p.diffView(defaultPrefs.getDiffView());
-    return p;
-  }
-
-  public final short changesPerPage() {
-    short changesPerPage =
-        get("changes_per_page", AccountGeneralPreferences.DEFAULT_PAGESIZE);
-    return 0 < changesPerPage
-        ? changesPerPage
-        : AccountGeneralPreferences.DEFAULT_PAGESIZE;
-  }
-  private final native short get(String n, int d)
-  /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
-
-  public final native boolean showSiteHeader()
-  /*-{ return this.show_site_header || false }-*/;
-
-  public final native boolean useFlashClipboard()
-  /*-{ return this.use_flash_clipboard || false }-*/;
-
-  public final native String downloadScheme()
-  /*-{ return this.download_scheme }-*/;
-
-  public final DownloadCommand downloadCommand() {
-    String s = downloadCommandRaw();
-    return s != null ? DownloadCommand.valueOf(s) : null;
-  }
-  private final native String downloadCommandRaw()
-  /*-{ return this.download_command }-*/;
-
-  public final native boolean copySelfOnEmail()
-  /*-{ return this.copy_self_on_email || false }-*/;
-
-  public final DateFormat dateFormat() {
-    String s = dateFormatRaw();
-    return s != null ? DateFormat.valueOf(s) : null;
-  }
-  private final native String dateFormatRaw()
-  /*-{ return this.date_format }-*/;
-
-  public final TimeFormat timeFormat() {
-    String s = timeFormatRaw();
-    return s != null ? TimeFormat.valueOf(s) : null;
-  }
-  private final native String timeFormatRaw()
-  /*-{ return this.time_format }-*/;
-
-  public final native boolean relativeDateInChangeTable()
-  /*-{ return this.relative_date_in_change_table || false }-*/;
-
-  public final native boolean sizeBarInChangeTable()
-  /*-{ return this.size_bar_in_change_table || false }-*/;
-
-  public final native boolean legacycidInChangeTable()
-  /*-{ return this.legacycid_in_change_table || false }-*/;
-
-  public final native boolean muteCommonPathPrefixes()
-  /*-{ return this.mute_common_path_prefixes || false }-*/;
-
-  public final ReviewCategoryStrategy reviewCategoryStrategy() {
-    String s = reviewCategeoryStrategyRaw();
-    return s != null ? ReviewCategoryStrategy.valueOf(s) : ReviewCategoryStrategy.NONE;
-  }
-  private final native String reviewCategeoryStrategyRaw()
-  /*-{ return this.review_category_strategy }-*/;
-
-  public final DiffView diffView() {
-    String s = diffViewRaw();
-    return s != null ? DiffView.valueOf(s) : null;
-  }
-  private final native String diffViewRaw()
-  /*-{ return this.diff_view }-*/;
-
-  public final native JsArray<TopMenuItem> my()
-  /*-{ return this.my; }-*/;
-
-  public final native void changesPerPage(short n)
-  /*-{ this.changes_per_page = n }-*/;
-
-  public final native void showSiteHeader(boolean s)
-  /*-{ this.show_site_header = s }-*/;
-
-  public final native void useFlashClipboard(boolean u)
-  /*-{ this.use_flash_clipboard = u }-*/;
-
-  public final native void downloadScheme(String d)
-  /*-{ this.download_scheme = d }-*/;
-
-  public final void downloadCommand(DownloadCommand d) {
-    downloadCommandRaw(d != null ? d.toString() : null);
-  }
-  public final native void downloadCommandRaw(String d)
-  /*-{ this.download_command = d }-*/;
-
-  public final native void copySelfOnEmail(boolean c)
-  /*-{ this.copy_self_on_email = c }-*/;
-
-  public final void dateFormat(DateFormat f) {
-    dateFormatRaw(f != null ? f.toString() : null);
-  }
-  private final native void dateFormatRaw(String f)
-  /*-{ this.date_format = f }-*/;
-
-  public final void timeFormat(TimeFormat f) {
-    timeFormatRaw(f != null ? f.toString() : null);
-  }
-  private final native void timeFormatRaw(String f)
-  /*-{ this.time_format = f }-*/;
-
-  public final native void relativeDateInChangeTable(boolean d)
-  /*-{ this.relative_date_in_change_table = d }-*/;
-
-  public final native void sizeBarInChangeTable(boolean s)
-  /*-{ this.size_bar_in_change_table = s }-*/;
-
-  public final native void legacycidInChangeTable(boolean s)
-  /*-{ this.legacycid_in_change_table = s }-*/;
-
-  public final native void muteCommonPathPrefixes(boolean s)
-  /*-{ this.mute_common_path_prefixes = s }-*/;
-
-  public final void reviewCategoryStrategy(ReviewCategoryStrategy s) {
-    reviewCategoryStrategyRaw(s != null ? s.toString() : null);
-  }
-  private final native void reviewCategoryStrategyRaw(String s)
-  /*-{ this.review_category_strategy = s }-*/;
-
-  public final void diffView(DiffView d) {
-    diffViewRaw(d != null ? d.toString() : null);
-  }
-  private final native void diffViewRaw(String d)
-  /*-{ this.diff_view = d }-*/;
-
-  public final void setMyMenus(List<TopMenuItem> myMenus) {
-    initMy();
-    for (TopMenuItem n : myMenus) {
-      addMy(n);
-    }
-  }
-  final native void initMy() /*-{ this.my = []; }-*/;
-  final native void addMy(TopMenuItem m) /*-{ this.my.push(m); }-*/;
-
-  public final Map<String, String> urlAliases() {
-    Map<String, String> urlAliases = new HashMap<>();
-    for (String k : Natives.keys(_urlAliases())) {
-      urlAliases.put(k, urlAliasToken(k));
-    }
-    return urlAliases;
-  }
-
-  private final native String urlAliasToken(String m) /*-{ return this.url_aliases[m]; }-*/;
-  private final native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
-
-  public final void setUrlAliases(Map<String, String> urlAliases) {
-    initUrlAliases();
-    for (Map.Entry<String, String> e : urlAliases.entrySet()) {
-      putUrlAlias(e.getKey(), e.getValue());
-    }
-  }
-  private final native void putUrlAlias(String m, String t) /*-{ this.url_aliases[m] = t; }-*/;
-  private final native void initUrlAliases() /*-{ this.url_aliases = {}; }-*/;
-
-  protected AccountPreferencesInfo() {
-  }
-}
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 15d1c6c..345e1e3 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,6 +15,7 @@
 package com.google.gerrit.client.info;
 
 import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.reviewdb.client.AuthType;
@@ -74,12 +75,16 @@
   }
 
   public final boolean isHttpPasswordSettingsEnabled() {
-    if (isLdap() && isGitBasicAuth()) {
+    if (isGitBasicAuth() && gitBasicAuthPolicy() == GitBasicAuthPolicy.LDAP) {
       return false;
     }
     return true;
   }
 
+  public final GitBasicAuthPolicy gitBasicAuthPolicy() {
+    return GitBasicAuthPolicy.valueOf(gitBasicAuthPolicyRaw());
+  }
+
   public final native boolean useContributorAgreements()
   /*-{ return this.use_contributor_agreements || false; }-*/;
   public final native String loginUrl() /*-{ return this.login_url; }-*/;
@@ -90,8 +95,10 @@
   public final native String editFullNameUrl() /*-{ return this.edit_full_name_url; }-*/;
   public final native String httpPasswordUrl() /*-{ return this.http_password_url; }-*/;
   public final native boolean isGitBasicAuth() /*-{ return this.is_git_basic_auth || false; }-*/;
-  private final native String authTypeRaw() /*-{ return this.auth_type; }-*/;
-  private final native JsArrayString _editableAccountFields()
+  private native String gitBasicAuthPolicyRaw()
+  /*-{ return this.git_basic_auth_policy; }-*/;
+  private native String authTypeRaw() /*-{ return this.auth_type; }-*/;
+  private native JsArrayString _editableAccountFields()
   /*-{ return this.editable_account_fields; }-*/;
 
   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 cace7ad..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
@@ -19,6 +19,8 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -28,9 +30,13 @@
 import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
 
 import java.sql.Timestamp;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
@@ -65,13 +71,17 @@
     return revList.get(revList.length() - 1).isEdit();
   }
 
-  private final native Timestamp _getCts() /*-{ return this._cts; }-*/;
-  private final native void _setCts(Timestamp ts) /*-{ this._cts = ts; }-*/;
+  private native Timestamp _getCts() /*-{ return this._cts; }-*/;
+  private native void _setCts(Timestamp ts) /*-{ this._cts = ts; }-*/;
 
   public final Timestamp updated() {
     return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
   }
 
+  public final Timestamp submitted() {
+    return JavaSqlTimestamp_JsonSerializer.parseTimestamp(submittedRaw());
+  }
+
   public final String idAbbreviated() {
     return new Change.Key(changeId()).abbreviate();
   }
@@ -84,6 +94,16 @@
     return allLabels().keySet();
   }
 
+  public final Set<Integer> removableReviewerIds() {
+    Set<Integer> removable = new HashSet<>();
+    if (removableReviewers() != null) {
+      for (AccountInfo a : Natives.asList(removableReviewers())) {
+        removable.add(a._accountId());
+      }
+    }
+    return removable;
+  }
+
   public final native String id() /*-{ return this.id; }-*/;
   public final native String project() /*-{ return this.project; }-*/;
   public final native String branch() /*-{ return this.branch; }-*/;
@@ -92,11 +112,12 @@
   public final native boolean mergeable() /*-{ return this.mergeable ? true : false; }-*/;
   public final native int insertions() /*-{ return this.insertions; }-*/;
   public final native int deletions() /*-{ return this.deletions; }-*/;
-  private final native String statusRaw() /*-{ return this.status; }-*/;
+  private native String statusRaw() /*-{ return this.status; }-*/;
   public final native String subject() /*-{ return this.subject; }-*/;
   public final native AccountInfo owner() /*-{ return this.owner; }-*/;
-  private final native String createdRaw() /*-{ return this.created; }-*/;
-  private final native String updatedRaw() /*-{ return this.updated; }-*/;
+  private native String createdRaw() /*-{ return this.created; }-*/;
+  private native String updatedRaw() /*-{ return this.updated; }-*/;
+  private native String submittedRaw() /*-{ return this.submitted; }-*/;
   public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
   public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
   public final native NativeMap<LabelInfo> allLabels() /*-{ return this.labels; }-*/;
@@ -121,6 +142,23 @@
   public final native JsArray<AccountInfo> removableReviewers()
   /*-{ return this.removable_reviewers; }-*/;
 
+  private native NativeMap<JsArray<AccountInfo>> _reviewers()
+  /*-{ return this.reviewers; }-*/;
+  public final Map<ReviewerState, List<AccountInfo>> reviewers() {
+    NativeMap<JsArray<AccountInfo>> reviewers = _reviewers();
+    Map<ReviewerState, List<AccountInfo>> result = new HashMap<>();
+    for (String k : reviewers.keySet()) {
+      ReviewerState state = ReviewerState.valueOf(k.toUpperCase());
+      List<AccountInfo> accounts = result.get(state);
+      if (accounts == null) {
+        accounts = new ArrayList<>();
+        result.put(state, accounts);
+      }
+      accounts.addAll(Natives.asList(reviewers.get(k)));
+    }
+    return result;
+  }
+
   public final native boolean hasActions() /*-{ return this.hasOwnProperty('actions') }-*/;
   public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
 
@@ -128,12 +166,21 @@
   public final native boolean _more_changes()
   /*-{ return this._more_changes ? true : false; }-*/;
 
+  public final SubmitType submitType() {
+    String submitType = _submitType();
+    if (submitType == null) {
+      return null;
+    }
+    return SubmitType.valueOf(submitType);
+  }
+  private native String _submitType() /*-{ return this.submit_type; }-*/;
+
   public final boolean submittable() {
     init();
     return _submittable();
   }
 
-  private final native boolean _submittable()
+  private native boolean _submittable()
   /*-{ return this.submittable ? true : false; }-*/;
 
   /**
@@ -161,9 +208,8 @@
             // more than one label is missing, so it's unclear which to quick
             // approve, return -1
             return -1;
-          } else {
-            ret = i;
           }
+          ret = i;
           continue;
 
         case OK: // Label already applied.
@@ -212,7 +258,7 @@
       return null;
     }
 
-    private final native NativeMap<NativeString> _values() /*-{ return this.values; }-*/;
+    private native NativeMap<NativeString> _values() /*-{ return this.values; }-*/;
     public final Set<String> values() {
       return Natives.keys(_values());
     }
@@ -287,12 +333,22 @@
       revisionInfo.takeFromEdit(edit);
       return revisionInfo;
     }
-    private final native void takeFromEdit(EditInfo edit) /*-{
+    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; }-*/;
@@ -388,7 +444,7 @@
   public static class GitPerson extends JavaScriptObject {
     public final native String name() /*-{ return this.name; }-*/;
     public final native String email() /*-{ return this.email; }-*/;
-    private final native String dateRaw() /*-{ return this.date; }-*/;
+    private native String dateRaw() /*-{ return this.date; }-*/;
 
     public final Timestamp date() {
       return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
@@ -402,7 +458,7 @@
     public final native AccountInfo author() /*-{ return this.author; }-*/;
     public final native String message() /*-{ return this.message; }-*/;
     public final native int _revisionNumber() /*-{ return this._revision_number || 0; }-*/;
-    private final native String dateRaw() /*-{ return this.date; }-*/;
+    private native String dateRaw() /*-{ return this.date; }-*/;
 
     public final Timestamp date() {
       return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
@@ -428,7 +484,7 @@
     public final native JsArrayString branches() /*-{ return this.branches; }-*/;
     public final native JsArrayString tags() /*-{ return this.tags; }-*/;
     public final native JsArrayString external(String n) /*-{ return this.external[n]; }-*/;
-    private final native NativeMap<JsArrayString> external() /*-{ return this.external; }-*/;
+    private native NativeMap<JsArrayString> external() /*-{ return this.external; }-*/;
 
     protected IncludedInInfo() {
     }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java
index eee2847..183e6af 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java
@@ -39,8 +39,8 @@
   }
 
   public final native DownloadSchemeInfo scheme(String n) /*-{ return this.schemes[n]; }-*/;
-  private final native NativeMap<DownloadSchemeInfo> _schemes() /*-{ return this.schemes; }-*/;
-  private final native JsArrayString _archives() /*-{ return this.archives; }-*/;
+  private native NativeMap<DownloadSchemeInfo> _schemes() /*-{ return this.schemes; }-*/;
+  private native JsArrayString _archives() /*-{ return this.archives; }-*/;
 
   protected DownloadInfo() {
   }
@@ -96,8 +96,8 @@
     public final native boolean isAuthSupported() /*-{ return this.is_auth_supported || false; }-*/;
     public final native String command(String n) /*-{ return this.commands[n]; }-*/;
     public final native String cloneCommand(String n) /*-{ return this.clone_commands[n]; }-*/;
-    private final native NativeMap<NativeString> _commands() /*-{ return this.commands; }-*/;
-    private final native NativeMap<NativeString> _cloneCommands() /*-{ return this.clone_commands; }-*/;
+    private native NativeMap<NativeString> _commands() /*-{ return this.commands; }-*/;
+    private native NativeMap<NativeString> _cloneCommands() /*-{ return this.clone_commands; }-*/;
 
     protected DownloadSchemeInfo() {
     }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
index d95f9ef..9b290a5 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
@@ -34,10 +34,15 @@
   // JSNI methods cannot have 'long' as a parameter type or a return type and
   // it's suggested to use double in this case:
   // http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html#important
+  public final long size() {
+    return (long)_size();
+  }
+  private native double _size() /*-{ return this.size || 0; }-*/;
+
   public final long sizeDelta() {
     return (long)_sizeDelta();
   }
-  private final native double _sizeDelta() /*-{ return this.size_delta || 0; }-*/;
+  private native double _sizeDelta() /*-{ return this.size_delta || 0; }-*/;
 
   public final native int _row() /*-{ return this._row }-*/;
   public final native void _row(int r) /*-{ this._row = r }-*/;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
new file mode 100644
index 0000000..45953cb
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
@@ -0,0 +1,235 @@
+// 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.client.info;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DateFormat;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.ReviewCategoryStrategy;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.TimeFormat;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class GeneralPreferences extends JavaScriptObject {
+  public static GeneralPreferences create() {
+    return createObject().cast();
+  }
+
+  public static GeneralPreferences createDefault() {
+    GeneralPreferencesInfo d =
+        GeneralPreferencesInfo.defaults();
+    GeneralPreferences p = createObject().cast();
+    p.changesPerPage(d.changesPerPage);
+    p.showSiteHeader(d.showSiteHeader);
+    p.useFlashClipboard(d.useFlashClipboard);
+    p.downloadScheme(d.downloadScheme);
+    p.downloadCommand(d.downloadCommand);
+    p.dateFormat(d.getDateFormat());
+    p.timeFormat(d.getTimeFormat());
+    p.relativeDateInChangeTable(d.relativeDateInChangeTable);
+    p.sizeBarInChangeTable(d.sizeBarInChangeTable);
+    p.legacycidInChangeTable(d.legacycidInChangeTable);
+    p.muteCommonPathPrefixes(d.muteCommonPathPrefixes);
+    p.signedOffBy(d.signedOffBy);
+    p.reviewCategoryStrategy(d.getReviewCategoryStrategy());
+    p.diffView(d.getDiffView());
+    p.emailStrategy(d.emailStrategy);
+    return p;
+  }
+
+  public final int changesPerPage() {
+    int changesPerPage =
+        get("changes_per_page", GeneralPreferencesInfo.DEFAULT_PAGESIZE);
+    return 0 < changesPerPage
+        ? changesPerPage
+        : GeneralPreferencesInfo.DEFAULT_PAGESIZE;
+  }
+  private native short get(String n, int d)
+  /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
+
+  public final native boolean showSiteHeader()
+  /*-{ return this.show_site_header || false }-*/;
+
+  public final native boolean useFlashClipboard()
+  /*-{ return this.use_flash_clipboard || false }-*/;
+
+  public final native String downloadScheme()
+  /*-{ return this.download_scheme }-*/;
+
+  public final DownloadCommand downloadCommand() {
+    String s = downloadCommandRaw();
+    return s != null ? DownloadCommand.valueOf(s) : null;
+  }
+  private native String downloadCommandRaw()
+  /*-{ return this.download_command }-*/;
+
+  public final DateFormat dateFormat() {
+    String s = dateFormatRaw();
+    return s != null ? DateFormat.valueOf(s) : null;
+  }
+  private native String dateFormatRaw()
+  /*-{ return this.date_format }-*/;
+
+  public final TimeFormat timeFormat() {
+    String s = timeFormatRaw();
+    return s != null ? TimeFormat.valueOf(s) : null;
+  }
+  private native String timeFormatRaw()
+  /*-{ return this.time_format }-*/;
+
+  public final native boolean relativeDateInChangeTable()
+  /*-{ return this.relative_date_in_change_table || false }-*/;
+
+  public final native boolean sizeBarInChangeTable()
+  /*-{ return this.size_bar_in_change_table || false }-*/;
+
+  public final native boolean legacycidInChangeTable()
+  /*-{ return this.legacycid_in_change_table || false }-*/;
+
+  public final native boolean muteCommonPathPrefixes()
+  /*-{ return this.mute_common_path_prefixes || false }-*/;
+
+  public final native boolean signedOffBy()
+  /*-{ return this.signed_off_by || false }-*/;
+
+  public final ReviewCategoryStrategy reviewCategoryStrategy() {
+    String s = reviewCategeoryStrategyRaw();
+    return s != null ? ReviewCategoryStrategy.valueOf(s) : ReviewCategoryStrategy.NONE;
+  }
+  private native String reviewCategeoryStrategyRaw()
+  /*-{ return this.review_category_strategy }-*/;
+
+  public final DiffView diffView() {
+    String s = diffViewRaw();
+    return s != null ? DiffView.valueOf(s) : null;
+  }
+  private native String diffViewRaw()
+  /*-{ return this.diff_view }-*/;
+
+  public final EmailStrategy emailStrategy() {
+    String s = emailStrategyRaw();
+    return s != null ? EmailStrategy.valueOf(s) : null;
+  }
+
+  private native String emailStrategyRaw()
+  /*-{ return this.email_strategy }-*/;
+
+  public final native JsArray<TopMenuItem> my()
+  /*-{ return this.my; }-*/;
+
+  public final native void changesPerPage(int n)
+  /*-{ this.changes_per_page = n }-*/;
+
+  public final native void showSiteHeader(boolean s)
+  /*-{ this.show_site_header = s }-*/;
+
+  public final native void useFlashClipboard(boolean u)
+  /*-{ this.use_flash_clipboard = u }-*/;
+
+  public final native void downloadScheme(String d)
+  /*-{ this.download_scheme = d }-*/;
+
+  public final void downloadCommand(DownloadCommand d) {
+    downloadCommandRaw(d != null ? d.toString() : null);
+  }
+  public final native void downloadCommandRaw(String d)
+  /*-{ this.download_command = d }-*/;
+
+  public final void dateFormat(DateFormat f) {
+    dateFormatRaw(f != null ? f.toString() : null);
+  }
+  private native void dateFormatRaw(String f)
+  /*-{ this.date_format = f }-*/;
+
+  public final void timeFormat(TimeFormat f) {
+    timeFormatRaw(f != null ? f.toString() : null);
+  }
+  private native void timeFormatRaw(String f)
+  /*-{ this.time_format = f }-*/;
+
+  public final native void relativeDateInChangeTable(boolean d)
+  /*-{ this.relative_date_in_change_table = d }-*/;
+
+  public final native void sizeBarInChangeTable(boolean s)
+  /*-{ this.size_bar_in_change_table = s }-*/;
+
+  public final native void legacycidInChangeTable(boolean s)
+  /*-{ this.legacycid_in_change_table = s }-*/;
+
+  public final native void muteCommonPathPrefixes(boolean s)
+  /*-{ this.mute_common_path_prefixes = s }-*/;
+
+  public final native void signedOffBy(boolean s)
+  /*-{ this.signed_off_by = s }-*/;
+
+  public final void reviewCategoryStrategy(ReviewCategoryStrategy s) {
+    reviewCategoryStrategyRaw(s != null ? s.toString() : null);
+  }
+  private native void reviewCategoryStrategyRaw(String s)
+  /*-{ this.review_category_strategy = s }-*/;
+
+  public final void diffView(DiffView d) {
+    diffViewRaw(d != null ? d.toString() : null);
+  }
+  private native void diffViewRaw(String d)
+  /*-{ this.diff_view = d }-*/;
+
+  public final void emailStrategy(EmailStrategy s) {
+    emailStrategyRaw(s != null ? s.toString() : null);
+  }
+  private native void emailStrategyRaw(String s)
+  /*-{ this.email_strategy = s }-*/;
+
+  public final void setMyMenus(List<TopMenuItem> myMenus) {
+    initMy();
+    for (TopMenuItem n : myMenus) {
+      addMy(n);
+    }
+  }
+  final native void initMy() /*-{ this.my = []; }-*/;
+  final native void addMy(TopMenuItem m) /*-{ this.my.push(m); }-*/;
+
+  public final Map<String, String> urlAliases() {
+    Map<String, String> urlAliases = new HashMap<>();
+    for (String k : Natives.keys(_urlAliases())) {
+      urlAliases.put(k, urlAliasToken(k));
+    }
+    return urlAliases;
+  }
+
+  private native String urlAliasToken(String m) /*-{ return this.url_aliases[m]; }-*/;
+  private native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
+
+  public final void setUrlAliases(Map<String, String> urlAliases) {
+    initUrlAliases();
+    for (Map.Entry<String, String> e : urlAliases.entrySet()) {
+      putUrlAlias(e.getKey(), e.getValue());
+    }
+  }
+  private native void putUrlAlias(String m, String t) /*-{ this.url_aliases[m] = t; }-*/;
+  private native void initUrlAliases() /*-{ this.url_aliases = {}; }-*/;
+
+  protected GeneralPreferences() {
+  }
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
index 55ef892..750412d 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
@@ -36,6 +36,7 @@
 
   public final native String allProjects() /*-{ return this.all_projects; }-*/;
   public final native String allUsers() /*-{ return this.all_users; }-*/;
+  public final native boolean docSearch() /*-{ return this.doc_search; }-*/;
   public final native String docUrl() /*-{ return this.doc_url; }-*/;
   public final native boolean editGpgKeys() /*-{ return this.edit_gpg_keys || false; }-*/;
   public final native String reportBugUrl() /*-{ return this.report_bug_url; }-*/;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebInfo.java
deleted file mode 100644
index eb5a697..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebInfo.java
+++ /dev/null
@@ -1,169 +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.client.info;
-
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.common.data.ParameterizedString;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.http.client.URL;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class GitwebInfo extends JavaScriptObject {
-  public final native String url() /*-{ return this.url; }-*/;
-  public final native GitwebTypeInfo type() /*-{ return this.type; }-*/;
-
-  /**
-   * Checks whether the given patch set can be linked.
-   *
-   * Draft patch sets can only be linked if linking of drafts was enabled by
-   * configuration.
-   *
-   * @param ps patch set to check whether it can be linked
-   * @return true if the patch set can be linked, otherwise false
-   */
-  public final boolean canLink(PatchSet ps) {
-    return !ps.isDraft() || type().linkDrafts();
-  }
-
-  /**
-   * Checks whether the given revision can be linked.
-   *
-   * Draft revisions can only be linked if linking of drafts was enabled by
-   * configuration.
-   *
-   * @param revision revision to check whether it can be linked
-   * @return true if the revision can be linked, otherwise false
-   */
-  public final boolean canLink(RevisionInfo revision) {
-    return revision.draft() || type().linkDrafts();
-  }
-
-  /**
-   * Returns the name for gitweb links.
-   *
-   * @return the name for gitweb links
-   */
-  public final String getLinkName() {
-    return "(" + type().name() + ")";
-  }
-
-  /**
-   * Returns the gitweb link to a revision.
-   *
-   * @param project the name of the project
-   * @param commit the commit ID
-   * @return gitweb link to a revision
-   */
-  public final String toRevision(String  project, String commit) {
-    ParameterizedString pattern = new ParameterizedString(type().revision());
-    Map<String, String> p = new HashMap<>();
-    p.put("project", encode(project));
-    p.put("commit", encode(commit));
-    return url() + pattern.replace(p);
-  }
-
-  /**
-   * Returns the gitweb link to a revision.
-   *
-   * @param project the name of the project
-   * @param ps the patch set
-   * @return gitweb link to a revision
-   */
-  public final String toRevision(Project.NameKey project, PatchSet ps) {
-    return toRevision(project.get(), ps.getRevision().get());
-  }
-
-  /**
-   * Returns the gitweb link to a project.
-   *
-   * @param project the project name key
-   * @return gitweb link to a project
-   */
-  public final String toProject(Project.NameKey project) {
-    ParameterizedString pattern = new ParameterizedString(type().project());
-
-    Map<String, String> p = new HashMap<>();
-    p.put("project", encode(project.get()));
-    return url() + pattern.replace(p);
-  }
-
-  /**
-   * Returns the gitweb link to a branch.
-   *
-   * @param branch the branch name key
-   * @return gitweb link to a branch
-   */
-  public final String toBranch(Branch.NameKey branch) {
-    ParameterizedString pattern = new ParameterizedString(type().branch());
-
-    Map<String, String> p = new HashMap<>();
-    p.put("project", encode(branch.getParentKey().get()));
-    p.put("branch", encode(branch.get()));
-    return url() + pattern.replace(p);
-  }
-
-  /**
-   * Returns the gitweb link to a file.
-   *
-   * @param project the branch name key
-   * @param commit the commit ID
-   * @param file the path of the file
-   * @return gitweb link to a file
-   */
-  public final String toFile(String  project, String commit, String file) {
-    Map<String, String> p = new HashMap<>();
-    p.put("project", encode(project));
-    p.put("commit", encode(commit));
-    p.put("file", encode(file));
-
-    ParameterizedString pattern = (file == null || file.isEmpty())
-        ? new ParameterizedString(type().rootTree())
-        : new ParameterizedString(type().file());
-    return url() + pattern.replace(p);
-  }
-
-  /**
-   * Returns the gitweb link to a file history.
-   *
-   * @param branch the branch name key
-   * @param file the path of the file
-   * @return gitweb link to a file history
-   */
-  public final String toFileHistory(Branch.NameKey branch, String file) {
-    ParameterizedString pattern = new ParameterizedString(type().fileHistory());
-
-    Map<String, String> p = new HashMap<>();
-    p.put("project", encode(branch.getParentKey().get()));
-    p.put("branch", encode(branch.get()));
-    p.put("file", encode(file));
-    return url() + pattern.replace(p);
-  }
-
-  private final String encode(String segment) {
-    if (type().urlEncode()) {
-      return URL.encodeQueryString(type().replacePathSeparator(segment));
-    } else {
-      return segment;
-    }
-  }
-
-  protected GitwebInfo() {
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebTypeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebTypeInfo.java
deleted file mode 100644
index 6726719..0000000
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebTypeInfo.java
+++ /dev/null
@@ -1,48 +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.client.info;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class GitwebTypeInfo extends JavaScriptObject {
-  /**
-   * Replace the standard path separator ('/') in a branch name or project
-   * name with a custom path separator configured by the property
-   * gitweb.pathSeparator.
-   * @param urlSegment The branch or project to replace the path separator in
-   * @return the urlSegment with the standard path separator replaced by the
-   * custom path separator
-   */
-  public final String replacePathSeparator(String urlSegment) {
-    if (!"/".equals(pathSeparator())) {
-      return urlSegment.replace("/", pathSeparator());
-    }
-    return urlSegment;
-  }
-
-  public final native String name() /*-{ return this.name; }-*/;
-  public final native String revision() /*-{ return this.revision; }-*/;
-  public final native String project() /*-{ return this.project; }-*/;
-  public final native String branch() /*-{ return this.branch; }-*/;
-  public final native String rootTree() /*-{ return this.root_tree; }-*/;
-  public final native String file() /*-{ return this.file; }-*/;
-  public final native String fileHistory() /*-{ return this.file_history; }-*/;
-  public final native String pathSeparator() /*-{ return this.path_separator; }-*/;
-  public final native boolean linkDrafts() /*-{ return this.link_drafts || false; }-*/;
-  public final native boolean urlEncode() /*-{ return this.url_encode || false; }-*/;
-
-  protected GitwebTypeInfo() {
-  }
-}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
index f7477a1..7955347 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
@@ -27,7 +27,7 @@
   public final native JsArrayString userIds() /*-{ return this.user_ids; }-*/;
   public final native String key() /*-{ return this.key; }-*/;
 
-  private final native String statusRaw() /*-{ return this.status; }-*/;
+  private native String statusRaw() /*-{ return this.status; }-*/;
   public final Status status() {
     String s = statusRaw();
     if (s == null) {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/OAuthTokenInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/OAuthTokenInfo.java
new file mode 100644
index 0000000..08fd130
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/OAuthTokenInfo.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.client.info;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+
+public class OAuthTokenInfo extends JavaScriptObject {
+
+  protected OAuthTokenInfo() {
+  }
+
+  public final native String username() /*-{ return this.username; }-*/;
+  public final native String resourceHost() /*-{ return this.resource_host; }-*/;
+  public final native String accessToken() /*-{ return this.access_token; }-*/;
+  public final native String providerId() /*-{ return this.provider_id; }-*/;
+  public final native String expiresAt() /*-{ return this.expires_at; }-*/;
+  public final native String type() /*-{ return this.type; }-*/;
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
index b0e52fa..112c4db 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -27,7 +28,6 @@
   public final native ChangeConfigInfo change() /*-{ return this.change; }-*/;
   public final native DownloadInfo download() /*-{ return this.download; }-*/;
   public final native GerritInfo gerrit() /*-{ return this.gerrit; }-*/;
-  public final native GitwebInfo gitweb() /*-{ return this.gitweb; }-*/;
   public final native PluginConfigInfo plugin() /*-{ return this.plugin; }-*/;
   public final native SshdInfo sshd() /*-{ return this.sshd; }-*/;
   public final native SuggestInfo suggest() /*-{ return this.suggest; }-*/;
@@ -43,7 +43,7 @@
   }
 
   public final native String urlAliasToken(String n) /*-{ return this.url_aliases[n]; }-*/;
-  private final native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
+  private native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
 
 
   public final boolean hasSshd() {
@@ -55,6 +55,7 @@
 
   public static class ChangeConfigInfo extends JavaScriptObject {
     public final native boolean allowDrafts() /*-{ return this.allow_drafts || false; }-*/;
+    public final native boolean allowBlame() /*-{ return this.allow_blame || false; }-*/;
     public final native int largeChange() /*-{ return this.large_change || 0; }-*/;
     public final native String replyLabel() /*-{ return this.reply_label; }-*/;
     public final native String replyTooltip() /*-{ return this.reply_tooltip; }-*/;
@@ -68,6 +69,8 @@
 
   public static class PluginConfigInfo extends JavaScriptObject {
     public final native boolean hasAvatars() /*-{ return this.has_avatars || false; }-*/;
+    public final native JsArrayString jsResourcePaths() /*-{
+        return this.js_resource_paths || []; }-*/;
 
     protected PluginConfigInfo() {
     }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java
index 0e16dc0..572d454 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java
@@ -22,7 +22,7 @@
   public static final JavaScriptObject TYPE = init();
 
   // Used from core and plugins
-  private static final native JavaScriptObject init() /*-{
+  private static native JavaScriptObject init() /*-{
     if ($wnd.Gerrit === undefined || $wnd.Gerrit.JsonString === undefined) {
       return function(s){this.s=s};
     } else {
@@ -30,16 +30,16 @@
     }
   }-*/;
 
-  static final NativeString wrap(String s) {
+  static NativeString wrap(String s) {
     return wrap0(TYPE, s);
   }
 
-  private static final native NativeString wrap0(JavaScriptObject T, String s)
+  private static native NativeString wrap0(JavaScriptObject T, String s)
   /*-{ return new T(s) }-*/;
 
-  public final native String asString() /*-{ return this.s; }-*/;
+  public native String asString() /*-{ return this.s; }-*/;
 
-  public static final AsyncCallback<NativeString>
+  public static AsyncCallback<NativeString>
   unwrap(final AsyncCallback<String> cb) {
     return new AsyncCallback<NativeString>() {
       @Override
@@ -54,11 +54,11 @@
     };
   }
 
-  public static final boolean is(JavaScriptObject o) {
+  public static boolean is(JavaScriptObject o) {
     return is(TYPE, o);
   }
 
-  private static final native boolean is(JavaScriptObject T, JavaScriptObject o)
+  private static native boolean is(JavaScriptObject T, JavaScriptObject o)
   /*-{ return o instanceof T }-*/;
 
   protected NativeString() {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
index 93c86a2..00e6bb1 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
@@ -17,7 +17,7 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 /** Transforms a value and passes it on to another callback. */
-public abstract class TransformCallback<I, O> implements AsyncCallback<I>{
+public abstract class TransformCallback<I, O> implements AsyncCallback<I> {
   private final AsyncCallback<O> callback;
 
   protected TransformCallback(AsyncCallback<O> callback) {
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/addFileComment.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/addFileComment.png
deleted file mode 100644
index 4ae3ae8..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/addFileComment.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowDown.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowDown.png
deleted file mode 100644
index ba67de7..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowDown.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowRight.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowRight.png
deleted file mode 100644
index 8549f5d..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowRight.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowUp.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowUp.png
deleted file mode 100644
index 5674d6c..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowUp.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrow_undo.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrow_undo.png
new file mode 100644
index 0000000..6972c5e
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrow_undo.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/cog.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/cog.png
new file mode 100644
index 0000000..67de2c6
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/cog.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/comment_draft.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/comment_draft.png
new file mode 100644
index 0000000..3408ddf
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/comment_draft.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/cross.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/cross.png
new file mode 100644
index 0000000..1514d51
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/cross.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/deleteHover.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/deleteHover.png
deleted file mode 100644
index 9fde3fa..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/deleteHover.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/deleteNormal.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/deleteNormal.png
deleted file mode 100644
index 47a1195..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/deleteNormal.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/diffy100.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/diffy100.png
deleted file mode 100644
index 4be4541..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/diffy100.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/disk.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/disk.png
new file mode 100644
index 0000000..99d532e
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/disk.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/downloadIcon.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/downloadIcon.png
deleted file mode 100644
index 1a6520e..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/downloadIcon.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/draftComments.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/draftComments.png
deleted file mode 100644
index 276912a..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/draftComments.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/editText.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/editText.png
deleted file mode 100644
index 2927275..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/editText.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/editUndo.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/editUndo.png
deleted file mode 100644
index 4790e10..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/editUndo.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/exclamation.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/exclamation.png
new file mode 100644
index 0000000..c37bd06
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/exclamation.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/find.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/find.png
new file mode 100644
index 0000000..1547479
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/find.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/gear.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/gear.png
deleted file mode 100644
index 2f84e47..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/gear.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goDown.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goDown.png
new file mode 100644
index 0000000..5d87e45
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goDown.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/greenCheck.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/greenCheck.png
deleted file mode 100644
index 207c0e7..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/greenCheck.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/help.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/help.png
new file mode 100644
index 0000000..5c87017
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/help.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/info.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/info.png
deleted file mode 100644
index 8851b99..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/info.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/lightbulb.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/lightbulb.png
new file mode 100644
index 0000000..d22fde8
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/lightbulb.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/lock.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/lock.png
new file mode 100644
index 0000000..2ebc4f6
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/lock.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/mediaFloppy.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/mediaFloppy.png
deleted file mode 100644
index f1d7a19..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/mediaFloppy.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/note_add.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/note_add.png
new file mode 100644
index 0000000..abdad91
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/note_add.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/page_edit.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/page_edit.png
new file mode 100644
index 0000000..046811e
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/page_edit.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/page_white_put.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/page_white_put.png
new file mode 100644
index 0000000..884ffd6
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/page_white_put.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/queryIcon.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/queryIcon.png
deleted file mode 100644
index 5ebf2cb..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/queryIcon.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/question.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/question.png
deleted file mode 100644
index f25fc3f..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/question.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/readOnly.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/readOnly.png
deleted file mode 100644
index 62e89f9..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/readOnly.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/redNot.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/redNot.png
deleted file mode 100644
index 99834fd..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/redNot.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_down_gray.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_down_gray.png
new file mode 100644
index 0000000..7bdd8ea
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_down_gray.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_next_gray.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_next_gray.png
new file mode 100644
index 0000000..3049ef4
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_next_gray.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_up_gray.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_up_gray.png
new file mode 100644
index 0000000..966a25e
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/resultset_up_gray.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/star-open.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/star-open.png
new file mode 100644
index 0000000..edd577c
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/star-open.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/star.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/star.png
new file mode 100644
index 0000000..b88c857
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/star.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/starFilled.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/starFilled.png
deleted file mode 100644
index db1e24e..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/starFilled.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/starOpen.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/starOpen.png
deleted file mode 100644
index 6c955de..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/starOpen.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tag_blue.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tag_blue.png
new file mode 100644
index 0000000..9757fc6
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tag_blue.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tag_blue_add.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tag_blue_add.png
new file mode 100644
index 0000000..f135248
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tag_blue_add.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tick.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tick.png
new file mode 100644
index 0000000..a9925a0
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/tick.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/undoNormal.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/undoNormal.png
deleted file mode 100644
index b780f75..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/undoNormal.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/user_add.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/user_add.png
new file mode 100644
index 0000000..deae99b
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/user_add.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/warning.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/warning.png
deleted file mode 100644
index 81e9ed2..0000000
--- a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/warning.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/BUCK b/gerrit-gwtui/BUCK
index ead19f4..1e39831 100644
--- a/gerrit-gwtui/BUCK
+++ b/gerrit-gwtui/BUCK
@@ -19,40 +19,40 @@
   visibility = ['//:'],
 )
 
-gwt_module(
-  name = 'ui_module',
-  srcs = glob(['src/main/java/**/*.java']),
-  gwt_xml = 'src/main/java/%s.gwt.xml' % MODULE.replace('.', '/'),
-  resources = glob(['src/main/java/**/*']),
-  deps = [
-    ':freebie_application_icon_set',
-    '//gerrit-gwtui-common:diffy_logo',
-    '//gerrit-gwtui-common:client',
-    '//gerrit-gwtexpui:CSS',
-    '//lib/codemirror:codemirror',
-    '//lib/gwt:user',
-  ],
-  visibility = [
-    '//tools/eclipse:classpath',
-    '//Documentation:licenses.txt',
-    '//Documentation:js_licenses.txt',
-  ],
-)
+def gen_ui_module(name, suffix = ""):
+  gwt_module(
+    name = name + suffix,
+    srcs = glob(['src/main/java/**/*.java']),
+    gwt_xml = 'src/main/java/%s.gwt.xml' % MODULE.replace('.', '/'),
+    resources = glob(['src/main/java/**/*']),
+    deps = [
+      ':silk_icons',
+      '//gerrit-gwtui-common:diffy_logo',
+      '//gerrit-gwtui-common:client',
+      '//gerrit-gwtexpui:CSS',
+      '//lib/codemirror:codemirror' + suffix,
+      '//lib/gwt:user',
+    ],
+    visibility = [
+      '//tools/eclipse:classpath',
+      '//Documentation:licenses.txt',
+      '//Documentation:js_licenses.txt',
+    ],
+  )
+
+gen_ui_module(name = 'ui_module')
+gen_ui_module(name = 'ui_module', suffix = '_r')
 
 java_library(
-  name = 'freebie_application_icon_set',
+  name = 'silk_icons',
   deps = [
-    '//lib:LICENSE-freebie_application_icon_set',
-    '//lib:LICENSE-CC-BY3.0',
+    '//lib:LICENSE-silk_icons',
   ],
 )
 
 java_test(
   name = 'ui_tests',
   srcs = glob(['src/test/java/**/*.java']),
-  resources = glob(['src/test/resources/**/*']) + [
-    'src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml',
-  ],
   deps = [
     ':ui_module',
     '//gerrit-common:client',
@@ -60,7 +60,6 @@
     '//lib:junit',
     '//lib/gwt:dev',
     '//lib/gwt:user',
-    '//lib/gwt:gwt-test-utils',
   ],
   source_under_test = [':ui_module'],
   vm_args = ['-Xmx512m'],
diff --git a/gerrit-gwtui/gwt.defs b/gerrit-gwtui/gwt.defs
index 783c343..cd8fa74 100644
--- a/gerrit-gwtui/gwt.defs
+++ b/gerrit-gwtui/gwt.defs
@@ -18,12 +18,14 @@
   'firefox',
   'gecko1_8',
   'safari',
-  'msie', 'ie8', 'ie9',
+  'msie', 'ie8', 'ie9', 'ie10', 'ie11',
+  'edge',
 ]
 ALIASES = {
   'chrome': 'safari',
   'firefox': 'gecko1_8',
-  'msie': 'ie9',
+  'msie': 'ie11',
+  'edge': 'edge',
 }
 MODULE = 'com.google.gerrit.GerritGwtUI'
 CPU_COUNT = cpu_count()
@@ -32,6 +34,7 @@
   dbg = 'ui_dbg' + suffix
   opt = 'ui_opt' + suffix
   soyc = 'ui_soyc' + suffix
+  module_dep = ':ui_module' + suffix
   args = GWT_COMPILER_ARGS_RELEASE_MODE if suffix == "_r" else GWT_COMPILER_ARGS
 
   genrule(
@@ -51,7 +54,7 @@
   gwt_binary(
     name = opt,
     modules = [module],
-    module_deps = [':ui_module'],
+    module_deps = [module_dep],
     deps = deps + ([':' + dbg] if CPU_COUNT < 8 else []),
     local_workers = CPU_COUNT,
     strict = True,
@@ -64,19 +67,19 @@
     modules = [module],
     style = 'PRETTY',
     optimize = 0,
-    module_deps = [':ui_module'],
+    module_deps = [module_dep],
     deps = deps,
     local_workers = CPU_COUNT,
     strict = True,
     experimental_args = args,
     vm_args = GWT_JVM_ARGS,
-    visibility = ['//:eclipse'],
+    visibility = ['PUBLIC'],
   )
 
   gwt_binary(
     name = soyc,
     modules = [module],
-    module_deps = [':ui_module'],
+    module_deps = [module_dep],
     deps = deps + [':' + dbg],
     local_workers = CPU_COUNT,
     strict = True,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
index c02518b..9644093 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
@@ -34,6 +34,8 @@
       <when-property-is name="user.agent" value="ie8"/>
       <when-property-is name="user.agent" value="ie9"/>
       <when-property-is name="user.agent" value="ie10"/>
+      <when-property-is name="user.agent" value="ie11"/>
+      <when-property-is name="user.agent" value="edge"/>
     </any>
   </replace-with>
 </module>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
index bec89cc..4ca1721 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -46,18 +46,6 @@
    * @param size A requested size. Note that the size can be ignored depending
    *        on the avatar provider. A size <= 0 indicates to let the provider
    *        decide a default size.
-   */
-  public AvatarImage(AccountInfo account, int size) {
-    this(account, size, true);
-  }
-
-  /**
-   * An avatar image for the given account using the requested size.
-   *
-   * @param account The account in which we are interested
-   * @param size A requested size. Note that the size can be ignored depending
-   *        on the avatar provider. A size <= 0 indicates to let the provider
-   *        decide a default size.
    * @param addPopup show avatar popup with user info on hovering over the
    *        avatar image
    */
@@ -138,7 +126,7 @@
     private Timer showTimer;
     private Timer hideTimer;
 
-    public PopupHandler(AccountInfo account, UIObject target) {
+    PopupHandler(AccountInfo account, UIObject target) {
       this.account = account;
       this.target = target;
     }
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 ba3cc4c..ba4b202 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
@@ -35,6 +35,7 @@
 import static com.google.gerrit.common.PageLinks.SETTINGS_HTTP_PASSWORD;
 import static com.google.gerrit.common.PageLinks.SETTINGS_MYGROUPS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_NEW_AGREEMENT;
+import static com.google.gerrit.common.PageLinks.SETTINGS_OAUTH_TOKEN;
 import static com.google.gerrit.common.PageLinks.SETTINGS_PREFERENCES;
 import static com.google.gerrit.common.PageLinks.SETTINGS_PROJECTS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_SSHKEYS;
@@ -48,6 +49,7 @@
 import com.google.gerrit.client.account.MyGpgKeysScreen;
 import com.google.gerrit.client.account.MyGroupsScreen;
 import com.google.gerrit.client.account.MyIdentitiesScreen;
+import com.google.gerrit.client.account.MyOAuthTokenScreen;
 import com.google.gerrit.client.account.MyPasswordScreen;
 import com.google.gerrit.client.account.MyPreferencesScreen;
 import com.google.gerrit.client.account.MyProfileScreen;
@@ -83,17 +85,17 @@
 import com.google.gerrit.client.dashboards.DashboardList;
 import com.google.gerrit.client.diff.DisplaySide;
 import com.google.gerrit.client.diff.SideBySide;
+import com.google.gerrit.client.diff.Unified;
 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.patches.UnifiedPatchScreen;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -102,23 +104,29 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.RunAsyncCallback;
 import com.google.gwt.http.client.URL;
+import com.google.gwtexpui.user.client.UserAgent;
 import com.google.gwtorm.client.KeyUtil;
 
 public class Dispatcher {
-  public static String toSideBySide(PatchSet.Id diffBase, Patch.Key id) {
-    return toPatch("", diffBase, id);
-  }
-
-  public static String toSideBySide(PatchSet.Id diffBase,
+  public static String toPatch(PatchSet.Id diffBase,
       PatchSet.Id revision, String fileName) {
     return toPatch("", diffBase, revision, fileName, null, 0);
   }
 
-  public static String toSideBySide(PatchSet.Id diffBase,
+  public static String toPatch(PatchSet.Id diffBase,
       PatchSet.Id revision, String fileName, DisplaySide side, int line) {
     return toPatch("", diffBase, revision, fileName, side, line);
   }
 
+  public static String toSideBySide(PatchSet.Id diffBase, Patch.Key id) {
+    return toPatch("sidebyside", diffBase, id);
+  }
+
+  public static String toSideBySide(PatchSet.Id diffBase,
+      PatchSet.Id revision, String fileName) {
+    return toPatch("sidebyside", diffBase, revision, fileName, null, 0);
+  }
+
   public static String toUnified(PatchSet.Id diffBase,
       PatchSet.Id revision, String fileName) {
     return toPatch("unified", diffBase, revision, fileName, null, 0);
@@ -277,12 +285,10 @@
   private static Screen mine() {
     if (Gerrit.isSignedIn()) {
       return new AccountDashboardScreen(Gerrit.getUserAccount().getId());
-
-    } else {
-      Screen r = new AccountDashboardScreen(null);
-      r.setRequiresSignIn(true);
-      return r;
     }
+    Screen r = new AccountDashboardScreen(null);
+    r.setRequiresSignIn(true);
+    return r;
   }
 
   private static void dashboard(final String token) {
@@ -418,7 +424,7 @@
       int line = 0;
       int at = rest.lastIndexOf('@');
       if (at > 0) {
-        String l = rest.substring(at+1);
+        String l = rest.substring(at + 1);
         if (l.startsWith("a")) {
           side = DisplaySide.A;
           l = l.substring(1);
@@ -471,14 +477,14 @@
 
     if ("".equals(panel) || /* DEPRECATED URL */"cm".equals(panel)) {
       if (preferUnified()) {
-        unified(token, baseId, id);
+        unified(token, baseId, id, side, line);
       } else {
         codemirror(token, baseId, id, side, line, false);
       }
     } else if ("sidebyside".equals(panel)) {
-      codemirror(token, null, id, side, line, false);
+      codemirror(token, baseId, id, side, line, false);
     } else if ("unified".equals(panel)) {
-      unified(token, baseId, id);
+      unified(token, baseId, id, side, line);
     } else if ("edit".equals(panel)) {
       codemirror(token, null, id, side, line, true);
     } else {
@@ -487,17 +493,17 @@
   }
 
   private static boolean preferUnified() {
-    return DiffView.UNIFIED_DIFF.equals(Gerrit.getUserPreferences().diffView());
+    return DiffView.UNIFIED_DIFF.equals(Gerrit.getUserPreferences().diffView())
+        || (UserAgent.isPortrait() && UserAgent.isMobile());
   }
 
-  private static void unified(final String token,
-      final PatchSet.Id baseId,
-      final Patch.Key id) {
+  private static void unified(final String token, final PatchSet.Id baseId,
+      final Patch.Key id, final DisplaySide side, final int line) {
     GWT.runAsync(new AsyncSplit(token) {
       @Override
       public void onSuccess() {
-        UnifiedPatchScreen.TopView top = Gerrit.getPatchScreenTopView();
-        Gerrit.display(token, new UnifiedPatchScreen(id, top, baseId));
+        Gerrit.display(token,
+            new Unified(baseId, id.getParentKey(), id.get(), side, line));
       }
     });
   }
@@ -564,6 +570,12 @@
           return new MyPasswordScreen();
         }
 
+        if (matchExact(SETTINGS_OAUTH_TOKEN, token)
+            && Gerrit.info().auth().isOAuth()
+            && Gerrit.info().auth().isGitBasicAuth()) {
+          return new MyOAuthTokenScreen();
+        }
+
         if (matchExact(MY_GROUPS, token)
             || matchExact(SETTINGS_MYGROUPS, token)) {
           return new MyGroupsScreen();
@@ -599,9 +611,8 @@
               new ExtensionSettingsScreen(skip(token));
           if (view.isFound()) {
             return view;
-          } else {
-            return new NotFoundScreen();
           }
+          return new NotFoundScreen();
         }
 
         return new NotFoundScreen();
@@ -706,11 +717,15 @@
               // shown in the web UI).
               //
               if (AccountGroup.isInternalGroup(group.getGroupUUID())) {
-                Gerrit.display(toGroup(group.getGroupId(), AccountGroupScreen.MEMBERS),
-                    new AccountGroupMembersScreen(group, token));
+                String newToken =
+                    toGroup(group.getGroupId(), AccountGroupScreen.MEMBERS);
+                Gerrit.display(newToken,
+                    new AccountGroupMembersScreen(group, newToken));
               } else {
-                Gerrit.display(toGroup(group.getGroupId(), AccountGroupScreen.INFO),
-                    new AccountGroupInfoScreen(group, token));
+                String newToken =
+                    toGroup(group.getGroupId(), AccountGroupScreen.INFO);
+                Gerrit.display(newToken,
+                    new AccountGroupInfoScreen(group, newToken));
               }
             } else if (AccountGroupScreen.INFO.equals(panel)) {
               Gerrit.display(token, new AccountGroupInfoScreen(group, token));
@@ -780,9 +795,8 @@
     if (token.startsWith(want)) {
       prefixlen = want.length();
       return true;
-    } else {
-      return false;
     }
+    return false;
   }
 
   private static String skip(String token) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
index a5c5659..87cb4b3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
@@ -27,7 +27,6 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
 
@@ -96,11 +95,6 @@
     body.add(message.toBlockWidget());
   }
 
-  public ErrorDialog(final Widget w) {
-    this();
-    body.add(w);
-  }
-
   /** Create a dialog box to nicely format an exception. */
   public ErrorDialog(final Throwable what) {
     this();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index c77b71f..dd1505c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.client;
 
+import com.google.gerrit.client.change.Resources;
 import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gerrit.client.info.GeneralPreferences;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.i18n.client.NumberFormat;
 
@@ -25,7 +26,7 @@
 public class FormatUtil {
   private static DateFormatter dateFormatter;
 
-  public static void setPreferences(AccountPreferencesInfo prefs) {
+  public static void setPreferences(GeneralPreferences prefs) {
     dateFormatter = new DateFormatter(prefs);
   }
 
@@ -134,4 +135,19 @@
         + NumberFormat.getFormat("#.0").format(bytes / Math.pow(1024, exp))
         + " " + "KMGTPE".charAt(exp - 1) + "iB";
   }
+
+  public static String formatPercentage(long size, long delta) {
+    if (size == 0) {
+      return Resources.C.notAvailable();
+    }
+    return (delta > 0 ? "+" : "-") + formatAbsPercentage(size, delta);
+  }
+
+  public static String formatAbsPercentage(long size, long delta) {
+    if (size == 0) {
+      return Resources.C.notAvailable();
+    }
+    int p = Math.abs(Math.round(delta * 100 / size));
+    return p + "%";
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index 560fa9e..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
@@ -25,18 +25,17 @@
 import com.google.gerrit.client.admin.ProjectScreen;
 import com.google.gerrit.client.api.ApiGlue;
 import com.google.gerrit.client.api.PluginLoader;
-import com.google.gerrit.client.changes.ChangeConstants;
+import com.google.gerrit.client.change.LocalComments;
 import com.google.gerrit.client.changes.ChangeListScreen;
 import com.google.gerrit.client.config.ConfigServerApi;
 import com.google.gerrit.client.documentation.DocInfo;
 import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.AccountPreferencesInfo;
 import com.google.gerrit.client.info.AuthInfo;
+import com.google.gerrit.client.info.GeneralPreferences;
 import com.google.gerrit.client.info.ServerInfo;
 import com.google.gerrit.client.info.TopMenu;
 import com.google.gerrit.client.info.TopMenuItem;
 import com.google.gerrit.client.info.TopMenuList;
-import com.google.gerrit.client.patches.UnifiedPatchScreen;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
@@ -55,7 +54,6 @@
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.AnchorElement;
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -101,7 +99,6 @@
 
 public class Gerrit implements EntryPoint {
   public static final GerritConstants C = GWT.create(GerritConstants.class);
-  public static final ChangeConstants CC = GWT.create(ChangeConstants.class);
   public static final GerritMessages M = GWT.create(GerritMessages.class);
   public static final GerritResources RESOURCES =
       GWT.create(GerritResources.class);
@@ -114,9 +111,10 @@
   private static String myHost;
   private static ServerInfo myServerInfo;
   private static AccountInfo myAccount;
-  private static AccountPreferencesInfo myPrefs;
+  private static GeneralPreferences myPrefs;
   private static UrlAliasMatcher urlAliasMatcher;
   private static boolean hasDocumentation;
+  private static boolean docSearch;
   private static String docUrl;
   private static HostPageData.Theme myTheme;
   private static String defaultScreenToken;
@@ -136,7 +134,6 @@
   private static SearchPanel searchPanel;
   private static final Dispatcher dispatcher = new Dispatcher();
   private static ViewSite<Screen> body;
-  private static UnifiedPatchScreen patchScreen;
   private static String lastChangeListToken;
   private static String lastViewToken;
 
@@ -150,13 +147,6 @@
     Window.Location.reload();
   }
 
-  public static UnifiedPatchScreen.TopView getPatchScreenTopView() {
-    if (patchScreen == null) {
-      return null;
-    }
-    return patchScreen.getTopView();
-  }
-
   public static void displayLastChangeList() {
     if (lastChangeListToken != null) {
       display(lastChangeListToken);
@@ -205,27 +195,10 @@
       doSignIn(token);
     } else {
       view.setToken(token);
-      body.setView(view);
-    }
-  }
-
-  /**
-   * Update any top level menus which can vary based on the view which was
-   * loaded.
-   * @param view the loaded view.
-   */
-  public static void updateMenus(Screen view) {
-    LinkMenuBar diffBar = menuBars.get(GerritTopMenu.DIFFERENCES.menuName);
-    if (view instanceof UnifiedPatchScreen) {
-      patchScreen = (UnifiedPatchScreen) view;
-      menuLeft.setVisible(diffBar, true);
-      menuLeft.selectTab(menuLeft.getWidgetIndex(diffBar));
-    } else {
-      if (patchScreen != null && menuLeft.getSelectedWidget() == diffBar) {
-        menuLeft.selectTab(isSignedIn() ? 1 : 0);
+      if (isSignedIn()) {
+        LocalComments.saveInlineComments();
       }
-      patchScreen = null;
-      menuLeft.setVisible(diffBar, false);
+      body.setView(view);
     }
   }
 
@@ -318,7 +291,7 @@
   }
 
   /** @return the preferences of the currently signed in user, the default preferences if not signed in */
-  public static AccountPreferencesInfo getUserPreferences() {
+  public static GeneralPreferences getUserPreferences() {
     return myPrefs;
   }
 
@@ -404,7 +377,7 @@
     myAccount = AccountInfo.create(0, null, null, null);
     myAccountDiffPref = null;
     editPrefs = null;
-    myPrefs = AccountPreferencesInfo.createDefault();
+    myPrefs = GeneralPreferences.createDefault();
     urlAliasMatcher.clearUserAliases();
     xGerritAuth = null;
     refreshMenuBar();
@@ -417,7 +390,6 @@
 
   private void setXsrfToken() {
     xGerritAuth = Cookies.getCookie(XSRF_COOKIE_NAME);
-    Cookies.removeCookie(XSRF_COOKIE_NAME);
     JsonUtil.setDefaultXsrfManager(new XsrfManager() {
       @Override
       public String getToken(JsonDefTarget proxy) {
@@ -433,7 +405,9 @@
 
   @Override
   public void onModuleLoad() {
-    UserAgent.assertNotInIFrame();
+    if (!canLoadInIFrame()) {
+      UserAgent.assertNotInIFrame();
+    }
     setXsrfToken();
 
     KeyUtil.setEncoderImpl(new KeyUtil.Encoder() {
@@ -483,6 +457,7 @@
           hasDocumentation = true;
           docUrl = du;
         }
+        docSearch = info.gerrit().docSearch();
       }
     }));
     HostPageDataService hpd = GWT.create(HostPageDataService.class);
@@ -508,9 +483,9 @@
                 }
           }));
           AccountApi.self().view("preferences")
-              .get(cbg.add(new GerritCallback<AccountPreferencesInfo>() {
+              .get(cbg.add(new GerritCallback<GeneralPreferences>() {
             @Override
-            public void onSuccess(AccountPreferencesInfo prefs) {
+            public void onSuccess(GeneralPreferences prefs) {
               myPrefs = prefs;
               onModuleLoad2(result);
             }
@@ -526,7 +501,7 @@
           }));
         } else {
           myAccount = AccountInfo.create(0, null, null, null);
-          myPrefs = AccountPreferencesInfo.createDefault();
+          myPrefs = GeneralPreferences.createDefault();
           editPrefs = null;
           onModuleLoad2(result);
         }
@@ -534,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('.');
@@ -713,15 +692,6 @@
       menuLeft.selectTab(0);
     }
 
-    patchScreen = null;
-    LinkMenuBar diffBar = new LinkMenuBar();
-    menuBars.put(GerritTopMenu.DIFFERENCES.menuName, diffBar);
-    menuLeft.addInvisible(diffBar, C.menuDiff());
-    addDiffLink(diffBar, C.menuDiffCommit(), UnifiedPatchScreen.TopView.COMMIT);
-    addDiffLink(diffBar, C.menuDiffPreferences(), UnifiedPatchScreen.TopView.PREFERENCES);
-    addDiffLink(diffBar, C.menuDiffPatchSets(), UnifiedPatchScreen.TopView.PATCH_SETS);
-    addDiffLink(diffBar, C.menuDiffFiles(), UnifiedPatchScreen.TopView.FILES);
-
     final LinkMenuBar projectsBar = new LinkMenuBar();
     menuBars.put(GerritTopMenu.PROJECTS.menuName, projectsBar);
     addLink(projectsBar, C.menuProjectsList(), PageLinks.ADMIN_PROJECTS);
@@ -885,18 +855,18 @@
   public static void refreshUserPreferences() {
     if (isSignedIn()) {
       AccountApi.self().view("preferences")
-          .get(new GerritCallback<AccountPreferencesInfo>() {
+          .get(new GerritCallback<GeneralPreferences>() {
             @Override
-            public void onSuccess(AccountPreferencesInfo prefs) {
+            public void onSuccess(GeneralPreferences prefs) {
               setUserPreferences(prefs);
             }
           });
     } else {
-      setUserPreferences(AccountPreferencesInfo.createDefault());
+      setUserPreferences(GeneralPreferences.createDefault());
     }
   }
 
-  public static void setUserPreferences(AccountPreferencesInfo prefs) {
+  public static void setUserPreferences(GeneralPreferences prefs) {
     myPrefs = prefs;
     applyUserPreferences();
     refreshMenuBar();
@@ -914,6 +884,10 @@
     urlAliasMatcher.updateUserAliases(myPrefs.urlAliases());
   }
 
+  public static boolean hasDocSearch() {
+    return docSearch;
+  }
+
   private static void getDocIndex(final AsyncCallback<DocInfo> cb) {
     RequestBuilder req =
         new RequestBuilder(RequestBuilder.HEAD, GWT.getHostPageBaseURL()
@@ -966,7 +940,7 @@
 
       @Override
       public void onKeyDown(KeyDownEvent event) {
-        if(event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
+        if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
           showHidePopup();
           event.preventDefault();
         }
@@ -1009,19 +983,6 @@
     m.insertItem(new LinkMenuItem(text, historyToken), beforeIndex);
   }
 
-  private static void addDiffLink(final LinkMenuBar m, final String text,
-      final UnifiedPatchScreen.TopView tv) {
-    m.addItem(new LinkMenuItem(text, "") {
-        @Override
-        public void go() {
-          if (patchScreen != null) {
-            patchScreen.setTopView(tv);
-          }
-          AnchorElement.as(getElement()).blur();
-        }
-      });
-  }
-
   private static LinkMenuItem addProjectLink(LinkMenuBar m, TopMenuItem item) {
     LinkMenuItem i = new ProjectLinkMenuItem(item.getName(), item.getUrl()) {
         @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index 269999c..4c8c58d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -105,6 +105,7 @@
   String sectionNavigation();
   String sectionActions();
   String keySearch();
+  String keyEditor();
   String keyHelp();
 
   String sectionJumping();
@@ -126,4 +127,7 @@
   String stringListPanelDelete();
   String stringListPanelUp();
   String stringListPanelDown();
+
+  String searchDropdownChanges();
+  String searchDropdownDoc();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index fb74506..10d7e1d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -88,6 +88,7 @@
 sectionNavigation = Navigation
 sectionActions = Actions
 keySearch = Search
+keyEditor = Open Inline Editor
 keyHelp = Press '?' to view keyboard shortcuts
 
 sectionJumping = Jumping
@@ -109,3 +110,6 @@
 stringListPanelDelete = Delete
 stringListPanelUp = Up
 stringListPanelDown = Down
+
+searchDropdownChanges = Changes
+searchDropdownDoc = Docs
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index 7735e1d..32e30d4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -37,27 +37,9 @@
   String cSIZE();
   String cSUBJECT();
   String cSTATUS();
-  String cellsNextToFileComment();
-  String changeScreenDescription();
-  String changeScreenStarIcon();
   String changeSize();
   String changeTable();
   String changeTablePrevNextLinks();
-  String changeTypeCell();
-  String commentCell();
-  String commentEditorPanel();
-  String commentHolder();
-  String commentHolderLeftmost();
-  String commentPanel();
-  String commentPanelAuthorCell();
-  String commentPanelButtons();
-  String commentPanelContent();
-  String commentPanelDateCell();
-  String commentPanelHeader();
-  String commentPanelLast();
-  String commentPanelMessage();
-  String commentPanelSummary();
-  String commentPanelSummaryCell();
   String commentedActionDialog();
   String commentedActionMessage();
   String contributorAgreementAlreadySubmitted();
@@ -69,14 +51,6 @@
   String dataCellHidden();
   String dataHeader();
   String dataHeaderHidden();
-  String diffLinkCell();
-  String diffText();
-  String diffTextCONTEXT();
-  String diffTextDELETE();
-  String diffTextFileHeader();
-  String diffTextHunkHeader();
-  String diffTextINSERT();
-  String diffTextNoLF();
   String downloadBox();
   String downloadBoxTable();
   String downloadBoxTableCommandColumn();
@@ -89,7 +63,6 @@
   String downloadLinkHeaderGap();
   String downloadLinkList();
   String downloadLink_Active();
-  String drafts();
   String editHeadButton();
   String emptySection();
   String errorDialog();
@@ -99,12 +72,6 @@
   String errorDialogTitle();
   String extensionPanel();
   String loadingPluginsDialog();
-  String fileColumnHeader();
-  String fileCommentBorder();
-  String fileLine();
-  String fileLineDELETE();
-  String fileLineINSERT();
-  String filePathCell();
   String gerritBody();
   String gerritTopMenu();
   String greenCheckClass();
@@ -120,18 +87,15 @@
   String groupUUIDPanel();
   String header();
   String iconCell();
-  String iconCellOfFileCommentRow();
   String iconHeader();
   String identityUntrustedExternalId();
   String infoBlock();
   String inputFieldTypeHint();
   String labelNotApplicable();
   String leftMostCell();
-  String lineNumber();
   String link();
   String linkMenuBar();
   String linkMenuItemNotLast();
-  String linkPanel();
   String maxObjectSizeLimitEffectiveLabel();
   String menuBarUserName();
   String menuBarUserNameAvatar();
@@ -141,18 +105,16 @@
   String menuScreenMenuBar();
   String needsReview();
   String negscore();
-  String noborder();
-  String nowrap();
+  String oauthExpires();
+  String oauthInfoBlock();
+  String oauthPanel();
+  String oauthPanelCookieEntry();
+  String oauthPanelCookieHeading();
+  String oauthPanelNetRCEntry();
+  String oauthPanelNetRCHeading();
+  String oauthToken();
   String pagingLink();
-  String patchBrowserPopup();
-  String patchBrowserPopupBody();
-  String patchCellReverseDiff();
-  String patchContentTable();
-  String patchHistoryTable();
-  String patchHistoryTablePatchSetHeader();
-  String patchNoDifference();
   String patchSetActions();
-  String patchSizeCell();
   String pluginProjectConfigInheritedValue();
   String pluginsTable();
   String posscore();
@@ -160,23 +122,20 @@
   String projectFilterLabel();
   String projectFilterPanel();
   String projectNameColumn();
+  String queryIcon();
   String rebaseContentPanel();
   String rebaseSuggestBox();
   String registerScreenExplain();
   String registerScreenNextLinks();
   String registerScreenSection();
-  String reviewedPanelBottom();
-  String rightBorder();
   String rpcStatus();
   String screen();
   String screenHeader();
   String searchPanel();
   String suggestBoxPopup();
   String sectionHeader();
-  String sideBySideScreenLinkTable();
   String singleLine();
   String smallHeading();
-  String sourceFilePath();
   String specialBranchDataCell();
   String specialBranchIconCell();
   String sshHostKeyPanel();
@@ -187,15 +146,12 @@
   String sshKeyPanelInvalid();
   String sshKeyTable();
   String stringListPanelButtons();
-  String topMostCell();
   String topmenu();
   String topmenuMenuLeft();
   String topmenuMenuRight();
   String topmenuTDglue();
   String topmenuTDmenu();
   String topmost();
-  String unifiedTable();
-  String unifiedTableHeader();
   String userInfoPopup();
   String usernameField();
   String watchedProjectFilter();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
index 21da8ce..58442b8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
@@ -36,4 +36,6 @@
   String cannotDownloadPlugin(String scriptPath);
 
   String parentUpdateFailed(String message);
+
+  String fileCount(int fileNumber, int fileCount);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
index 2832d41..b2d67b8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
@@ -17,3 +17,5 @@
 cannotDownloadPlugin = Cannot load plugin from {0}
 
 parentUpdateFailed = Setting parent project failed: {0}
+
+fileCount = File {0} of {1}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RangeInfo.java
new file mode 100644
index 0000000..f86d8f4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RangeInfo.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.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class RangeInfo extends JavaScriptObject {
+  public final native int start() /*-{ return this.start; }-*/;
+
+  public final native int end() /*-{ return this.end; }-*/;
+
+  protected RangeInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
index 45b1d52..83a187b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
@@ -27,6 +27,7 @@
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
@@ -34,6 +35,7 @@
 
 class SearchPanel extends Composite {
   private final HintTextBox searchBox;
+  private final ListBox dropdown;
   private HandlerRegistration regFocus;
 
   SearchPanel() {
@@ -54,6 +56,18 @@
       }
     });
 
+    if (Gerrit.hasDocSearch()) {
+      dropdown = new ListBox();
+      dropdown.setStyleName("searchDropdown");
+      dropdown.addItem(Gerrit.C.searchDropdownChanges());
+      dropdown.addItem(Gerrit.C.searchDropdownDoc());
+      dropdown.setVisibleItemCount(1);
+      dropdown.setSelectedIndex(0);
+    } else {
+      // Doc search is NOT available.
+      dropdown = null;
+    }
+
     final SuggestBox suggestBox =
         new SuggestBox(new SearchSuggestOracle(), searchBox, suggestionDisplay);
     searchBox.setStyleName("searchTextBox");
@@ -70,6 +84,9 @@
     });
 
     body.add(suggestBox);
+    if (dropdown != null) {
+      body.add(dropdown);
+    }
     body.add(searchButton);
   }
 
@@ -110,14 +127,23 @@
 
     searchBox.setFocus(false);
 
-    if (query.matches("^[1-9][0-9]*$")) {
-      Gerrit.display(PageLinks.toChange(Change.Id.parse(query)));
+    if (dropdown != null
+        && dropdown.getSelectedValue().equals(Gerrit.C.searchDropdownDoc())) {
+      // doc
+      Gerrit.display(PageLinks.toDocumentationQuery(query));
     } else {
-      Gerrit.display(PageLinks.toChangeQuery(query), QueryScreen.forQuery(query));
+      // changes
+      if (query.matches("^[1-9][0-9]*$")) {
+        Gerrit.display(PageLinks.toChange(Change.Id.parse(query)));
+      } else {
+        Gerrit.display(
+            PageLinks.toChangeQuery(query), QueryScreen.forQuery(query));
+      }
     }
   }
 
-  private static class MySuggestionDisplay extends SuggestBox.DefaultSuggestionDisplay {
+  private static class MySuggestionDisplay
+      extends SuggestBox.DefaultSuggestionDisplay {
     private boolean isSuggestionSelected;
 
     private MySuggestionDisplay() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index afbaf85..54c5b92 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -29,7 +29,9 @@
   private static final List<ParamSuggester> paramSuggester = Arrays.asList(
       new ParamSuggester(Arrays.asList("project:", "parentproject:"),
           new ProjectNameSuggestOracle()),
-      new ParamSuggester(Arrays.asList("owner:", "reviewer:"),
+      new ParamSuggester(Arrays.asList(
+          "owner:", "reviewer:", "commentby:", "reviewedby:", "author:",
+          "committer:", "from:"),
           new AccountSuggestOracle() {
             @Override
             public void onRequestSuggestions(final Request request, final Callback done) {
@@ -102,6 +104,8 @@
     suggestions.add("has:draft");
     suggestions.add("has:edit");
     suggestions.add("has:star");
+    suggestions.add("has:stars");
+    suggestions.add("star:");
 
     suggestions.add("is:");
     suggestions.add("is:starred");
@@ -124,6 +128,7 @@
     suggestions.add("status:closed");
     suggestions.add("status:merged");
     suggestions.add("status:abandoned");
+    suggestions.add("status:draft");
 
     suggestions.add("added:");
     suggestions.add("deleted:");
@@ -205,7 +210,7 @@
   private static class SearchSuggestion implements SuggestOracle.Suggestion {
     private final String suggestion;
     private final String fullQuery;
-    public SearchSuggestion(String suggestion, String fullQuery) {
+    SearchSuggestion(String suggestion, String fullQuery) {
       this.suggestion = suggestion;
       // Add a space to the query if it is a complete operation (e.g.
       // "status:open") so the user can keep on typing.
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 a1bcfe8..acd2e78 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
@@ -27,6 +27,7 @@
 import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
+import java.util.HashSet;
 import java.util.Set;
 
 /**
@@ -45,13 +46,14 @@
 
   /** Put the account edit preferences */
   public static void putEditPreferences(EditPreferences in,
-      AsyncCallback<VoidResult> cb) {
+      AsyncCallback<EditPreferences> cb) {
     self().view("preferences.edit").put(in, cb);
   }
 
   public static void suggest(String query, int limit,
       AsyncCallback<JsArray<AccountInfo>> cb) {
     new RestApi("/accounts/")
+      .addParameterTrue("suggest")
       .addParameter("q", query)
       .addParameter("n", limit)
       .background()
@@ -76,6 +78,11 @@
     new RestApi("/accounts/").id(account).view("username").put(input, cb);
   }
 
+  /** Retrieve the account name */
+  public static void getName(String account, AsyncCallback<NativeString> cb) {
+    new RestApi("/accounts/").id(account).view("name").get(cb);
+  }
+
   /** Retrieve email addresses */
   public static void getEmails(String account,
       AsyncCallback<JsArray<EmailInfo>> cb) {
@@ -103,6 +110,54 @@
         .post(sshPublicKey, cb);
   }
 
+  /** Retrieve Watched Projects */
+  public static void getWatchedProjects(String account,
+      AsyncCallback<JsArray<ProjectWatchInfo>> cb) {
+    new RestApi("/accounts/").id(account).view("watched.projects").get(cb);
+  }
+
+  /** Create/Update Watched Project */
+  public static void updateWatchedProject(
+      String account,
+      ProjectWatchInfo watchedProjectInfo,
+      AsyncCallback<JsArray<ProjectWatchInfo>> cb) {
+    Set<ProjectWatchInfo> watchedProjectInfos = new HashSet<>();
+    watchedProjectInfos.add(watchedProjectInfo);
+    updateWatchedProjects(account, watchedProjectInfos, cb);
+  }
+
+  /** Create/Update Watched Projects */
+  public static void updateWatchedProjects(
+      String account,
+      Set<ProjectWatchInfo> watchedProjectInfos,
+      AsyncCallback<JsArray<ProjectWatchInfo>> cb) {
+    new RestApi("/accounts/")
+        .id(account)
+        .view("watched.projects")
+        .post(projectWatchArrayFromSet(watchedProjectInfos), cb);
+  }
+
+  /** Delete Watched Project */
+  public static void deleteWatchedProject(
+      String account,
+      ProjectWatchInfo watchedProjectInfo,
+      AsyncCallback<JsArray<ProjectWatchInfo>> cb) {
+    Set<ProjectWatchInfo> watchedProjectInfos = new HashSet<>();
+    watchedProjectInfos.add(watchedProjectInfo);
+    deleteWatchedProjects(account, watchedProjectInfos, cb);
+  }
+
+  /** Delete Watched Projects */
+  public static void deleteWatchedProjects(
+      String account,
+      Set<ProjectWatchInfo> watchedProjectInfos,
+      AsyncCallback<JsArray<ProjectWatchInfo>> cb) {
+    new RestApi("/accounts/")
+        .id(account)
+        .view("watched.projects:delete")
+        .post(projectWatchArrayFromSet(watchedProjectInfos), cb);
+  }
+
   /**
    * Delete SSH keys. For each key to be deleted a separate DELETE request is
    * fired to the server. The {@code onSuccess} method of the provided callback
@@ -141,6 +196,15 @@
     new RestApi("/accounts/").id(account).view("password.http").delete(cb);
   }
 
+  private static JsArray<ProjectWatchInfo> projectWatchArrayFromSet(
+      Set<ProjectWatchInfo> set) {
+    JsArray<ProjectWatchInfo> jsArray = JsArray.createArray().cast();
+    for (ProjectWatchInfo p : set) {
+      jsArray.push(p);
+    }
+    return jsArray;
+  }
+
   private static class HttpPasswordInput extends JavaScriptObject {
     final native void generate(boolean g) /*-{ if(g)this.generate=g; }-*/;
 
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 94884fa..a084612 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
@@ -31,7 +31,6 @@
   String contextWholeFile();
   String showSiteHeader();
   String useFlashClipboard();
-  String copySelfOnEmails();
   String reviewCategoryLabel();
   String messageShowInReviewCategoryNone();
   String messageShowInReviewCategoryName();
@@ -43,6 +42,7 @@
   String showSizeBarInChangeTable();
   String showLegacycidInChangeTable();
   String muteCommonPathPrefixes();
+  String signedOffBy();
   String myMenu();
   String myMenuInfo();
   String myMenuName();
@@ -57,6 +57,7 @@
   String tabGpgKeys();
   String tabHttpAccess();
   String tabMyGroups();
+  String tabOAuthToken();
   String tabPreferences();
   String tabSshKeys();
   String tabWatchedProjects();
@@ -81,6 +82,12 @@
   String invalidUserName();
   String invalidUserEmail();
 
+  String labelOAuthToken();
+  String labelOAuthExpires();
+  String labelOAuthNetRCEntry();
+  String labelOAuthGitCookie();
+  String labelOAuthExpired();
+
   String sshKeyInvalid();
   String sshKeyAlgorithm();
   String sshKeyKey();
@@ -159,4 +166,9 @@
   String welcomeAgreementText();
   String welcomeAgreementLater();
   String welcomeContinue();
+
+  String messageEnabled();
+  String messageCCMeOnMyComments();
+  String messageDisabled();
+  String emailFieldLabel();
 }
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 0944448..ca2d316 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,9 +5,8 @@
 preferredEmail = Email Address
 registeredOn = Registered
 accountId = Account ID
-showSiteHeader = Show Site Header
+showSiteHeader = Show Site Header / Footer
 useFlashClipboard = Use Flash Clipboard Widget
-copySelfOnEmails = CC Me On Comments I Write
 reviewCategoryLabel = Display In Review Category
 messageShowInReviewCategoryNone = None (default)
 messageShowInReviewCategoryName = Show Name
@@ -15,6 +14,11 @@
 messageShowInReviewCategoryUsername = Show Username
 messageShowInReviewCategoryAbbrev = Show Abbreviated Name
 
+emailFieldLabel = Email Notifications:
+messageEnabled = Enabled
+messageCCMeOnMyComments = CC Me On Comments I Write
+messageDisabled = Disabled
+
 maximumPageSizeFieldLabel = Maximum Page Size:
 diffViewLabel = Diff View:
 dateFormatLabel = Date/Time Format:
@@ -24,6 +28,7 @@
 showSizeBarInChangeTable = Show Change Sizes As Colored Bars
 showLegacycidInChangeTable = Show Change Number In Changes Table
 muteCommonPathPrefixes = Mute Common Path Prefixes In File List
+signedOffBy = Insert Signed-off-by Footer For Inline Edit Changes
 myMenu = My Menu
 myMenuInfo = \
   Menu items for the 'My' top level menu. \
@@ -39,6 +44,7 @@
 tabEditPreferences = Edit Preferences
 tabGpgKeys = GPG Public Keys
 tabHttpAccess = HTTP Password
+tabOAuthToken = OAuth Token
 tabMyGroups = Groups
 tabPreferences = Preferences
 tabSshKeys = SSH Public Keys
@@ -63,6 +69,13 @@
 linkReloadContact = Reload
 invalidUserName = Username must contain only letters, numbers, _, - or .
 invalidUserEmail = Email format is wrong.
+
+labelOAuthToken = Access Token
+labelOAuthExpires = Expires
+labelOAuthNetRCEntry = Entry for ~/.netrc
+labelOAuthGitCookie = Entry for ~/.gitcookies
+labelOAuthExpired = To obtain an access token please sign out and sign in again.
+
 sshKeyInvalid = Invalid Key
 sshKeyAlgorithm = Algorithm
 sshKeyKey = Key
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
index 68a99e0..b398c0f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
@@ -18,7 +18,7 @@
 
 public interface AccountMessages extends Messages {
   String lines(short cnt);
-  String rowsPerPage(short cnt);
+  String rowsPerPage(int cnt);
   String changeScreenServerDefault(String d);
   String enterIAGREE(String iagree);
 }
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 901fe96..ae3599d 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
@@ -18,7 +18,9 @@
 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;
 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.OnEditEnabler;
 import com.google.gerrit.common.PageLinks;
@@ -59,6 +61,7 @@
   NpTextBox nameTxt;
   private ListBox emailPick;
   private Button registerNewEmail;
+  private OnEditEnabler onEditEnabler;
 
   ContactPanelShort() {
     body = new FlowPanel();
@@ -165,6 +168,8 @@
         }
       }
     });
+
+    onEditEnabler = new OnEditEnabler(save, nameTxt);
   }
 
   private boolean canEditFullName() {
@@ -194,36 +199,41 @@
     haveAccount = false;
     haveEmails = false;
 
-    Util.ACCOUNT_SVC.myAccount(new GerritCallback<Account>() {
+    CallbackGroup group = new CallbackGroup();
+    AccountApi.getName("self", group.add(new GerritCallback<NativeString>() {
+
       @Override
-      public void onSuccess(Account result) {
-        if (!isAttached()) {
-          return;
-        }
-        display(FormatUtil.asInfo(result));
+      public void onSuccess(NativeString result) {
+        nameTxt.setText(result.asString());
         haveAccount = true;
-        postLoad();
       }
-    });
-    AccountApi.getEmails("self", new GerritCallback<JsArray<EmailInfo>>() {
+
+      @Override
+      public void onFailure(Throwable caught) {
+      }
+    }));
+
+    AccountApi.getEmails("self", group.addFinal(new GerritCallback<JsArray<EmailInfo>>() {
       @Override
       public void onSuccess(JsArray<EmailInfo> result) {
-        if (!isAttached()) {
-          return;
-        }
         for (EmailInfo i : Natives.asList(result)) {
           emailPick.addItem(i.email());
+          if (i.isPreferred()) {
+            currentEmail = i.email();
+          }
         }
         haveEmails = true;
         postLoad();
       }
-    });
+    }));
   }
 
   private void postLoad() {
     if (haveAccount && haveEmails) {
       updateEmailList();
       registerNewEmail.setEnabled(true);
+      save.setEnabled(false);
+      onEditEnabler.updateOriginalValue(nameTxt);
     }
     display();
   }
@@ -242,7 +252,7 @@
     currentEmail = account.email();
     nameTxt.setText(account.name());
     save.setEnabled(false);
-    new OnEditEnabler(save, nameTxt);
+    onEditEnabler.updateOriginalValue(nameTxt);
   }
 
   private void doRegisterNewEmail() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
index daf1181..423d05f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
@@ -44,6 +44,7 @@
     p.theme(in.theme);
     p.hideEmptyPane(in.hideEmptyPane);
     p.retainHeader(in.retainHeader);
+    p.skipUnchanged(in.skipUnchanged);
     p.skipUncommented(in.skipUncommented);
     p.skipDeleted(in.skipDeleted);
     p.matchBrackets(in.matchBrackets);
@@ -64,6 +65,7 @@
     p.showTabs = showTabs();
     p.showWhitespaceErrors = showWhitespaceErrors();
     p.skipDeleted = skipDeleted();
+    p.skipUnchanged = skipUnchanged();
     p.skipUncommented = skipUncommented();
     p.syntaxHighlighting = syntaxHighlighting();
     p.hideTopMenu = hideTopMenu();
@@ -127,23 +129,24 @@
   public final native void lineLength(int c) /*-{ this.line_length = c }-*/;
   public final native void context(int c) /*-{ this.context = c }-*/;
   public final native void cursorBlinkRate(int r) /*-{ this.cursor_blink_rate = r }-*/;
-  public final native void intralineDifference(boolean i) /*-{ this.intraline_difference = i }-*/;
-  public final native void showLineEndings(boolean s) /*-{ this.show_line_endings = s }-*/;
-  public final native void showTabs(boolean s) /*-{ this.show_tabs = s }-*/;
-  public final native void showWhitespaceErrors(boolean s) /*-{ this.show_whitespace_errors = s }-*/;
-  public final native void syntaxHighlighting(boolean s) /*-{ this.syntax_highlighting = s }-*/;
-  public final native void hideTopMenu(boolean s) /*-{ this.hide_top_menu = s }-*/;
-  public final native void autoHideDiffTableHeader(boolean s) /*-{ this.auto_hide_diff_table_header = s }-*/;
-  public final native void hideLineNumbers(boolean s) /*-{ this.hide_line_numbers = s }-*/;
-  public final native void expandAllComments(boolean e) /*-{ this.expand_all_comments = e }-*/;
-  public final native void manualReview(boolean r) /*-{ this.manual_review = r }-*/;
-  public final native void renderEntireFile(boolean r) /*-{ this.render_entire_file = r }-*/;
-  public final native void retainHeader(boolean r) /*-{ this.retain_header = r }-*/;
-  public final native void hideEmptyPane(boolean s) /*-{ this.hide_empty_pane = s }-*/;
-  public final native void skipUncommented(boolean s) /*-{ this.skip_uncommented = s }-*/;
-  public final native void skipDeleted(boolean s) /*-{ this.skip_deleted = s }-*/;
-  public final native void matchBrackets(boolean m) /*-{ this.match_brackets = m }-*/;
-  public final native void lineWrapping(boolean w) /*-{ this.line_wrapping = w }-*/;
+  public final native void intralineDifference(Boolean i) /*-{ this.intraline_difference = i }-*/;
+  public final native void showLineEndings(Boolean s) /*-{ this.show_line_endings = s }-*/;
+  public final native void showTabs(Boolean s) /*-{ this.show_tabs = s }-*/;
+  public final native void showWhitespaceErrors(Boolean s) /*-{ this.show_whitespace_errors = s }-*/;
+  public final native void syntaxHighlighting(Boolean s) /*-{ this.syntax_highlighting = s }-*/;
+  public final native void hideTopMenu(Boolean s) /*-{ this.hide_top_menu = s }-*/;
+  public final native void autoHideDiffTableHeader(Boolean s) /*-{ this.auto_hide_diff_table_header = s }-*/;
+  public final native void hideLineNumbers(Boolean s) /*-{ this.hide_line_numbers = s }-*/;
+  public final native void expandAllComments(Boolean e) /*-{ this.expand_all_comments = e }-*/;
+  public final native void manualReview(Boolean r) /*-{ this.manual_review = r }-*/;
+  public final native void renderEntireFile(Boolean r) /*-{ this.render_entire_file = r }-*/;
+  public final native void retainHeader(Boolean r) /*-{ this.retain_header = r }-*/;
+  public final native void hideEmptyPane(Boolean s) /*-{ this.hide_empty_pane = s }-*/;
+  public final native void skipUnchanged(Boolean s) /*-{ this.skip_unchanged = s }-*/;
+  public final native void skipUncommented(Boolean s) /*-{ this.skip_uncommented = s }-*/;
+  public final native void skipDeleted(Boolean s) /*-{ this.skip_deleted = s }-*/;
+  public final native void matchBrackets(Boolean m) /*-{ this.match_brackets = m }-*/;
+  public final native void lineWrapping(Boolean w) /*-{ this.line_wrapping = w }-*/;
   public final native boolean intralineDifference() /*-{ return this.intraline_difference || false }-*/;
   public final native boolean showLineEndings() /*-{ return this.show_line_endings || false }-*/;
   public final native boolean showTabs() /*-{ return this.show_tabs || false }-*/;
@@ -157,16 +160,17 @@
   public final native boolean renderEntireFile() /*-{ return this.render_entire_file || false }-*/;
   public final native boolean hideEmptyPane() /*-{ return this.hide_empty_pane || false }-*/;
   public final native boolean retainHeader() /*-{ return this.retain_header || false }-*/;
+  public final native boolean skipUnchanged() /*-{ return this.skip_unchanged || false }-*/;
   public final native boolean skipUncommented() /*-{ return this.skip_uncommented || false }-*/;
   public final native boolean skipDeleted() /*-{ return this.skip_deleted || false }-*/;
   public final native boolean matchBrackets() /*-{ return this.match_brackets || false }-*/;
   public final native boolean lineWrapping() /*-{ return this.line_wrapping || false }-*/;
 
-  private final native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
-  private final native void setIgnoreWhitespaceRaw(String i) /*-{ this.ignore_whitespace = i }-*/;
-  private final native String ignoreWhitespaceRaw() /*-{ return this.ignore_whitespace }-*/;
-  private final native String themeRaw() /*-{ return this.theme }-*/;
-  private final native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
+  private native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
+  private native void setIgnoreWhitespaceRaw(String i) /*-{ this.ignore_whitespace = i }-*/;
+  private native String ignoreWhitespaceRaw() /*-{ return this.ignore_whitespace }-*/;
+  private native String themeRaw() /*-{ return this.theme }-*/;
+  private native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
 
   protected DiffPreferences() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
index 39af4d4..ae89607 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
@@ -24,6 +24,7 @@
     EditPreferences p = createObject().cast();
     p.tabSize(in.tabSize);
     p.lineLength(in.lineLength);
+    p.indentUnit(in.indentUnit);
     p.cursorBlinkRate(in.cursorBlinkRate);
     p.hideTopMenu(in.hideTopMenu);
     p.showTabs(in.showTabs);
@@ -33,14 +34,16 @@
     p.matchBrackets(in.matchBrackets);
     p.lineWrapping(in.lineWrapping);
     p.autoCloseBrackets(in.autoCloseBrackets);
+    p.showBase(in.showBase);
     p.theme(in.theme);
     p.keyMapType(in.keyMapType);
     return p;
   }
 
-  public final void copyTo(EditPreferencesInfo p) {
+  public final EditPreferencesInfo copyTo(EditPreferencesInfo p) {
     p.tabSize = tabSize();
     p.lineLength = lineLength();
+    p.indentUnit = indentUnit();
     p.cursorBlinkRate = cursorBlinkRate();
     p.hideTopMenu = hideTopMenu();
     p.showTabs = showTabs();
@@ -50,22 +53,25 @@
     p.matchBrackets = matchBrackets();
     p.lineWrapping = lineWrapping();
     p.autoCloseBrackets = autoCloseBrackets();
+    p.showBase = showBase();
     p.theme = theme();
     p.keyMapType = keyMapType();
+    return p;
   }
 
   public final void theme(Theme i) {
     setThemeRaw(i != null ? i.toString() : Theme.DEFAULT.toString());
   }
-  private final native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
+  private native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
 
   public final void keyMapType(KeyMapType i) {
     setkeyMapTypeRaw(i != null ? i.toString() : KeyMapType.DEFAULT.toString());
   }
-  private final native void setkeyMapTypeRaw(String i) /*-{ this.key_map_type = i }-*/;
+  private native void setkeyMapTypeRaw(String i) /*-{ this.key_map_type = i }-*/;
 
   public final native void tabSize(int t) /*-{ this.tab_size = t }-*/;
   public final native void lineLength(int c) /*-{ this.line_length = c }-*/;
+  public final native void indentUnit(int c) /*-{ this.indent_unit = c }-*/;
   public final native void cursorBlinkRate(int r) /*-{ this.cursor_blink_rate = r }-*/;
   public final native void hideTopMenu(boolean s) /*-{ this.hide_top_menu = s }-*/;
   public final native void showTabs(boolean s) /*-{ this.show_tabs = s }-*/;
@@ -75,18 +81,19 @@
   public final native void matchBrackets(boolean m) /*-{ this.match_brackets = m }-*/;
   public final native void lineWrapping(boolean w) /*-{ this.line_wrapping = w }-*/;
   public final native void autoCloseBrackets(boolean c) /*-{ this.auto_close_brackets = c }-*/;
+  public final native void showBase(boolean s) /*-{ this.show_base = s }-*/;
 
   public final Theme theme() {
     String s = themeRaw();
     return s != null ? Theme.valueOf(s) : Theme.DEFAULT;
   }
-  private final native String themeRaw() /*-{ return this.theme }-*/;
+  private native String themeRaw() /*-{ return this.theme }-*/;
 
   public final KeyMapType keyMapType() {
     String s = keyMapTypeRaw();
     return s != null ? KeyMapType.valueOf(s) : KeyMapType.DEFAULT;
   }
-  private final native String keyMapTypeRaw() /*-{ return this.key_map_type }-*/;
+  private native String keyMapTypeRaw() /*-{ return this.key_map_type }-*/;
 
   public final int tabSize() {
     return get("tab_size", 8);
@@ -96,6 +103,10 @@
     return get("line_length", 100);
   }
 
+  public final int indentUnit() {
+    return get("indent_unit", 2);
+  }
+
   public final int cursorBlinkRate() {
     return get("cursor_blink_rate", 0);
   }
@@ -108,7 +119,8 @@
   public final native boolean matchBrackets() /*-{ return this.match_brackets || false }-*/;
   public final native boolean lineWrapping() /*-{ return this.line_wrapping || false }-*/;
   public final native boolean autoCloseBrackets() /*-{ return this.auto_close_brackets || false }-*/;
-  private final native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
+  public final native boolean showBase() /*-{ return this.show_base || false }-*/;
+  private native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
 
   protected EditPreferences() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyOAuthTokenScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyOAuthTokenScreen.java
new file mode 100644
index 0000000..a4c92fe
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyOAuthTokenScreen.java
@@ -0,0 +1,197 @@
+// 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.account;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.info.GeneralPreferences;
+import com.google.gerrit.client.info.OAuthTokenInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.i18n.client.LocaleInfo;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+
+import java.util.Date;
+
+public class MyOAuthTokenScreen extends SettingsScreen {
+  private CopyableLabel tokenLabel;
+  private Label expiresLabel;
+  private Label expiredNote;
+  private CopyableLabel netrcValue;
+  private CopyableLabel cookieValue;
+  private FlowPanel flow;
+  private Grid grid;
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+
+    tokenLabel = new CopyableLabel("");
+    tokenLabel.addStyleName(Gerrit.RESOURCES.css().oauthToken());
+
+    expiresLabel = new Label("");
+    expiresLabel.addStyleName(Gerrit.RESOURCES.css().oauthExpires());
+
+    grid = new Grid(2, 2);
+    grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
+    grid.addStyleName(Gerrit.RESOURCES.css().oauthInfoBlock());
+    add(grid);
+
+    expiredNote = new Label(Util.C.labelOAuthExpired());
+    expiredNote.setVisible(false);
+    add(expiredNote);
+
+    row(grid, 0, Util.C.labelOAuthToken(), tokenLabel);
+    row(grid, 1, Util.C.labelOAuthExpires(), expiresLabel);
+
+    CellFormatter fmt = grid.getCellFormatter();
+    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
+    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
+    fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader());
+
+    flow = new FlowPanel();
+    flow.setStyleName(Gerrit.RESOURCES.css().oauthPanel());
+    add(flow);
+
+    Label netrcLabel = new Label(Util.C.labelOAuthNetRCEntry());
+    netrcLabel.setStyleName(Gerrit.RESOURCES.css().oauthPanelNetRCHeading());
+    flow.add(netrcLabel);
+    netrcValue= new CopyableLabel("");
+    netrcValue.setStyleName(Gerrit.RESOURCES.css().oauthPanelNetRCEntry());
+    flow.add(netrcValue);
+
+    Label cookieLabel = new Label(Util.C.labelOAuthGitCookie());
+    cookieLabel.setStyleName(Gerrit.RESOURCES.css().oauthPanelCookieHeading());
+    flow.add(cookieLabel);
+    cookieValue = new CopyableLabel("");
+    cookieValue.setStyleName(Gerrit.RESOURCES.css().oauthPanelCookieEntry());
+    flow.add(cookieValue);
+  }
+
+  private void row(Grid grid, int row, String name, Widget field) {
+    final CellFormatter fmt = grid.getCellFormatter();
+    if (LocaleInfo.getCurrentLocale().isRTL()) {
+      grid.setText(row, 1, name);
+      grid.setWidget(row, 0, field);
+      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().header());
+    } else {
+      grid.setText(row, 0, name);
+      grid.setWidget(row, 1, field);
+      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().header());
+    }
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    AccountApi.self().view("preferences")
+    .get(new ScreenLoadCallback<GeneralPreferences>(this) {
+      @Override
+      protected void preDisplay(GeneralPreferences prefs) {
+        display(prefs);
+      }
+    });
+  }
+
+  private void display(final GeneralPreferences prefs) {
+    AccountApi.self().view("oauthtoken")
+    .get(new GerritCallback<OAuthTokenInfo>() {
+      @Override
+      public void onSuccess(OAuthTokenInfo tokenInfo) {
+        tokenLabel.setText(tokenInfo.accessToken());
+        expiresLabel.setText(getExpiresAt(tokenInfo, prefs));
+        netrcValue.setText(getNetRC(tokenInfo));
+        cookieValue.setText(getCookie(tokenInfo));
+        flow.setVisible(true);
+        expiredNote.setVisible(false);
+      }
+      @Override
+      public void onFailure(Throwable caught) {
+        if (isNoSuchEntity(caught) || isSigninFailure(caught)) {
+          tokenLabel.setText("");
+          expiresLabel.setText("");
+          netrcValue.setText("");
+          cookieValue.setText("");
+          flow.setVisible(false);
+          expiredNote.setVisible(true);
+        } else {
+          showFailure(caught);
+        }
+      }
+    });
+  }
+
+  private static long getExpiresAt(OAuthTokenInfo tokenInfo) {
+    if (tokenInfo.expiresAt() == null) {
+      return Long.MAX_VALUE;
+    }
+    long expiresAt;
+    try {
+      expiresAt = Long.parseLong(tokenInfo.expiresAt());
+    } catch (NumberFormatException e) {
+      return Long.MAX_VALUE;
+    }
+    return expiresAt;
+  }
+
+  private static long getExpiresAtSeconds(OAuthTokenInfo tokenInfo) {
+    return getExpiresAt(tokenInfo) / 1000L;
+  }
+
+  private static String getExpiresAt(OAuthTokenInfo tokenInfo,
+      GeneralPreferences prefs) {
+    long expiresAt = getExpiresAt(tokenInfo);
+    if (expiresAt == Long.MAX_VALUE) {
+      return "";
+    }
+    String dateFormat = prefs.dateFormat().getLongFormat();
+    String timeFormat = prefs.timeFormat().getFormat();
+    DateTimeFormat formatter = DateTimeFormat.getFormat(
+        dateFormat + " " + timeFormat);
+    return formatter.format(new Date(expiresAt));
+  }
+
+  private static String getNetRC(OAuthTokenInfo accessTokenInfo) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("machine ");
+    sb.append(accessTokenInfo.resourceHost());
+    sb.append(" login ");
+    sb.append(accessTokenInfo.username());
+    sb.append(" password ");
+    sb.append(accessTokenInfo.accessToken());
+    return sb.toString();
+  }
+
+  private static String getCookie(OAuthTokenInfo accessTokenInfo) {
+    StringBuilder sb = new StringBuilder();
+    sb.append(accessTokenInfo.resourceHost());
+    sb.append("\tFALSE\t/\tTRUE\t");
+    sb.append(getExpiresAtSeconds(accessTokenInfo));
+    sb.append("\tgit-");
+    sb.append(accessTokenInfo.username());
+    sb.append('\t');
+    sb.append(accessTokenInfo.accessToken());
+    if (accessTokenInfo.providerId() != null) {
+      sb.append('@').append(accessTokenInfo.providerId());
+    }
+    return sb.toString();
+  }
+
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index 2b20ad6..2b01b59 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -14,22 +14,22 @@
 
 package com.google.gerrit.client.account;
 
-import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DEFAULT_PAGESIZE;
-import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.PAGESIZE_CHOICES;
+import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.DEFAULT_PAGESIZE;
+import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.PAGESIZE_CHOICES;
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GerritUiExtensionPoint;
 import com.google.gerrit.client.StringListPanel;
 import com.google.gerrit.client.api.ExtensionPanel;
 import com.google.gerrit.client.config.ConfigServerApi;
-import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gerrit.client.info.GeneralPreferences;
 import com.google.gerrit.client.info.TopMenuItem;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.ReviewCategoryStrategy;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -50,16 +50,17 @@
 public class MyPreferencesScreen extends SettingsScreen {
   private CheckBox showSiteHeader;
   private CheckBox useFlashClipboard;
-  private CheckBox copySelfOnEmails;
   private CheckBox relativeDateInChangeTable;
   private CheckBox sizeBarInChangeTable;
   private CheckBox legacycidInChangeTable;
   private CheckBox muteCommonPathPrefixes;
+  private CheckBox signedOffBy;
   private ListBox maximumPageSize;
   private ListBox dateFormat;
   private ListBox timeFormat;
   private ListBox reviewCategoryStrategy;
   private ListBox diffView;
+  private ListBox emailStrategy;
   private StringListPanel myMenus;
   private Button save;
 
@@ -69,41 +70,54 @@
 
     showSiteHeader = new CheckBox(Util.C.showSiteHeader());
     useFlashClipboard = new CheckBox(Util.C.useFlashClipboard());
-    copySelfOnEmails = new CheckBox(Util.C.copySelfOnEmails());
     maximumPageSize = new ListBox();
-    for (final short v : PAGESIZE_CHOICES) {
+    for (final int v : PAGESIZE_CHOICES) {
       maximumPageSize.addItem(Util.M.rowsPerPage(v), String.valueOf(v));
     }
 
     reviewCategoryStrategy = new ListBox();
     reviewCategoryStrategy.addItem(
         Util.C.messageShowInReviewCategoryNone(),
-        AccountGeneralPreferences.ReviewCategoryStrategy.NONE.name());
+        GeneralPreferencesInfo.ReviewCategoryStrategy.NONE.name());
     reviewCategoryStrategy.addItem(
         Util.C.messageShowInReviewCategoryName(),
-        AccountGeneralPreferences.ReviewCategoryStrategy.NAME.name());
+        GeneralPreferencesInfo.ReviewCategoryStrategy.NAME.name());
     reviewCategoryStrategy.addItem(
         Util.C.messageShowInReviewCategoryEmail(),
-        AccountGeneralPreferences.ReviewCategoryStrategy.EMAIL.name());
+        GeneralPreferencesInfo.ReviewCategoryStrategy.EMAIL.name());
     reviewCategoryStrategy.addItem(
         Util.C.messageShowInReviewCategoryUsername(),
-        AccountGeneralPreferences.ReviewCategoryStrategy.USERNAME.name());
+        GeneralPreferencesInfo.ReviewCategoryStrategy.USERNAME.name());
     reviewCategoryStrategy.addItem(
         Util.C.messageShowInReviewCategoryAbbrev(),
-        AccountGeneralPreferences.ReviewCategoryStrategy.ABBREV.name());
+        GeneralPreferencesInfo.ReviewCategoryStrategy.ABBREV.name());
+
+    emailStrategy = new ListBox();
+    emailStrategy.addItem(Util.C.messageEnabled(),
+        GeneralPreferencesInfo.EmailStrategy.ENABLED.name());
+    emailStrategy
+        .addItem(
+            Util.C.messageCCMeOnMyComments(),
+            GeneralPreferencesInfo.EmailStrategy.CC_ON_OWN_COMMENTS
+                .name());
+    emailStrategy
+        .addItem(
+            Util.C.messageDisabled(),
+            GeneralPreferencesInfo.EmailStrategy.DISABLED
+                .name());
 
     diffView = new ListBox();
     diffView.addItem(
         com.google.gerrit.client.changes.Util.C.sideBySide(),
-        AccountGeneralPreferences.DiffView.SIDE_BY_SIDE.name());
+        GeneralPreferencesInfo.DiffView.SIDE_BY_SIDE.name());
     diffView.addItem(
         com.google.gerrit.client.changes.Util.C.unifiedDiff(),
-        AccountGeneralPreferences.DiffView.UNIFIED_DIFF.name());
+        GeneralPreferencesInfo.DiffView.UNIFIED_DIFF.name());
 
     Date now = new Date();
     dateFormat = new ListBox();
-    for (AccountGeneralPreferences.DateFormat fmt : AccountGeneralPreferences.DateFormat
-        .values()) {
+    for (GeneralPreferencesInfo.DateFormat fmt
+        : GeneralPreferencesInfo.DateFormat.values()) {
       StringBuilder r = new StringBuilder();
       r.append(DateTimeFormat.getFormat(fmt.getShortFormat()).format(now));
       r.append(" ; ");
@@ -112,8 +126,8 @@
     }
 
     timeFormat = new ListBox();
-    for (AccountGeneralPreferences.TimeFormat fmt : AccountGeneralPreferences.TimeFormat
-        .values()) {
+    for (GeneralPreferencesInfo.TimeFormat fmt
+        : GeneralPreferencesInfo.TimeFormat.values()) {
       StringBuilder r = new StringBuilder();
       r.append(DateTimeFormat.getFormat(fmt.getFormat()).format(now));
       timeFormat.addItem(r.toString(), fmt.name());
@@ -139,24 +153,12 @@
     sizeBarInChangeTable = new CheckBox(Util.C.showSizeBarInChangeTable());
     legacycidInChangeTable = new CheckBox(Util.C.showLegacycidInChangeTable());
     muteCommonPathPrefixes = new CheckBox(Util.C.muteCommonPathPrefixes());
+    signedOffBy = new CheckBox(Util.C.signedOffBy());
 
     boolean flashClippy = !UserAgent.hasJavaScriptClipboard() && UserAgent.Flash.isInstalled();
-    final Grid formGrid = new Grid(10 + (flashClippy ? 1 : 0), 2);
+    final Grid formGrid = new Grid(12 + (flashClippy ? 1 : 0), 2);
 
     int row = 0;
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, showSiteHeader);
-    row++;
-
-    if (flashClippy) {
-      formGrid.setText(row, labelIdx, "");
-      formGrid.setWidget(row, fieldIdx, useFlashClipboard);
-      row++;
-    }
-
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, copySelfOnEmails);
-    row++;
 
     formGrid.setText(row, labelIdx, Util.C.reviewCategoryLabel());
     formGrid.setWidget(row, fieldIdx, reviewCategoryStrategy);
@@ -170,6 +172,18 @@
     formGrid.setWidget(row, fieldIdx, dateTimePanel);
     row++;
 
+    formGrid.setText(row, labelIdx, Util.C.emailFieldLabel());
+    formGrid.setWidget(row, fieldIdx, emailStrategy);
+    row++;
+
+    formGrid.setText(row, labelIdx, Util.C.diffViewLabel());
+    formGrid.setWidget(row, fieldIdx, diffView);
+    row++;
+
+    formGrid.setText(row, labelIdx, "");
+    formGrid.setWidget(row, fieldIdx, showSiteHeader);
+    row++;
+
     formGrid.setText(row, labelIdx, "");
     formGrid.setWidget(row, fieldIdx, relativeDateInChangeTable);
     row++;
@@ -186,8 +200,14 @@
     formGrid.setWidget(row, fieldIdx, muteCommonPathPrefixes);
     row++;
 
-    formGrid.setText(row, labelIdx, Util.C.diffViewLabel());
-    formGrid.setWidget(row, fieldIdx, diffView);
+    formGrid.setText(row, labelIdx, "");
+    formGrid.setWidget(row, fieldIdx, signedOffBy);
+    row++;
+
+    if (flashClippy) {
+      formGrid.setText(row, labelIdx, "");
+      formGrid.setWidget(row, fieldIdx, useFlashClipboard);
+    }
 
     add(formGrid);
 
@@ -208,7 +228,6 @@
     final OnEditEnabler e = new OnEditEnabler(save);
     e.listenTo(showSiteHeader);
     e.listenTo(useFlashClipboard);
-    e.listenTo(copySelfOnEmails);
     e.listenTo(maximumPageSize);
     e.listenTo(dateFormat);
     e.listenTo(timeFormat);
@@ -216,8 +235,10 @@
     e.listenTo(sizeBarInChangeTable);
     e.listenTo(legacycidInChangeTable);
     e.listenTo(muteCommonPathPrefixes);
+    e.listenTo(signedOffBy);
     e.listenTo(diffView);
     e.listenTo(reviewCategoryStrategy);
+    e.listenTo(emailStrategy);
   }
 
   @Override
@@ -229,9 +250,9 @@
     add(extensionPanel);
 
     AccountApi.self().view("preferences")
-        .get(new ScreenLoadCallback<AccountPreferencesInfo>(this) {
+        .get(new ScreenLoadCallback<GeneralPreferences>(this) {
       @Override
-      public void preDisplay(AccountPreferencesInfo prefs) {
+      public void preDisplay(GeneralPreferences prefs) {
         display(prefs);
       }
     });
@@ -240,7 +261,6 @@
   private void enable(final boolean on) {
     showSiteHeader.setEnabled(on);
     useFlashClipboard.setEnabled(on);
-    copySelfOnEmails.setEnabled(on);
     maximumPageSize.setEnabled(on);
     dateFormat.setEnabled(on);
     timeFormat.setEnabled(on);
@@ -248,29 +268,34 @@
     sizeBarInChangeTable.setEnabled(on);
     legacycidInChangeTable.setEnabled(on);
     muteCommonPathPrefixes.setEnabled(on);
+    signedOffBy.setEnabled(on);
     reviewCategoryStrategy.setEnabled(on);
     diffView.setEnabled(on);
+    emailStrategy.setEnabled(on);
   }
 
-  private void display(AccountPreferencesInfo p) {
+  private void display(GeneralPreferences p) {
     showSiteHeader.setValue(p.showSiteHeader());
     useFlashClipboard.setValue(p.useFlashClipboard());
-    copySelfOnEmails.setValue(p.copySelfOnEmail());
     setListBox(maximumPageSize, DEFAULT_PAGESIZE, p.changesPerPage());
-    setListBox(dateFormat, AccountGeneralPreferences.DateFormat.STD, //
+    setListBox(dateFormat, GeneralPreferencesInfo.DateFormat.STD, //
         p.dateFormat());
-    setListBox(timeFormat, AccountGeneralPreferences.TimeFormat.HHMM_12, //
+    setListBox(timeFormat, GeneralPreferencesInfo.TimeFormat.HHMM_12, //
         p.timeFormat());
     relativeDateInChangeTable.setValue(p.relativeDateInChangeTable());
     sizeBarInChangeTable.setValue(p.sizeBarInChangeTable());
     legacycidInChangeTable.setValue(p.legacycidInChangeTable());
     muteCommonPathPrefixes.setValue(p.muteCommonPathPrefixes());
+    signedOffBy.setValue(p.signedOffBy());
     setListBox(reviewCategoryStrategy,
-        AccountGeneralPreferences.ReviewCategoryStrategy.NONE,
+        GeneralPreferencesInfo.ReviewCategoryStrategy.NONE,
         p.reviewCategoryStrategy());
     setListBox(diffView,
-        AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
+        GeneralPreferencesInfo.DiffView.SIDE_BY_SIDE,
         p.diffView());
+    setListBox(emailStrategy,
+        GeneralPreferencesInfo.EmailStrategy.ENABLED,
+        p.emailStrategy());
     display(p.my());
   }
 
@@ -282,8 +307,8 @@
     myMenus.display(values);
   }
 
-  private void setListBox(final ListBox f, final short defaultValue,
-      final short currentValue) {
+  private void setListBox(final ListBox f, final int defaultValue,
+      final int currentValue) {
     setListBox(f, String.valueOf(defaultValue), String.valueOf(currentValue));
   }
 
@@ -308,7 +333,7 @@
     }
   }
 
-  private short getListBox(final ListBox f, final short defaultValue) {
+  private int getListBox(final ListBox f, final int defaultValue) {
     final int idx = f.getSelectedIndex();
     if (0 <= idx) {
       return Short.parseShort(f.getValue(idx));
@@ -334,27 +359,31 @@
   }
 
   private void doSave() {
-    AccountPreferencesInfo p = AccountPreferencesInfo.create();
+    GeneralPreferences p = GeneralPreferences.create();
     p.showSiteHeader(showSiteHeader.getValue());
     p.useFlashClipboard(useFlashClipboard.getValue());
-    p.copySelfOnEmail(copySelfOnEmails.getValue());
     p.changesPerPage(getListBox(maximumPageSize, DEFAULT_PAGESIZE));
     p.dateFormat(getListBox(dateFormat,
-        AccountGeneralPreferences.DateFormat.STD,
-        AccountGeneralPreferences.DateFormat.values()));
+        GeneralPreferencesInfo.DateFormat.STD,
+        GeneralPreferencesInfo.DateFormat.values()));
     p.timeFormat(getListBox(timeFormat,
-        AccountGeneralPreferences.TimeFormat.HHMM_12,
-        AccountGeneralPreferences.TimeFormat.values()));
+        GeneralPreferencesInfo.TimeFormat.HHMM_12,
+        GeneralPreferencesInfo.TimeFormat.values()));
     p.relativeDateInChangeTable(relativeDateInChangeTable.getValue());
     p.sizeBarInChangeTable(sizeBarInChangeTable.getValue());
     p.legacycidInChangeTable(legacycidInChangeTable.getValue());
     p.muteCommonPathPrefixes(muteCommonPathPrefixes.getValue());
+    p.signedOffBy(signedOffBy.getValue());
     p.reviewCategoryStrategy(getListBox(reviewCategoryStrategy,
         ReviewCategoryStrategy.NONE,
         ReviewCategoryStrategy.values()));
     p.diffView(getListBox(diffView,
-        AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
-        AccountGeneralPreferences.DiffView.values()));
+        GeneralPreferencesInfo.DiffView.SIDE_BY_SIDE,
+        GeneralPreferencesInfo.DiffView.values()));
+
+    p.emailStrategy(getListBox(emailStrategy,
+        GeneralPreferencesInfo.EmailStrategy.ENABLED,
+        GeneralPreferencesInfo.EmailStrategy.values()));
 
     List<TopMenuItem> items = new ArrayList<>();
     for (List<String> v : myMenus.getValues()) {
@@ -366,9 +395,9 @@
     save.setEnabled(false);
 
     AccountApi.self().view("preferences")
-        .put(p, new GerritCallback<AccountPreferencesInfo>() {
+        .put(p, new GerritCallback<GeneralPreferences>() {
           @Override
-          public void onSuccess(AccountPreferencesInfo prefs) {
+          public void onSuccess(GeneralPreferences prefs) {
             Gerrit.setUserPreferences(prefs);
             enable(true);
             display(prefs);
@@ -395,9 +424,9 @@
         @Override
         public void onClick(ClickEvent event) {
           ConfigServerApi.defaultPreferences(
-              new GerritCallback<AccountPreferencesInfo>() {
+              new GerritCallback<GeneralPreferences>() {
                 @Override
-                public void onSuccess(AccountPreferencesInfo p) {
+                public void onSuccess(GeneralPreferences p) {
                   MyPreferencesScreen.this.display(p.my());
                   widget.setEnabled(true);
                 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
index a8bf3e5..ce5cfd3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
@@ -16,13 +16,13 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.client.ui.ProjectListPopup;
 import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
 import com.google.gerrit.client.ui.RemoteSuggestBox;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AccountProjectWatchInfo;
+import com.google.gwt.core.client.JavaScriptObject;
+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.event.dom.client.KeyCodes;
@@ -36,8 +36,6 @@
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.HorizontalPanel;
 
-import java.util.List;
-
 public class MyWatchedProjectsScreen extends SettingsScreen {
   private Button addNew;
   private RemoteSuggestBox nameBox;
@@ -188,35 +186,40 @@
     nameBox.setEnabled(false);
     filterTxt.setEnabled(false);
 
-    Util.ACCOUNT_SVC.addProjectWatch(projectName, filter,
-        new GerritCallback<AccountProjectWatchInfo>() {
+    final ProjectWatchInfo projectWatchInfo = JavaScriptObject
+        .createObject().cast();
+    projectWatchInfo.project(projectName);
+    projectWatchInfo.filter(filterTxt.getText());
+
+    AccountApi.updateWatchedProject("self", projectWatchInfo,
+        new GerritCallback<JsArray<ProjectWatchInfo>>() {
           @Override
-          public void onSuccess(final AccountProjectWatchInfo result) {
+          public void onSuccess(JsArray<ProjectWatchInfo> watchedProjects) {
             addNew.setEnabled(true);
             nameBox.setEnabled(true);
             filterTxt.setEnabled(true);
 
             nameBox.setText("");
-            watchesTab.insertWatch(result);
+            watchesTab.insertWatch(projectWatchInfo);
           }
 
           @Override
-          public void onFailure(final Throwable caught) {
+          public void onFailure(Throwable caught) {
             addNew.setEnabled(true);
             nameBox.setEnabled(true);
             filterTxt.setEnabled(true);
-
             super.onFailure(caught);
           }
         });
   }
 
   protected void populateWatches() {
-    Util.ACCOUNT_SVC.myProjectWatch(
-        new ScreenLoadCallback<List<AccountProjectWatchInfo>>(this) {
+    AccountApi.getWatchedProjects("self",
+        new GerritCallback<JsArray<ProjectWatchInfo>>() {
       @Override
-      public void preDisplay(final List<AccountProjectWatchInfo> result) {
-        watchesTab.display(result);
+      public void onSuccess(JsArray<ProjectWatchInfo> watchedProjects) {
+        display();
+        watchesTab.display(watchedProjects);
       }
     });
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
index 08effdc..3647baf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
@@ -16,23 +16,22 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.ProjectLink;
-import com.google.gerrit.common.data.AccountProjectWatchInfo;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Project;
+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.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Label;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
 
-public class MyWatchesTable extends FancyFlexTable<AccountProjectWatchInfo> {
+public class MyWatchesTable extends FancyFlexTable<ProjectWatchInfo> {
 
   public MyWatchesTable() {
     table.setWidth("");
@@ -63,22 +62,22 @@
   }
 
   public void deleteChecked() {
-    final Set<AccountProjectWatch.Key> ids = getCheckedIds();
-    if (!ids.isEmpty()) {
-      Util.ACCOUNT_SVC.deleteProjectWatches(ids,
-          new GerritCallback<VoidResult>() {
+    final Set<ProjectWatchInfo> infos = getCheckedProjectWatchInfos();
+    if (!infos.isEmpty()) {
+      AccountApi.deleteWatchedProjects("self", infos,
+          new GerritCallback<JsArray<ProjectWatchInfo>>() {
             @Override
-            public void onSuccess(final VoidResult result) {
-              remove(ids);
+            public void onSuccess(JsArray<ProjectWatchInfo> watchedProjects) {
+              remove(infos);
             }
           });
     }
   }
 
-  protected void remove(Set<AccountProjectWatch.Key> ids) {
+  protected void remove(Set<ProjectWatchInfo> infos) {
     for (int row = 1; row < table.getRowCount();) {
-      final AccountProjectWatchInfo k = getRowItem(row);
-      if (k != null && ids.contains(k.getWatch().getKey())) {
+      final ProjectWatchInfo k = getRowItem(row);
+      if (k != null && infos.contains(k)) {
         table.removeRow(row);
       } else {
         row++;
@@ -86,23 +85,23 @@
     }
   }
 
-  protected Set<AccountProjectWatch.Key> getCheckedIds() {
-    final Set<AccountProjectWatch.Key> ids = new HashSet<>();
+  protected Set<ProjectWatchInfo> getCheckedProjectWatchInfos() {
+    final Set<ProjectWatchInfo> infos = new HashSet<>();
     for (int row = 1; row < table.getRowCount(); row++) {
-      final AccountProjectWatchInfo k = getRowItem(row);
+      final ProjectWatchInfo k = getRowItem(row);
       if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-        ids.add(k.getWatch().getKey());
+        infos.add(k);
       }
     }
-    return ids;
+    return infos;
   }
 
-  public void insertWatch(final AccountProjectWatchInfo k) {
-    final String newName = k.getProject().getName();
+  public void insertWatch(final ProjectWatchInfo k) {
+    final String newName = k.project();
     int row = 1;
     for (; row < table.getRowCount(); row++) {
-      final AccountProjectWatchInfo i = getRowItem(row);
-      if (i != null && i.getProject().getName().compareTo(newName) >= 0) {
+      final ProjectWatchInfo i = getRowItem(row);
+      if (i != null && i.project().compareTo(newName) >= 0) {
         break;
       }
     }
@@ -112,24 +111,25 @@
     populate(row, k);
   }
 
-  public void display(final List<AccountProjectWatchInfo> result) {
+  public void display(final JsArray<ProjectWatchInfo> result) {
     while (2 < table.getRowCount()) {
       table.removeRow(table.getRowCount() - 1);
     }
 
-    for (final AccountProjectWatchInfo k : result) {
+    for (ProjectWatchInfo info : Natives.asList(result)) {
       final int row = table.getRowCount();
       table.insertRow(row);
       applyDataRowStyle(row);
-      populate(row, k);
+      populate(row, info);
     }
   }
 
-  protected void populate(final int row, final AccountProjectWatchInfo info) {
+  protected void populate(final int row, final ProjectWatchInfo info) {
     final FlowPanel fp = new FlowPanel();
-    fp.add(new ProjectLink(info.getProject().getNameKey()));
-    if (info.getWatch().getFilter() != null) {
-      Label filter = new Label(info.getWatch().getFilter());
+    fp.add(new ProjectLink(info.project(),
+        new Project.NameKey(info.project())));
+    if (info.filter() != null) {
+      Label filter = new Label(info.filter());
       filter.setStyleName(Gerrit.RESOURCES.css().watchedProjectFilter());
       fp.add(filter);
     }
@@ -137,11 +137,11 @@
     table.setWidget(row, 1, new CheckBox());
     table.setWidget(row, 2, fp);
 
-    addNotifyButton(AccountProjectWatch.NotifyType.NEW_CHANGES, info, row, 3);
-    addNotifyButton(AccountProjectWatch.NotifyType.NEW_PATCHSETS, info, row, 4);
-    addNotifyButton(AccountProjectWatch.NotifyType.ALL_COMMENTS, info, row, 5);
-    addNotifyButton(AccountProjectWatch.NotifyType.SUBMITTED_CHANGES, info, row, 6);
-    addNotifyButton(AccountProjectWatch.NotifyType.ABANDONED_CHANGES, info, row, 7);
+    addNotifyButton(ProjectWatchInfo.Type.NEW_CHANGES, info, row, 3);
+    addNotifyButton(ProjectWatchInfo.Type.NEW_PATCHSETS, info, row, 4);
+    addNotifyButton(ProjectWatchInfo.Type.ALL_COMMENTS, info, row, 5);
+    addNotifyButton(ProjectWatchInfo.Type.SUBMITTED_CHANGES, info, row, 6);
+    addNotifyButton(ProjectWatchInfo.Type.ABANDONED_CHANGES, info, row, 7);
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
     fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
@@ -155,27 +155,28 @@
     setRowItem(row, info);
   }
 
-  protected void addNotifyButton(final AccountProjectWatch.NotifyType type,
-      final AccountProjectWatchInfo info, final int row, final int col) {
+  protected void addNotifyButton(final ProjectWatchInfo.Type type,
+      final ProjectWatchInfo info, final int row, final int col) {
     final CheckBox cbox = new CheckBox();
 
     cbox.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
-        final boolean oldVal = info.getWatch().isNotify(type);
-        info.getWatch().setNotify(type, cbox.getValue());
+        final Boolean oldVal = info.notify(type);
+        info.notify(type, cbox.getValue());
         cbox.setEnabled(false);
-        Util.ACCOUNT_SVC.updateProjectWatch(info.getWatch(),
-            new GerritCallback<VoidResult>() {
+
+        AccountApi.updateWatchedProject("self", info,
+            new GerritCallback<JsArray<ProjectWatchInfo>>() {
               @Override
-              public void onSuccess(final VoidResult result) {
+              public void onSuccess(JsArray<ProjectWatchInfo> watchedProjects) {
                 cbox.setEnabled(true);
               }
 
               @Override
-              public void onFailure(final Throwable caught) {
+              public void onFailure(Throwable caught) {
                 cbox.setEnabled(true);
-                info.getWatch().setNotify(type, oldVal);
+                info.notify(type, oldVal);
                 cbox.setValue(oldVal);
                 super.onFailure(caught);
               }
@@ -183,7 +184,7 @@
       }
     });
 
-    cbox.setValue(info.getWatch().isNotify(type));
+    cbox.setValue(info.notify(type));
     table.setWidget(row, col, cbox);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ProjectWatchInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ProjectWatchInfo.java
new file mode 100644
index 0000000..e43ec0c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ProjectWatchInfo.java
@@ -0,0 +1,79 @@
+// 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.account;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ProjectWatchInfo extends JavaScriptObject {
+
+  public enum Type {
+    NEW_CHANGES,
+    NEW_PATCHSETS,
+    ALL_COMMENTS,
+    SUBMITTED_CHANGES,
+    ABANDONED_CHANGES
+  }
+
+  public final native String project() /*-{ return this.project; }-*/;
+  public final native String filter() /*-{ return this.filter; }-*/;
+
+  public final native void project(String s) /*-{ this.project = s; }-*/;
+  public final native void filter(String s) /*-{ this.filter = s; }-*/;
+
+  public final void notify(ProjectWatchInfo.Type t, Boolean b) {
+    if (t == ProjectWatchInfo.Type.NEW_CHANGES) {
+      notifyNewChanges(b.booleanValue());
+    } else if (t == Type.NEW_PATCHSETS) {
+      notifyNewPatchSets(b.booleanValue());
+    } else if (t == Type.ALL_COMMENTS) {
+      notifyAllComments(b.booleanValue());
+    } else if (t == Type.SUBMITTED_CHANGES) {
+      notifySubmittedChanges(b.booleanValue());
+    } else if (t == Type.ABANDONED_CHANGES) {
+      notifyAbandonedChanges(b.booleanValue());
+    }
+  }
+
+  public final Boolean notify(ProjectWatchInfo.Type t) {
+    boolean b = false;
+    if (t == ProjectWatchInfo.Type.NEW_CHANGES) {
+      b = notifyNewChanges();
+    } else if (t == Type.NEW_PATCHSETS) {
+      b = notifyNewPatchSets();
+    } else if (t == Type.ALL_COMMENTS) {
+      b = notifyAllComments();
+    } else if (t == Type.SUBMITTED_CHANGES) {
+      b = notifySubmittedChanges();
+    } else if (t == Type.ABANDONED_CHANGES) {
+      b = notifyAbandonedChanges();
+    }
+    return Boolean.valueOf(b);
+  }
+
+  private native boolean notifyNewChanges() /*-{ return this['notify_new_changes'] ? true : false; }-*/;
+  private native boolean notifyNewPatchSets() /*-{ return this['notify_new_patch_sets'] ? true : false; }-*/;
+  private native boolean notifyAllComments() /*-{ return this['notify_all_comments'] ? true : false; }-*/;
+  private native boolean notifySubmittedChanges() /*-{ return this['notify_submitted_changes'] ? true : false; }-*/;
+  private native boolean notifyAbandonedChanges() /*-{ return this['notify_abandoned_changes'] ? true : false; }-*/;
+
+  private native void notifyNewChanges(boolean b) /*-{ this['notify_new_changes'] = b ? true : null; }-*/;
+  private native void notifyNewPatchSets(boolean b) /*-{ this['notify_new_patch_sets'] = b ? true : null; }-*/;
+  private native void notifyAllComments(boolean b) /*-{ this['notify_all_comments'] = b ? true : null; }-*/;
+  private native void notifySubmittedChanges(boolean b) /*-{ this['notify_submitted_changes'] = b ? true : null; }-*/;
+  private native void notifyAbandonedChanges(boolean b) /*-{ this['notify_abandoned_changes'] = b ? true : null; }-*/;
+
+  protected ProjectWatchInfo() {
+
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
index e8c58ef..ee7407e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
@@ -47,6 +47,10 @@
     if (Gerrit.info().auth().isHttpPasswordSettingsEnabled()) {
       linkByGerrit(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
     }
+    if (Gerrit.info().auth().isOAuth()
+        && Gerrit.info().auth().isGitBasicAuth()) {
+      linkByGerrit(Util.C.tabOAuthToken(), PageLinks.SETTINGS_OAUTH_TOKEN);
+    }
     if (Gerrit.info().gerrit().editGpgKeys()) {
       linkByGerrit(Util.C.tabGpgKeys(), PageLinks.SETTINGS_GPGKEYS);
     }
@@ -81,7 +85,7 @@
 
   private void linkByPlugin(String pluginName, String text, String target) {
     if (ambiguousMenuNames.contains(text)) {
-      text += " ("+ pluginName + ")";
+      text += " (" + pluginName + ")";
     }
     link(text, target);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
index 8f5d7a8..39849c4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
@@ -57,11 +57,6 @@
     this(null, null, change, null, revision, action);
   }
 
-  public ActionButton(ChangeInfo change, EditInfo edit,
-      ActionInfo action) {
-    this(null, null, change, edit, null, action);
-  }
-
   private ActionButton(Project.NameKey project, BranchInfo branch,
       ChangeInfo change, EditInfo edit, RevisionInfo revision,
       ActionInfo action) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
index 52f3588..3710265 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
@@ -88,9 +88,6 @@
   .addContainer:hover {
     background-color: selectionColor;
   }
-  .addSelector {
-    font-size: 80%;
-  }
 
   .deleteIcon {
     position: absolute;
@@ -139,9 +136,7 @@
       ui:field='permissionContainer'
       styleName='{style.permissionList}'/>
   <div ui:field='addContainer' class='{style.addContainer}'>
-    <g:ValueListBox
-        ui:field='permissionSelector'
-        styleName='{style.addSelector}' />
+    <g:ValueListBox ui:field='permissionSelector'/>
   </div>
 </div>
 
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 cd72686..a71dffe 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
@@ -211,7 +211,9 @@
   protected void display(final GroupInfo group, final boolean canModify) {
     groupUUIDLabel.setText(group.getGroupUUID().get());
     groupNameTxt.setText(group.name());
-    ownerTxt.setText(group.owner() != null?group.owner():Util.M.deletedReference(group.getOwnerUUID().get()));
+    ownerTxt.setText(group.owner() != null
+        ? group.owner()
+        : Util.M.deletedReference(group.getOwnerUUID().get()));
     descTxt.setText(group.description());
     visibleToAllCheckBox.setValue(group.options().isVisibleToAll());
     setMembersTabVisible(AccountGroup.isInternalGroup(group.getGroupUUID()));
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 ad3b3f8..8c00ba7 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
@@ -48,9 +48,8 @@
   private String getTabToken(final String token, final String tab) {
     if (token.startsWith("/admin/groups/uuid-")) {
       return toGroup(group.getGroupUUID(), tab);
-    } else {
-      return toGroup(group.getGroupId(), tab);
     }
+    return toGroup(group.getGroupId(), tab);
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 511be5f..984c5a3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -45,6 +45,7 @@
   String enableSignedPush();
   String requireSignedPush();
   String requireChangeID();
+  String rejectImplicitMerges();
   String headingMaxObjectSizeLimit();
   String headingGroupOptions();
   String isVisibleToAll();
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..2fe5978 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
@@ -27,6 +27,7 @@
 enableSignedPush = Enable signed push
 requireSignedPush = Require signed push
 requireChangeID = Require <code>Change-Id</code> in commit message
+rejectImplicitMerges = Reject implicit merges when changes are pushed for review
 headingMaxObjectSizeLimit = Maximum Git object size limit
 headingGroupOptions = Group Options
 isVisibleToAll = Make group visible to all registered users.
@@ -120,6 +121,7 @@
 # Permission Names
 permissionNames = \
 	abandon, \
+	addPatchSet, \
 	create, \
 	deleteDrafts, \
 	editHashtags, \
@@ -141,6 +143,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/AdminResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
index f1ac27f..688a59f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
@@ -19,17 +19,24 @@
 import com.google.gwt.resources.client.ImageResource;
 
 public interface AdminResources extends ClientBundle {
-  public static final AdminResources I = GWT.create(AdminResources.class);
+  AdminResources I = GWT.create(AdminResources.class);
 
   @Source("admin.css")
   AdminCss css();
 
+  /**
+   * unknown origin
+   * TODO replace icons
+   */
   @Source("deleteNormal.png")
-  public ImageResource deleteNormal();
+  ImageResource deleteNormal();
 
   @Source("deleteHover.png")
-  public ImageResource deleteHover();
+  ImageResource deleteHover();
 
-  @Source("undoNormal.png")
-  public ImageResource undoNormal();
+  /**
+   * silk icons (CC-BY3.0): http://famfamfam.com/lab/icons/silk/
+   */
+  @Source("arrow_undo.png")
+  ImageResource undoNormal();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index bfe7787..86222ce 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -126,7 +126,7 @@
       @Override
       public void onKeyUp(KeyUpEvent event) {
         Query q = new Query(filterTxt.getValue())
-          .open(event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER);
+          .open(event.getNativeKeyCode() == KeyCodes.KEY_ENTER);
         if (match.equals(q.qMatch)) {
           q.start(start);
         }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
index c1082bc..d6d0fe3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
@@ -47,7 +47,7 @@
             toValue(event.getSelectedItem()));
       }
     });
-    suggestBox.addCloseHandler(new CloseHandler<RemoteSuggestBox>(){
+    suggestBox.addCloseHandler(new CloseHandler<RemoteSuggestBox>() {
       @Override
       public void onClose(CloseEvent<RemoteSuggestBox> event) {
         suggestBox.setText("");
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 aed1dc2..64fc0e5 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
@@ -113,7 +113,7 @@
         return a.name().compareTo(b.name());
       }
     });
-    for(GroupInfo group : list.subList(fromIndex, toIndex)) {
+    for (GroupInfo group : list.subList(fromIndex, toIndex)) {
       final int row = table.getRowCount();
       table.insertRow(row);
       applyDataRowStyle(row);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
index 6349803..66738c0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
@@ -21,7 +21,7 @@
 
 abstract class PaginatedProjectScreen extends ProjectScreen {
   protected int pageSize;
-  protected String match;
+  protected String match = "";
   protected int start;
 
   PaginatedProjectScreen(Project.NameKey toShow) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
index 7678097..025176c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.groups.GroupMap;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.SuggestUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupInfo;
@@ -243,14 +243,16 @@
       // If the oracle didn't get to complete a UUID, resolve it now.
       //
       addRule.setEnabled(false);
-      SuggestUtil.SVC.suggestAccountGroupForProject(
-          projectName, ref.getName(), 1,
-          new GerritCallback<List<GroupReference>>() {
+      GroupMap.suggestAccountGroupForProject(
+          projectName.get(), ref.getName(), 1,
+          new GerritCallback<GroupMap>() {
             @Override
-            public void onSuccess(List<GroupReference> result) {
+            public void onSuccess(GroupMap result) {
               addRule.setEnabled(true);
-              if (result.size() == 1) {
-                addGroup(result.get(0));
+              if (result.values().length() == 1) {
+                addGroup(new GroupReference(
+                    result.values().get(0).getGroupUUID(),
+                    result.values().get(0).name()));
               } else {
                 groupToAdd.setFocus(true);
                 new ErrorDialog(Gerrit.M.noSuchGroupMessage(ref.getName()))
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
index 177faff0..f29619a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
@@ -15,16 +15,12 @@
 package com.google.gerrit.client.admin;
 
 import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.GitwebInfo;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.ParentProjectBox;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.common.data.WebLinkInfoCommon;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.DivElement;
 import com.google.gwt.dom.client.Style.Display;
@@ -156,32 +152,14 @@
   }
 
   private void setUpWebLinks() {
-    if (!value.isConfigVisible()) {
+    List<WebLinkInfoCommon> links = value.getFileHistoryLinks();
+    if (!value.isConfigVisible() || links == null || links.isEmpty()) {
       history.getStyle().setDisplay(Display.NONE);
-    } else {
-      GitwebInfo c = Gerrit.info().gitweb();
-      List<WebLinkInfoCommon> links = value.getFileHistoryLinks();
-      if (c == null && links == null) {
-        history.getStyle().setDisplay(Display.NONE);
-      }
-      if (c != null) {
-        webLinkPanel.add(toAnchor(c.toFileHistory(new Branch.NameKey(value.getProjectName(),
-            RefNames.REFS_CONFIG), "project.config"), c.getLinkName()));
-      }
-
-      if (links != null) {
-        for (WebLinkInfoCommon link : links) {
-          webLinkPanel.add(toAnchor(link));
-        }
-      }
+      return;
     }
-  }
-
-  private Anchor toAnchor(String href, String name) {
-    Anchor a = new Anchor();
-    a.setHref(href);
-    a.setText(name);
-    return a;
+    for (WebLinkInfoCommon link : links) {
+      webLinkPanel.add(toAnchor(link));
+    }
   }
 
   private static Anchor toAnchor(WebLinkInfoCommon info) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
index ebe6caf..7fbf70d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
@@ -48,7 +48,6 @@
 
   .addContainer {
     margin-top: 5px;
-    font-size: 80%;
   }
   .addContainer:hover {
     background-color: selectionColor;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index b86d0a8..6345207 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -183,7 +183,7 @@
     driver.edit(mock);
   }
 
-  @UiHandler(value={"cancel1", "cancel2"})
+  @UiHandler(value = {"cancel1", "cancel2"})
   void onCancel(@SuppressWarnings("unused") ClickEvent event) {
     Gerrit.display(PageLinks.toProjectAcceess(getProjectKey()));
   }
@@ -231,19 +231,20 @@
 
           private Set<String> getDiffs(ProjectAccess wantedAccess,
               ProjectAccess newAccess) {
-            final List<AccessSection> wantedSections =
+            List<AccessSection> wantedSections =
                 mergeSections(removeEmptyPermissionsAndSections(wantedAccess.getLocal()));
-            final HashSet<AccessSection> same = new HashSet<>(wantedSections);
-            final HashSet<AccessSection> different =
-                new HashSet<>(wantedSections.size()
-                    + newAccess.getLocal().size());
+            List<AccessSection> newSections =
+                removeEmptyPermissionsAndSections(newAccess.getLocal());
+            HashSet<AccessSection> same = new HashSet<>(wantedSections);
+            HashSet<AccessSection> different =
+                new HashSet<>(wantedSections.size() + newSections.size());
             different.addAll(wantedSections);
-            different.addAll(newAccess.getLocal());
-            same.retainAll(newAccess.getLocal());
+            different.addAll(newSections);
+            same.retainAll(newSections);
             different.removeAll(same);
 
-            final Set<String> differentNames = new HashSet<>();
-            for (final AccessSection s : different) {
+            Set<String> differentNames = new HashSet<>();
+            for (AccessSection s : different) {
               differentNames.add(s.getName());
             }
             return differentNames;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index 606a2ea..31edefc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.client.access.ProjectAccessInfo;
 import com.google.gerrit.client.actions.ActionButton;
 import com.google.gerrit.client.info.ActionInfo;
-import com.google.gerrit.client.info.GitwebInfo;
 import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.projects.BranchInfo;
 import com.google.gerrit.client.projects.ProjectApi;
@@ -39,7 +38,6 @@
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.PagingHyperlink;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gwt.core.client.JsArray;
@@ -54,7 +52,6 @@
 import com.google.gwt.event.dom.client.KeyUpHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
@@ -408,8 +405,6 @@
     }
 
     void populate(int row, BranchInfo k) {
-      GitwebInfo c = Gerrit.info().gitweb();
-
       if (k.canDelete()) {
         CheckBox sel = new CheckBox();
         sel.addValueChangeHandler(updateDeleteHandler);
@@ -432,10 +427,6 @@
       }
 
       FlowPanel actionsPanel = new FlowPanel();
-      if (c != null) {
-        actionsPanel.add(new Anchor(c.getLinkName(), false,
-            c.toBranch(new Branch.NameKey(getProjectKey(), k.ref()))));
-      }
       if (k.webLinks() != null) {
         for (WebLinkInfo webLink : Natives.asList(k.webLinks())) {
           actionsPanel.add(webLink.toAnchor());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index c6bd1d1..e1cfa90 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -85,6 +85,7 @@
   private ListBox newChangeForAllNotInTarget;
   private ListBox enableSignedPush;
   private ListBox requireSignedPush;
+  private ListBox rejectImplicitMerges;
   private NpTextBox maxObjectSizeLimit;
   private Label effectiveMaxObjectSizeLimit;
   private Map<String, Map<String, HasEnabled>> pluginConfigWidgets;
@@ -184,6 +185,7 @@
     contributorAgreements.setEnabled(isOwner);
     signedOffBy.setEnabled(isOwner);
     requireChangeID.setEnabled(isOwner);
+    rejectImplicitMerges.setEnabled(isOwner);
     maxObjectSizeLimit.setEnabled(isOwner);
 
     if (pluginConfigWidgets != null) {
@@ -253,6 +255,10 @@
       grid.add(Util.C.requireSignedPush(), requireSignedPush);
     }
 
+    rejectImplicitMerges = newInheritedBooleanBox();
+    saveEnabler.listenTo(rejectImplicitMerges);
+    grid.addHtml(Util.C.rejectImplicitMerges(), rejectImplicitMerges);
+
     maxObjectSizeLimit = new NpTextBox();
     saveEnabler.listenTo(maxObjectSizeLimit);
     effectiveMaxObjectSizeLimit = new Label();
@@ -383,6 +389,7 @@
       setBool(enableSignedPush, result.enableSignedPush());
       setBool(requireSignedPush, result.requireSignedPush());
     }
+    setBool(rejectImplicitMerges, result.rejectImplicitMerges());
     setSubmitType(result.submitType());
     setState(result.state());
     maxObjectSizeLimit.setText(result.maxObjectSizeLimit().configuredValue());
@@ -659,7 +666,7 @@
     ProjectApi.setConfig(getProjectKey(), descTxt.getText().trim(),
         getBool(contributorAgreements), getBool(contentMerge),
         getBool(signedOffBy), getBool(newChangeForAllNotInTarget), getBool(requireChangeID),
-        esp, rsp,
+        esp, rsp, getBool(rejectImplicitMerges),
         maxObjectSizeLimit.getText().trim(),
         SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
         ProjectState.valueOf(state.getValue(state.getSelectedIndex())),
@@ -727,7 +734,7 @@
   private static class LabeledWidgetsGrid extends FlexTable {
     private String labelSuffix;
 
-    public LabeledWidgetsGrid() {
+    LabeledWidgetsGrid() {
       super();
       labelSuffix = ":";
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index 4503265..98eeb02 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -18,7 +18,6 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.info.GitwebInfo;
 import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.projects.ProjectInfo;
 import com.google.gerrit.client.projects.ProjectMap;
@@ -34,7 +33,6 @@
 import com.google.gwt.event.dom.client.KeyUpEvent;
 import com.google.gwt.event.dom.client.KeyUpHandler;
 import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.Image;
@@ -124,6 +122,7 @@
             state.setTitle(Util.toLongString(k.state()));
             table.setWidget(row, ProjectsTable.C_STATE, state);
             break;
+          case ACTIVE:
           default:
             // Intentionally left blank, do not show an icon when active.
             break;
@@ -140,22 +139,12 @@
       }
 
       private void addWebLinks(int row, ProjectInfo k) {
-        GitwebInfo gitwebLink = Gerrit.info().gitweb();
         List<WebLinkInfo> webLinks = Natives.asList(k.webLinks());
-        if (gitwebLink != null || (webLinks != null && !webLinks.isEmpty())) {
+        if (webLinks != null && !webLinks.isEmpty()) {
           FlowPanel p = new FlowPanel();
           table.setWidget(row, ProjectsTable.C_REPO_BROWSER, p);
-
-          if (gitwebLink != null) {
-            Anchor a = new Anchor();
-            a.setText(gitwebLink.getLinkName());
-            a.setHref(gitwebLink.toProject(k.name_key()));
-            p.add(a);
-          }
-          if (webLinks != null) {
-            for (WebLinkInfo weblink : webLinks) {
-              p.add(weblink.toAnchor());
-            }
+          for (WebLinkInfo weblink : webLinks) {
+            p.add(weblink.toAnchor());
           }
         }
       }
@@ -182,7 +171,7 @@
       @Override
       public void onKeyUp(KeyUpEvent event) {
         Query q = new Query(filterTxt.getValue())
-          .open(event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER);
+          .open(event.getNativeKeyCode() == KeyCodes.KEY_ENTER);
         if (match.equals(q.qMatch)) {
           q.start(start);
         }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
index 088899e..b549528 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
@@ -33,9 +33,8 @@
     public String render(Integer object) {
       if (0 <= object) {
         return "+" + object;
-      } else {
-        return String.valueOf(object);
       }
+      return String.valueOf(object);
     }
 
     @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/arrow_undo.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/arrow_undo.png
new file mode 100644
index 0000000..6972c5e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/arrow_undo.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/undoNormal.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/undoNormal.png
deleted file mode 100644
index b780f75..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/undoNormal.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
index 18fb833..626252a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
@@ -17,10 +17,11 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.info.AccountInfo;
-import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gerrit.client.info.GeneralPreferences;
 import com.google.gerrit.client.info.ServerInfo;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
+import com.google.gwt.dom.client.Element;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.Window;
 
@@ -235,14 +236,14 @@
     }
   }
 
-  private static final String getPluginName() {
+  private static String getPluginName() {
     if (pluginName != null) {
       return pluginName;
     }
     return PluginName.fromUrl(PluginName.getCallerUrl());
   }
 
-  private static final void go(String urlOrToken) {
+  private static void go(String urlOrToken) {
     if (urlOrToken.startsWith("http:")
         || urlOrToken.startsWith("https:")
         || urlOrToken.startsWith("//")) {
@@ -252,35 +253,35 @@
     }
   }
 
-  private static final void refresh() {
+  private static void refresh() {
     Gerrit.display(History.getToken());
   }
 
-  private static final ServerInfo getServerInfo() {
+  private static ServerInfo getServerInfo() {
     return Gerrit.info();
   }
 
-  private static final AccountInfo getCurrentUser() {
+  private static AccountInfo getCurrentUser() {
     return Gerrit.getUserAccount();
   }
 
-  private static final AccountPreferencesInfo getUserPreferences() {
+  private static GeneralPreferences getUserPreferences() {
     return Gerrit.getUserPreferences();
   }
 
-  private static final void refreshUserPreferences() {
+  private static void refreshUserPreferences() {
     Gerrit.refreshUserPreferences();
   }
 
-  private static final void refreshMenuBar() {
+  private static void refreshMenuBar() {
     Gerrit.refreshMenuBar();
   }
 
-  private static final boolean isSignedIn() {
+  private static boolean isSignedIn() {
     return Gerrit.isSignedIn();
   }
 
-  private static final void showError(String message) {
+  private static void showError(String message) {
     new ErrorDialog(message).center();
   }
 
@@ -296,6 +297,13 @@
     }
   }
 
+  public static final void fireEvent(String event, Element e) {
+    JsArray<JavaScriptObject> h = getEventHandlers(event);
+    for (int i = 0; i < h.length(); i++) {
+      invoke(h.get(i), e);
+    }
+  }
+
   static final void fireEvent(String event, JavaScriptObject a, JavaScriptObject b) {
     JsArray<JavaScriptObject> h = getEventHandlers(event);
     for (int i = 0; i < h.length(); i++) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
index 82b6810..1e77773 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
@@ -55,11 +55,11 @@
     }
   }
 
-  private static final native JavaScriptObject get(String id) /*-{
+  private static native JavaScriptObject get(String id) /*-{
     return $wnd.Gerrit.change_actions[id];
   }-*/;
 
-  private static final native boolean invoke(JavaScriptObject h,
+  private static native boolean invoke(JavaScriptObject h,
       ChangeInfo a, RevisionInfo r)
   /*-{ return h(a,r) }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
index 49b150a..b7e3df3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
@@ -45,7 +45,7 @@
     }
   }
 
-  private static final native JavaScriptObject get(String id) /*-{
+  private static native JavaScriptObject get(String id) /*-{
     return $wnd.Gerrit.edit_actions[id];
   }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
index 0702cba..e20c577 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
@@ -130,14 +130,14 @@
     final native void putBoolean(String k, boolean v) /*-{ this.p[k] = v; }-*/;
     final native void putObject(String k, JavaScriptObject v) /*-{ this.p[k] = v; }-*/;
 
-    private static final native Context create(
+    private static native Context create(
         JavaScriptObject T,
         Definition d,
         Element e)
     /*-{ return new T(d,e) }-*/;
 
     private static final JavaScriptObject TYPE = init();
-    private static final native JavaScriptObject init() /*-{
+    private static native JavaScriptObject init() /*-{
       var T = function(d,e) {
         this._d = d;
         this._u = [];
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionScreen.java
index 98b06bb..19dbe06 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionScreen.java
@@ -109,7 +109,7 @@
     final native void onLoad() /*-{ this._d.onLoad(this) }-*/;
     final native JsArray<JavaScriptObject> unload() /*-{ return this._u }-*/;
 
-    private static final native Context create(
+    private static native Context create(
         JavaScriptObject T,
         Definition d,
         ExtensionScreen s,
@@ -118,7 +118,7 @@
     /*-{ return new T(d,s,e,m) }-*/;
 
     private static final JavaScriptObject TYPE = init();
-    private static final native JavaScriptObject init() /*-{
+    private static native JavaScriptObject init() /*-{
       var T = function(d,s,e,m) {
         this._d = d;
         this._s = s;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java
index c351bbf..1d7259b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java
@@ -91,7 +91,7 @@
       return Natives.keys(settingsScreens());
     }
 
-    private static final native NativeMap<NativeString> settingsScreens()
+    private static native NativeMap<NativeString> settingsScreens()
     /*-{ return $wnd.Gerrit.settingsScreens; }-*/;
 
     public final native String getPath() /*-{ return this.path; }-*/;
@@ -114,7 +114,7 @@
     final native void onLoad() /*-{ this._d.onLoad(this) }-*/;
     final native JsArray<JavaScriptObject> unload() /*-{ return this._u }-*/;
 
-    private static final native Context create(
+    private static native Context create(
         JavaScriptObject T,
         Definition d,
         ExtensionSettingsScreen s,
@@ -122,7 +122,7 @@
     /*-{ return new T(d,s,e) }-*/;
 
     private static final JavaScriptObject TYPE = init();
-    private static final native JavaScriptObject init() /*-{
+    private static native JavaScriptObject init() /*-{
       var T = function(d,s,e) {
         this._d = d;
         this._s = s;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java
index 95757ab..6af244a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java
@@ -62,19 +62,19 @@
     };
   }-*/;
 
-  private static final String css(String css) {
+  private static String css(String css) {
     String name = DOM.createUniqueId();
     StyleInjector.inject("." + name + "{" + css + "}");
     return name;
   }
 
-  private static final String id(IdMap idMap, String key) {
+  private static String id(IdMap idMap, String key) {
     String id = DOM.createUniqueId();
     idMap.put(id, key);
     return " id='" + id + "'";
   }
 
-  private static final String html(ReplacementMap opts, String id) {
+  private static String html(ReplacementMap opts, String id) {
     int d = id.indexOf('.');
     if (0 < d) {
       String name = id.substring(0, d);
@@ -84,7 +84,7 @@
     return new SafeHtmlBuilder().append(opts.str(id)).asString();
   }
 
-  private static final Node parseHtml(
+  private static Node parseHtml(
       String html,
       IdMap ids,
       ReplacementMap opts,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
index 04d4ce5..fb549ee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
@@ -26,14 +26,14 @@
     return create(TYPE, url, name);
   }
 
-  final native String url() /*-{ return this._scriptUrl }-*/;
-  final native String name() /*-{ return this.name }-*/;
+  native String url() /*-{ return this._scriptUrl }-*/;
+  native String name() /*-{ return this.name }-*/;
 
-  final native boolean loaded() /*-{ return this._success || this._failure != null }-*/;
-  final native Exception failure() /*-{ return this._failure }-*/;
-  final native void failure(Exception e) /*-{ this._failure = e }-*/;
-  final native boolean success() /*-{ return this._success || false }-*/;
-  final native void _initialized() /*-{ this._success = true }-*/;
+  native boolean loaded() /*-{ return this._success || this._failure != null }-*/;
+  native Exception failure() /*-{ return this._failure }-*/;
+  native void failure(Exception e) /*-{ this._failure = e }-*/;
+  native boolean success() /*-{ return this._success || false }-*/;
+  native void _initialized() /*-{ this._success = true }-*/;
 
   private static native Plugin create(JavaScriptObject T, String u, String n)
   /*-{ return new T(u,n) }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
index a560711..330ec15 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
@@ -76,7 +76,7 @@
   protected static final native JavaScriptException makeException()
   /*-{ try { null.a() } catch (e) { return e } }-*/;
 
-  private static final native boolean hasStack(JavaScriptException e)
+  private static native boolean hasStack(JavaScriptException e)
   /*-{ return !!e.stack }-*/;
 
   /** Extracts URL from the stack frame. */
@@ -103,7 +103,7 @@
       return UNKNOWN;
     }
 
-    private static final native JsArrayString getStack(JavaScriptException e)
+    private static native JsArrayString getStack(JavaScriptException e)
     /*-{ return e.stack ? e.stack.split('\n') : [] }-*/;
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
index 69887e7..f9084d9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
@@ -61,11 +61,11 @@
     }
   }
 
-  private static final native JavaScriptObject projectAction(String id) /*-{
+  private static native JavaScriptObject projectAction(String id) /*-{
     return $wnd.Gerrit.project_actions[id];
   }-*/;
 
-  private static final native JavaScriptObject branchAction(String id) /*-{
+  private static native JavaScriptObject branchAction(String id) /*-{
     return $wnd.Gerrit.branch_actions[id];
   }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
index 914ef85..50ebce7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
@@ -46,7 +46,7 @@
     }
   }
 
-  private static final native JavaScriptObject get(String id) /*-{
+  private static native JavaScriptObject get(String id) /*-{
     return $wnd.Gerrit.revision_actions[id];
   }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/blame/BlameInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/blame/BlameInfo.java
new file mode 100644
index 0000000..bbd939a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/blame/BlameInfo.java
@@ -0,0 +1,31 @@
+// 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.client.blame;
+
+import com.google.gerrit.client.RangeInfo;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+public class BlameInfo extends JavaScriptObject {
+  public final native String author() /*-{ return this.author; }-*/;
+  public final native String id() /*-{ return this.id; }-*/;
+  public final native String commitMsg() /*-{ return this.commit_msg; }-*/;
+  public final native int time() /*-{ return this.time; }-*/;
+  public final native JsArray<RangeInfo> ranges() /*-{ return this.ranges; }-*/;
+
+  protected BlameInfo() {
+  }
+
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
index 28943ab..396bc8a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
@@ -35,7 +35,7 @@
   interface Binder extends UiBinder<HTMLPanel, ActionMessageBox> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
-  static interface Style extends CssResource {
+  interface Style extends CssResource {
     String popup();
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
index 033a3a8..36107ee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -37,7 +37,7 @@
 class Actions extends Composite {
   private static final String[] CORE = {
     "abandon", "cherrypick", "followup", "hashtags", "publish",
-    "rebase", "restore", "revert", "submit", "topic", "/"};
+    "rebase", "restore", "revert", "submit", "topic", "/",};
 
   interface Binder extends UiBinder<FlowPanel, Actions> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
index f5b26d1..63de389 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
@@ -16,7 +16,7 @@
 
 import com.google.gwt.i18n.client.Constants;
 
-interface ChangeConstants extends Constants {
+public interface ChangeConstants extends Constants {
   String previousChange();
   String nextChange();
   String openChange();
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 2673f49..87997d1 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
@@ -45,7 +45,6 @@
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
@@ -116,6 +115,7 @@
   interface Style extends CssResource {
     String avatar();
     String hashtagName();
+    String hashtagIcon();
     String highlight();
     String labelName();
     String label_may();
@@ -126,6 +126,7 @@
     String pushCertStatus();
     String replyBox();
     String selected();
+    String notCurrentPatchSet();
   }
 
   static ChangeScreen get(NativeEvent in) {
@@ -146,6 +147,7 @@
   private boolean hasDraftComments;
   private CommentLinkProcessor commentLinkProcessor;
   private EditInfo edit;
+  private LocalComments lc;
 
   private List<HandlerRegistration> handlers = new ArrayList<>(4);
   private UpdateCheckTimer updateCheck;
@@ -187,6 +189,8 @@
   @UiField Element actionText;
   @UiField Element actionDate;
   @UiField SimplePanel changeExtension;
+  @UiField SimplePanel relatedExtension;
+  @UiField SimplePanel commitExtension;
 
   @UiField Actions actions;
   @UiField Labels labels;
@@ -232,11 +236,14 @@
     this.revision = normalize(revision);
     this.openReplyBox = openReplyBox;
     this.fileTableMode = mode;
+    this.lc = new LocalComments(changeId);
     add(uiBinder.createAndBindUi(this));
   }
 
-  Change.Id getChangeId() {
-    return changeId;
+  PatchSet.Id getPatchSetId() {
+    return new PatchSet.Id(
+        changeInfo.legacyId(),
+        changeInfo.revisions().get(revision)._number());
   }
 
   @Override
@@ -271,30 +278,87 @@
     loadChangeInfo(true, group.addFinal(
         new GerritCallback<ChangeInfo>() {
           @Override
-          public void onSuccess(ChangeInfo info) {
+          public void onSuccess(final ChangeInfo info) {
             info.init();
-            addExtensionPoints(info);
-            loadConfigInfo(info, base);
+            addExtensionPoints(info, initCurrentRevision(info));
+
+            RevisionInfo rev = info.revision(revision);
+            CallbackGroup group = new CallbackGroup();
+            loadCommit(rev, group);
+
+            group.addListener(new GerritCallback<Void>() {
+              @Override
+              public void onSuccess(Void result) {
+                loadConfigInfo(info, base);
+              }
+            });
+            group.done();
           }
         }));
   }
 
-  private void addExtensionPoints(ChangeInfo change) {
+  private RevisionInfo initCurrentRevision(ChangeInfo info) {
+    info.revisions().copyKeysIntoChildren("name");
+    if (edit != null) {
+      edit.setName(edit.commit().commit());
+      info.setEdit(edit);
+      if (edit.hasFiles()) {
+        edit.files().copyKeysIntoChildren("path");
+      }
+      info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
+      JsArray<RevisionInfo> list = info.revisions().values();
+
+      // Edit is converted to a regular revision (with number = 0) and
+      // added to the list of revisions. Additionally under certain
+      // circumstances change edit is assigned to be the current revision
+      // and is selected to be shown on the change screen.
+      // We have two different strategies to assign edit to the current ps:
+      // 1. revision == null: no revision is selected, so use the edit only
+      //    if it is based on the latest patch set
+      // 2. edit was selected explicitly from ps drop down:
+      //    use the edit regardless of which patch set it is based on
+      if (revision == null) {
+        RevisionInfo.sortRevisionInfoByNumber(list);
+        RevisionInfo rev = list.get(list.length() - 1);
+        if (rev.isEdit()) {
+          info.setCurrentRevision(rev.name());
+        }
+      } else if (revision.equals("edit") || revision.equals("0")) {
+        for (int i = 0; i < list.length(); i++) {
+          RevisionInfo r = list.get(i);
+          if (r.isEdit()) {
+            info.setCurrentRevision(r.name());
+            break;
+          }
+        }
+      }
+    }
+    return resolveRevisionToDisplay(info);
+  }
+
+  private void addExtensionPoints(ChangeInfo change, RevisionInfo rev) {
     addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER,
-        headerExtension, change);
+        headerExtension, change, rev);
     addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS,
-        headerExtensionMiddle, change);
+        headerExtensionMiddle, change, rev);
     addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_POP_DOWNS,
-        headerExtensionRight, change);
+        headerExtensionRight, change, rev);
     addExtensionPoint(
         GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
-        changeExtension, change);
+        changeExtension, change, rev);
+    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,
-      Panel p, ChangeInfo change) {
+      Panel p, ChangeInfo change, RevisionInfo rev) {
     ExtensionPanel extensionPanel = new ExtensionPanel(extensionPoint);
     extensionPanel.putObject(GerritUiExtensionPoint.Key.CHANGE_INFO, change);
+    extensionPanel.putObject(GerritUiExtensionPoint.Key.REVISION_INFO, rev);
     p.add(extensionPanel);
   }
 
@@ -363,15 +427,16 @@
         .openDiv()
         .append(Gerrit.info().change().replyLabel())
         .closeDiv());
-      if (hasDraftComments) {
+      if (hasDraftComments || lc.hasReplyComment()) {
         reply.setStyleName(style.highlight());
       }
       reply.setVisible(true);
     }
   }
 
-  private void gotoSibling(final int offset) {
-    if (offset > 0 && changeInfo.currentRevision().equals(revision)) {
+  private void gotoSibling(int offset) {
+    if (offset > 0 && changeInfo.currentRevision() != null
+        && changeInfo.currentRevision().equals(revision)) {
       return;
     }
 
@@ -416,6 +481,14 @@
     }
   }
 
+  private void updatePatchSetsTextStyle(boolean isPatchSetCurrent) {
+    if (isPatchSetCurrent) {
+      patchSetsText.removeClassName(style.notCurrentPatchSet());
+    } else {
+      patchSetsText.addClassName(style.notCurrentPatchSet());
+    }
+  }
+
   private void initRevisionsAction(ChangeInfo info, String revision,
       NativeMap<ActionInfo> actions) {
     int currentPatchSet;
@@ -429,16 +502,22 @@
     }
 
     String currentlyViewedPatchSet;
-    if (info.revision(revision).id().equals("edit")) {
+    boolean isPatchSetCurrent = true;
+    String revisionId = info.revision(revision).id();
+    if (revisionId.equals("edit")) {
       currentlyViewedPatchSet =
           Resources.M.editPatchSet(RevisionInfo.findEditParent(info.revisions()
               .values()));
       currentPatchSet = info.revisions().values().length() - 1;
     } else {
-      currentlyViewedPatchSet = info.revision(revision).id();
+      currentlyViewedPatchSet = revisionId;
+      if (!currentlyViewedPatchSet.equals(Integer.toString(currentPatchSet))) {
+        isPatchSetCurrent = false;
+      }
     }
     patchSetsText.setInnerText(Resources.M.patchSets(
         currentlyViewedPatchSet, currentPatchSet));
+    updatePatchSetsTextStyle(isPatchSetCurrent);
     patchSetsAction = new PatchSetsAction(
         info.legacyId(), revision, edit,
         style, headerLine, patchSets);
@@ -491,9 +570,10 @@
   }
 
   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)) {
+      boolean isOpen = info.status().isOpen();
+      if (isOpen && isEditModeEnabled(info, rev)) {
         editMode.setVisible(fileTableMode == FileTable.Mode.REVIEW);
         addFile.setVisible(!editMode.isVisible());
         deleteFile.setVisible(!editMode.isVisible());
@@ -515,10 +595,12 @@
       }
 
       if (rev.isEdit()) {
-        if (info.hasEditBasedOnCurrentPatchSet()) {
-          publishEdit.setVisible(true);
-        } else {
-          rebaseEdit.setVisible(true);
+        if (isOpen) {
+          if (info.hasEditBasedOnCurrentPatchSet()) {
+            publishEdit.setVisible(true);
+          } else {
+            rebaseEdit.setVisible(true);
+          }
         }
         deleteEdit.setVisible(true);
       }
@@ -579,24 +661,24 @@
     KeyCommandSet keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
     keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
       @Override
-      public void onKeyPress(final KeyPressEvent event) {
+      public void onKeyPress(KeyPressEvent event) {
         Gerrit.displayLastChangeList();
       }
     });
     keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadChange()) {
       @Override
-      public void onKeyPress(final KeyPressEvent event) {
+      public void onKeyPress(KeyPressEvent event) {
         Gerrit.display(PageLinks.toChange(changeId));
       }
     });
     keysNavigation.add(new KeyCommand(0, 'n', Util.C.keyNextPatchSet()) {
         @Override
-        public void onKeyPress(final KeyPressEvent event) {
+        public void onKeyPress(KeyPressEvent event) {
           gotoSibling(1);
         }
       }, new KeyCommand(0, 'p', Util.C.keyPreviousPatchSet()) {
         @Override
-        public void onKeyPress(final KeyPressEvent event) {
+        public void onKeyPress(KeyPressEvent event) {
           gotoSibling(-1);
         }
       });
@@ -839,44 +921,9 @@
     }
   }
 
-  private void loadConfigInfo(final ChangeInfo info, final String base) {
-    info.revisions().copyKeysIntoChildren("name");
-    if (edit != null) {
-      edit.setName(edit.commit().commit());
-      info.setEdit(edit);
-      if (edit.hasFiles()) {
-        edit.files().copyKeysIntoChildren("path");
-      }
-      info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
-      JsArray<RevisionInfo> list = info.revisions().values();
-
-      // Edit is converted to a regular revision (with number = 0) and
-      // added to the list of revisions. Additionally under certain
-      // circumstances change edit is assigned to be the current revision
-      // and is selected to be shown on the change screen.
-      // We have two different strategies to assign edit to the current ps:
-      // 1. revision == null: no revision is selected, so use the edit only
-      //    if it is based on the latest patch set
-      // 2. edit was selected explicitly from ps drop down:
-      //    use the edit regardless of which patch set it is based on
-      if (revision == null) {
-        RevisionInfo.sortRevisionInfoByNumber(list);
-        RevisionInfo rev = list.get(list.length() - 1);
-        if (rev.isEdit()) {
-          info.setCurrentRevision(rev.name());
-        }
-      } else if (revision.equals("edit") || revision.equals("0")) {
-        for (int i = 0; i < list.length(); i++) {
-          RevisionInfo r = list.get(i);
-          if (r.isEdit()) {
-            info.setCurrentRevision(r.name());
-            break;
-          }
-        }
-      }
-    }
-    final RevisionInfo rev = resolveRevisionToDisplay(info);
-    final RevisionInfo b = resolveRevisionOrPatchSetId(info, base, null);
+  private void loadConfigInfo(final ChangeInfo info, String base) {
+    RevisionInfo rev = info.revision(revision);
+    RevisionInfo b = resolveRevisionOrPatchSetId(info, base, null);
 
     CallbackGroup group = new CallbackGroup();
     Timestamp lastReply = myLastReply(info);
@@ -890,7 +937,6 @@
     } else {
       loadDiff(b, rev, lastReply, group);
     }
-    loadCommit(rev, group);
 
     if (loaded) {
       group.done();
@@ -925,10 +971,10 @@
     return null;
   }
 
-  private void loadDiff(final RevisionInfo base, final RevisionInfo rev,
-      final Timestamp myLastReply, CallbackGroup group) {
-    final List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(rev, group);
-    final List<NativeMap<JsArray<CommentInfo>>> drafts = loadDrafts(rev, group);
+  private void loadDiff(RevisionInfo base, RevisionInfo rev,
+      Timestamp myLastReply, CallbackGroup group) {
+    List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(rev, group);
+    List<NativeMap<JsArray<CommentInfo>>> drafts = loadDrafts(rev, group);
     loadFileList(base, rev, myLastReply, group, comments, drafts);
 
     if (Gerrit.isSignedIn() && fileTableMode == FileTable.Mode.REVIEW) {
@@ -953,24 +999,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(
@@ -1036,7 +1083,7 @@
   }
 
   private void loadCommit(final RevisionInfo rev, CallbackGroup group) {
-    if (rev.isEdit()) {
+    if (rev.isEdit() || rev.commit() != null) {
       return;
     }
 
@@ -1053,33 +1100,16 @@
         }));
   }
 
-  private void loadSubmitType(final Change.Status status, final boolean canSubmit) {
-    if (canSubmit) {
-      if (status == Change.Status.NEW) {
-        statusText.setInnerText(Util.C.readyToSubmit());
-      }
+  private void renderSubmitType(Change.Status status, boolean canSubmit,
+      SubmitType submitType) {
+    if (canSubmit && status == Change.Status.NEW) {
+      statusText.setInnerText(changeInfo.mergeable()
+          ? Util.C.readyToSubmit()
+          : Util.C.mergeConflict());
     }
-    ChangeApi.revision(changeId.get(), revision)
-      .view("submit_type")
-      .get(new AsyncCallback<NativeString>() {
-        @Override
-        public void onSuccess(NativeString result) {
-          if (canSubmit) {
-            if (status == Change.Status.NEW) {
-              statusText.setInnerText(changeInfo.mergeable()
-                  ? Util.C.readyToSubmit()
-                  : Util.C.mergeConflict());
-            }
-          }
-          setVisible(notMergeable, !changeInfo.mergeable());
-
-          renderSubmitType(result.asString());
-        }
-
-        @Override
-        public void onFailure(Throwable caught) {
-        }
-      });
+    setVisible(notMergeable, !changeInfo.mergeable());
+    submitActionText.setInnerText(
+        com.google.gerrit.client.admin.Util.toLongString(submitType));
   }
 
   private RevisionInfo resolveRevisionToDisplay(ChangeInfo info) {
@@ -1099,15 +1129,13 @@
       rev = revisions.get(revisions.length() - 1);
       revision = rev.name();
       return rev;
-    } else {
-      new ErrorDialog(
-          Resources.M.changeWithNoRevisions(info.legacyId().get())).center();
-      throw new IllegalStateException("no revision, cannot proceed");
     }
+    new ErrorDialog(
+        Resources.M.changeWithNoRevisions(info.legacyId().get())).center();
+    throw new IllegalStateException("no revision, cannot proceed");
   }
 
   /**
-   *
    * 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.
@@ -1116,13 +1144,20 @@
    *
    * @param info change info
    * @param revOrId revision or patch set id
-   * @param defaultValue value returned when rev is null
+   * @param defaultValue value returned when revOrId is null
    * @return resolved revision or default value
    */
   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();
+      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++) {
@@ -1146,16 +1181,18 @@
         LabelInfo label = info.label(name);
         switch (label.status()) {
           case NEED:
-            statusText.setInnerText("Needs " + name);
+            statusText.setInnerText(Util.M.needs(name));
             canSubmit = false;
             break;
           case REJECT:
           case IMPOSSIBLE:
             if (label.blocking()) {
-              statusText.setInnerText("Not " + name);
+              statusText.setInnerText(Util.M.blockedOn(name));
               canSubmit = false;
             }
             break;
+          case MAY:
+          case OK:
           default:
             break;
           }
@@ -1192,7 +1229,7 @@
     related.set(info, revision);
     reviewers.set(info);
     if (Gerrit.isNoteDbEnabled()) {
-      hashtags.set(info);
+      hashtags.set(info, revision);
     } else {
       setVisible(hashtagTableRow, false);
     }
@@ -1241,7 +1278,7 @@
 
     if (current && info.status().isOpen()) {
       quickApprove.set(info, revision, replyAction);
-      loadSubmitType(info.status(), isSubmittable(info));
+      renderSubmitType(info.status(), isSubmittable(info), info.submitType());
     } else {
       quickApprove.setVisible(false);
     }
@@ -1346,16 +1383,6 @@
     return sb.toString();
   }
 
-  private void renderSubmitType(String action) {
-    try {
-      SubmitType type = SubmitType.valueOf(action);
-      submitActionText.setInnerText(
-          com.google.gerrit.client.admin.Util.toLongString(type));
-    } catch (IllegalArgumentException e) {
-      submitActionText.setInnerText(action);
-    }
-  }
-
   private void renderActionTextDate(ChangeInfo info) {
     String action;
     if (info.created().equals(info.updated())) {
@@ -1387,9 +1414,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);
   }
@@ -1409,6 +1447,10 @@
       nm = JsArray.createArray().cast();
     }
 
+    if (om.length() == nm.length()) {
+      return;
+    }
+
     if (updateAvailable == null) {
       updateAvailable = new UpdateAvailableBar() {
         @Override
@@ -1441,4 +1483,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 c643072..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
@@ -228,8 +228,9 @@
 
     .hashtagName {
       display: inline-block;
+      height: 15px;
       margin-bottom: 2px;
-      padding: 1px 3px 0px 3px;
+      padding: 1px 3px 1px 3px;
       border-radius: 5px;
       -webkit-border-radius: 5px;
       background: #E2F5FF;
@@ -237,6 +238,12 @@
       white-space: nowrap;
     }
 
+    .hashtagName a,
+    .hashtagName button {
+      position: relative;
+      top: -4px;
+    }
+
     .hashtagName button {
       cursor: pointer;
       padding: 0;
@@ -246,6 +253,11 @@
       white-space: nowrap;
     }
 
+    .hashtagIcon img {
+      position: relative;
+      top: 4px;
+    }
+
     .headerButtons button {
       margin: 5.286px 3px 0 0;
       text-align: center;
@@ -335,9 +347,21 @@
       padding-top: 5px;
     }
 
+    .relatedExtension {
+      padding-top: 5px;
+    }
+
+    .commitExtension {
+      padding-top: 5px;
+    }
+
     .pushCertStatus {
       padding-left: 5px;
     }
+
+    .notCurrentPatchSet {
+      background-color: #FFA62F;
+    }
   </ui:style>
 
   <g:HTMLPanel styleName='{style.cs2}'>
@@ -414,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'>
@@ -504,6 +529,7 @@
         </td>
         <td class='{style.relatedColumn}'>
           <c:RelatedChanges ui:field='related'/>
+          <g:SimplePanel ui:field='relatedExtension' styleName='{style.relatedExtension}'/>
         </td>
       </tr>
     </table>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
index f8ddef4..4eacc8c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
@@ -16,13 +16,11 @@
 
 import com.google.gerrit.client.AvatarImage;
 import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
 import com.google.gerrit.client.info.ChangeInfo.GitPerson;
 import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.info.GitwebInfo;
 import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
@@ -38,7 +36,6 @@
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.uibinder.client.UiHandler;
 import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -118,13 +115,13 @@
         committerDate, change);
     text.setHTML(commentLinkProcessor.apply(
         new SafeHtmlBuilder().append(commit.message()).linkify()));
-    setWebLinks(change, revision, revInfo);
+    setWebLinks(webLinkPanel, revInfo.commit());
 
     if (revInfo.commit().parents().length() > 1) {
       mergeCommit.setVisible(true);
     }
 
-    setParents(change.project(), revInfo.commit().parents());
+    setParents(revInfo.commit().parents());
   }
 
   void setParentNotCurrent(boolean parentNotCurrent) {
@@ -133,30 +130,16 @@
     parentNotCurrentText.setInnerText(parentNotCurrent ? "\u25CF" : "");
   }
 
-  private void setWebLinks(ChangeInfo change, String revision,
-      RevisionInfo revInfo) {
-    GitwebInfo gw = Gerrit.info().gitweb();
-    if (gw != null && gw.canLink(revInfo)) {
-      toAnchor(gw.toRevision(change.project(), revision),
-          gw.getLinkName());
-    }
-
-    JsArray<WebLinkInfo> links = revInfo.commit().webLinks();
+  private void setWebLinks(FlowPanel panel, CommitInfo commit) {
+    JsArray<WebLinkInfo> links = commit.webLinks();
     if (links != null) {
       for (WebLinkInfo link : Natives.asList(links)) {
-        webLinkPanel.add(link.toAnchor());
+        panel.add(link.toAnchor());
       }
     }
   }
 
-  private void toAnchor(String href, String name) {
-    Anchor a = new Anchor();
-    a.setHref(href);
-    a.setText(name);
-    webLinkPanel.add(a);
-  }
-
-  private void setParents(String project, JsArray<CommitInfo> commits) {
+  private void setParents(JsArray<CommitInfo> commits) {
     setVisible(firstParent, true);
     TableRowElement next = firstParent;
     TableRowElement previous = null;
@@ -164,7 +147,7 @@
       if (next == firstParent) {
         CopyableLabel copyLabel = getCommitLabel(c);
         parentCommits.add(copyLabel);
-        addLinks(project, c, parentWebLinks);
+        setWebLinks(parentWebLinks, c);
       } else {
         next.appendChild(DOM.createTD());
         Element td1 = DOM.createTD();
@@ -172,7 +155,7 @@
         next.appendChild(td1);
         FlowPanel linksPanel = new FlowPanel();
         linksPanel.addStyleName(style.parentWebLink());
-        addLinks(project, c, linksPanel);
+        setWebLinks(linksPanel, c);
         Element td2 = DOM.createTD();
         td2.appendChild(linksPanel.getElement());
         next.appendChild(td2);
@@ -183,22 +166,6 @@
     }
   }
 
-  private void addLinks(String project, CommitInfo c, FlowPanel panel) {
-    GitwebInfo gw = Gerrit.info().gitweb();
-    if (gw != null) {
-      Anchor a =
-          new Anchor(gw.getLinkName(), gw.toRevision(project, c.commit()));
-      a.setStyleName(style.parentWebLink());
-      panel.add(a);
-    }
-    JsArray<WebLinkInfo> links = c.webLinks();
-    if (links != null) {
-      for (WebLinkInfo link : Natives.asList(links)) {
-        panel.add(link.toAnchor());
-      }
-    }
-  }
-
   private CopyableLabel getCommitLabel(CommitInfo c) {
     CopyableLabel copyLabel;
     copyLabel = new CopyableLabel(c.commit());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
index c8326cc..e8707f4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
@@ -18,10 +18,10 @@
 import com.google.gerrit.client.account.AccountApi;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeList;
-import com.google.gerrit.client.info.AccountPreferencesInfo;
 import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.info.ChangeInfo.EditInfo;
 import com.google.gerrit.client.info.ChangeInfo.FetchInfo;
+import com.google.gerrit.client.info.GeneralPreferences;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -236,10 +236,10 @@
 
   private void saveScheme() {
     String schemeStr = scheme.getValue(scheme.getSelectedIndex());
-    AccountPreferencesInfo prefs = Gerrit.getUserPreferences();
+    GeneralPreferences prefs = Gerrit.getUserPreferences();
     if (Gerrit.isSignedIn() && !schemeStr.equals(prefs.downloadScheme())) {
       prefs.downloadScheme(schemeStr);
-      AccountPreferencesInfo in = AccountPreferencesInfo.create();
+      GeneralPreferences in = GeneralPreferences.create();
       in.downloadScheme(schemeStr);
       AccountApi.self().view("preferences")
           .put(in, new AsyncCallback<JavaScriptObject>() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
index e73c70a..9afcd4f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
@@ -49,6 +49,6 @@
   }
 
   private static String url(PatchSet.Id ps, CommentInfo info) {
-    return Dispatcher.toSideBySide(null, ps, info.path());
+    return Dispatcher.toPatch(null, ps, info.path());
   }
 }
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 d1ca517..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
@@ -15,7 +15,9 @@
 package com.google.gerrit.client.change;
 
 import static com.google.gerrit.client.FormatUtil.formatAbsBytes;
+import static com.google.gerrit.client.FormatUtil.formatAbsPercentage;
 import static com.google.gerrit.client.FormatUtil.formatBytes;
+import static com.google.gerrit.client.FormatUtil.formatPercentage;
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
@@ -34,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;
@@ -66,7 +69,7 @@
 import java.sql.Timestamp;
 
 public class FileTable extends FlowPanel {
-  static final FileTableResources R = GWT
+  private static final FileTableResources R = GWT
       .create(FileTableResources.class);
 
   interface FileTableResources extends ClientBundle {
@@ -93,7 +96,7 @@
     String restoreDelete();
   }
 
-  public static enum Mode {
+  public enum Mode {
     REVIEW,
     EDIT
   }
@@ -113,7 +116,7 @@
     init(DELETE, RESTORE, REVIEWED, OPEN);
   }
 
-  private static final native void init(String d, String t, String r, String o) /*-{
+  private static native void init(String d, String t, String r, String o) /*-{
     $wnd[d] = $entry(function(e,i) {
       @com.google.gerrit.client.change.FileTable::onDelete(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
     });
@@ -282,7 +285,7 @@
     return info.binary()
       ? Dispatcher.toUnified(base, curr, info.path())
       : mode == Mode.REVIEW
-            ? Dispatcher.toSideBySide(base, curr, info.path())
+            ? Dispatcher.toPatch(base, curr, info.path())
             : Dispatcher.toEditScreen(curr, info.path());
   }
 
@@ -464,6 +467,7 @@
     private boolean hasNonBinaryFile;
     private int inserted;
     private int deleted;
+    private long binOldSize;
     private long bytesInserted;
     private long bytesDeleted;
 
@@ -520,6 +524,7 @@
     private void computeInsertedDeleted() {
       inserted = 0;
       deleted = 0;
+      binOldSize = 0;
       bytesInserted = 0;
       bytesDeleted = 0;
       for (int i = 0; i < list.length(); i++) {
@@ -531,6 +536,7 @@
             deleted += info.linesDeleted();
           } else {
             hasBinaryFile = true;
+            binOldSize += info.size() - info.sizeDelta();
             if (info.sizeDelta() >= 0) {
               bytesInserted += info.sizeDelta();
             } else {
@@ -707,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) {
@@ -742,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) {
@@ -771,6 +791,12 @@
         }
       } else if (info.binary()) {
         sb.append(formatBytes(info.sizeDelta()));
+        long oldSize = info.size() - info.sizeDelta();
+        if (oldSize != 0) {
+          sb.append(" (")
+            .append(formatPercentage(oldSize, info.sizeDelta()))
+            .append(")");
+        }
       }
       sb.closeTd();
     }
@@ -827,8 +853,17 @@
         if (hasNonBinaryFile) {
           sb.br();
         }
-        sb.append(Util.M.patchTableSize_ModifyBinaryFiles(
-            formatAbsBytes(bytesInserted), formatAbsBytes(bytesDeleted)));
+        if (binOldSize != 0) {
+          sb.append(Util.M.patchTableSize_ModifyBinaryFilesWithPercentages(
+              formatAbsBytes(bytesInserted),
+              formatAbsPercentage(binOldSize, bytesInserted),
+              formatAbsBytes(bytesDeleted),
+              formatAbsPercentage(binOldSize, bytesDeleted)));
+        } else {
+          sb.append(Util.M.patchTableSize_ModifyBinaryFiles(
+              formatAbsBytes(bytesInserted),
+              formatAbsBytes(bytesDeleted)));
+        }
       }
       sb.closeTh();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
index 55f66b7..44aae25 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
@@ -20,12 +20,14 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyDownEvent;
 import com.google.gwt.event.dom.client.KeyDownHandler;
@@ -34,9 +36,10 @@
 import com.google.gwt.uibinder.client.UiHandler;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.rpc.StatusCodeException;
-import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.ImageResourceRenderer;
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
@@ -51,6 +54,7 @@
   private static final String REMOVE;
   private static final String DATA_ID = "data-id";
 
+  private PatchSet.Id psId;
   private boolean canEdit;
 
   static {
@@ -58,7 +62,7 @@
     init(REMOVE);
   }
 
-  private static final native void init(String r) /*-{
+  private static native void init(String r) /*-{
     $wnd[r] = $entry(function(e) {
       @com.google.gerrit.client.change.Hashtags::onRemove(Lcom/google/gwt/dom/client/NativeEvent;)(e)
     });
@@ -68,12 +72,13 @@
     String hashtags = getDataId(event);
     if (hashtags != null) {
       final ChangeScreen screen = ChangeScreen.get(event);
-      ChangeApi.hashtags(screen.getChangeId().get()).post(
+      final PatchSet.Id psId = screen.getPatchSetId();
+      ChangeApi.hashtags(psId.getParentKey().get()).post(
           PostInput.create(null, hashtags), new GerritCallback<JavaScriptObject>() {
             @Override
             public void onSuccess(JavaScriptObject result) {
               if (screen.isCurrentView()) {
-                Gerrit.display(PageLinks.toChange(screen.getChangeId()));
+                Gerrit.display(PageLinks.toChange(psId));
               }
             }
           });
@@ -93,7 +98,7 @@
   }
 
   @UiField Element hashtagsText;
-  @UiField Button openForm;
+  @UiField Image addHashtagIcon;
   @UiField Element form;
   @UiField Element error;
   @UiField NpTextBox hashtagTextBox;
@@ -109,35 +114,43 @@
     hashtagTextBox.addKeyDownHandler(new KeyDownHandler() {
       @Override
       public void onKeyDown(KeyDownEvent e) {
-        if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) {
+        if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
           onCancel(null);
-        } else if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+        } else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
           onAdd(null);
         }
       }
     });
+
+    addHashtagIcon.addDomHandler(
+        new ClickHandler() {
+          @Override
+          public void onClick(ClickEvent event) {
+            onOpenForm();
+          }
+        },
+        ClickEvent.getType());
   }
 
-  void init(ChangeScreen.Style style){
+  void init(ChangeScreen.Style style) {
     this.style = style;
   }
 
-  void set(ChangeInfo info) {
+  void set(ChangeInfo info, String revision) {
+    psId = new PatchSet.Id(
+        info.legacyId(),
+        info.revisions().get(revision)._number());
+
     canEdit = info.hasActions() && info.actions().containsKey("hashtags");
     this.changeId = info.legacyId();
     display(info);
-    openForm.setVisible(canEdit);
-  }
-
-  @UiHandler("openForm")
-  void onOpenForm(@SuppressWarnings("unused") ClickEvent e) {
-    onOpenForm();
+    addHashtagIcon.setVisible(canEdit);
   }
 
   void onOpenForm() {
     UIObject.setVisible(form, true);
     UIObject.setVisible(error, false);
-    openForm.setVisible(false);
+    addHashtagIcon.setVisible(false);
     hashtagTextBox.setFocus(true);
   }
 
@@ -167,7 +180,13 @@
           .setAttribute("href",
               "#" + PageLinks.toChangeQuery("hashtag:\"" + hashtagName + "\""))
           .setAttribute("role", "listitem")
-          .append("#").append(hashtagName)
+          .openSpan()
+            .setStyleName(style.hashtagIcon())
+            .append(new ImageResourceRenderer().render(
+                Gerrit.RESOURCES.hashtag()))
+          .closeSpan()
+          .append(" ")
+          .append(hashtagName)
           .closeAnchor();
       if (canEdit) {
         html.openElement("button")
@@ -186,7 +205,7 @@
 
   @UiHandler("cancel")
   void onCancel(@SuppressWarnings("unused") ClickEvent e) {
-    openForm.setVisible(true);
+    addHashtagIcon.setVisible(true);
     UIObject.setVisible(form, false);
     hashtagTextBox.setFocus(false);
   }
@@ -205,14 +224,9 @@
         new GerritCallback<JsArrayString>() {
           @Override
           public void onSuccess(JsArrayString result) {
-            hashtagTextBox.setEnabled(true);
-            UIObject.setVisible(error, false);
-            error.setInnerText("");
-            hashtagTextBox.setText("");
-
-            if (result != null && result.length() > 0) {
-              updateHashtagList(result);
-            }
+            Gerrit.display(PageLinks.toChange(
+                psId.getParentKey(),
+                String.valueOf(psId.get())));
           }
 
           @Override
@@ -226,32 +240,18 @@
         });
   }
 
-  protected void updateHashtagList() {
-    ChangeApi.detail(changeId.get(),
-        new GerritCallback<ChangeInfo>() {
-          @Override
-          public void onSuccess(ChangeInfo result) {
-            display(result);
-          }
-        });
-  }
-
-  protected void updateHashtagList(JsArrayString hashtags){
-    display(hashtags);
-  }
-
   public static class PostInput extends JavaScriptObject {
     public static PostInput create(String add, String remove) {
       PostInput input = createObject().cast();
       input.init(toJsArrayString(add), toJsArrayString(remove));
       return input;
     }
-    private static JsArrayString toJsArrayString(String commaSeparated){
+    private static JsArrayString toJsArrayString(String commaSeparated) {
       if (commaSeparated == null || commaSeparated.equals("")) {
         return null;
       }
       JsArrayString array = JsArrayString.createArray().cast();
-      for (String hashtag : commaSeparated.split(",")){
+      for (String hashtag : commaSeparated.split(",")) {
         array.push(hashtag.trim());
       }
       return array;
@@ -265,12 +265,4 @@
     protected PostInput() {
     }
   }
-
-  public static class Result extends JavaScriptObject {
-    public final native JsArrayString hashtags() /*-{ return this.hashtags; }-*/;
-    public final native String error() /*-{ return this.error; }-*/;
-
-    protected Result() {
-    }
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
index ba4d6cc..c0bfd1c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
@@ -18,6 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
   <ui:style gss='false'>
     button.openAdd {
@@ -47,21 +48,19 @@
       font-weight: bold;
     }
 
+    .addHashtag,
     .cancel {
+      cursor: pointer;
       float: right;
     }
   </ui:style>
   <g:HTMLPanel>
     <div>
       <span ui:field='hashtagsText'/>
-      <g:Button ui:field='openForm'
-         title='Add hashtags to this change'
-         styleName='{res.style.button}'
-         addStyleNames='{style.openAdd}'
-         visible='false'>
-       <ui:attribute name='title'/>
-       <div><ui:msg>Add #...</ui:msg></div>
-      </g:Button>
+      <g:Image ui:field='addHashtagIcon'
+        resource='{ico.addHashtag}'
+        styleName='{style.addHashtag}'
+        title='Add Hashtag'/>
     </div>
     <div ui:field='form' style='display: none' aria-hidden='true'>
       <c:NpTextBox ui:field='hashtagTextBox' styleName='{style.hashtagTextBox}'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
index 4139348..89550c6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
@@ -48,29 +49,54 @@
 /** Displays a table of label and reviewer scores. */
 class Labels extends Grid {
   private static final String DATA_ID = "data-id";
-  private static final String REMOVE;
+  private static final String DATA_VOTE = "data-vote";
+  private static final String REMOVE_REVIEWER;
+  private static final String REMOVE_VOTE;
 
   static {
-    REMOVE = DOM.createUniqueId().replace('-', '_');
-    init(REMOVE);
+    REMOVE_REVIEWER = DOM.createUniqueId().replace('-', '_');
+    REMOVE_VOTE = DOM.createUniqueId().replace('-', '_');
+    init(REMOVE_REVIEWER, REMOVE_VOTE);
   }
 
-  private static final native void init(String r) /*-{
+  private static native void init(String r, String v) /*-{
     $wnd[r] = $entry(function(e) {
-      @com.google.gerrit.client.change.Labels::onRemove(Lcom/google/gwt/dom/client/NativeEvent;)(e)
+      @com.google.gerrit.client.change.Labels::onRemoveReviewer(Lcom/google/gwt/dom/client/NativeEvent;)(e)
+    });
+    $wnd[v] = $entry(function(e) {
+      @com.google.gerrit.client.change.Labels::onRemoveVote(Lcom/google/gwt/dom/client/NativeEvent;)(e)
     });
   }-*/;
 
-  private static void onRemove(NativeEvent event) {
+  private static void onRemoveReviewer(NativeEvent event) {
     Integer user = getDataId(event);
     if (user != null) {
       final ChangeScreen screen = ChangeScreen.get(event);
-      ChangeApi.reviewer(screen.getChangeId().get(), user).delete(
+      final Change.Id changeId = screen.getPatchSetId().getParentKey();
+      ChangeApi.reviewer(changeId.get(), user).delete(
           new GerritCallback<JavaScriptObject>() {
             @Override
             public void onSuccess(JavaScriptObject result) {
               if (screen.isCurrentView()) {
-                Gerrit.display(PageLinks.toChange(screen.getChangeId()));
+                Gerrit.display(PageLinks.toChange(changeId));
+              }
+            }
+          });
+    }
+  }
+
+  private static void onRemoveVote(NativeEvent event) {
+    Integer user = getDataId(event);
+    String vote = getVoteId(event);
+    if (user != null && vote != null) {
+      final ChangeScreen screen = ChangeScreen.get(event);
+      final Change.Id changeId = screen.getPatchSetId().getParentKey();
+      ChangeApi.vote(changeId.get(), user, vote).delete(
+          new GerritCallback<JavaScriptObject>() {
+            @Override
+            public void onSuccess(JavaScriptObject result) {
+              if (screen.isCurrentView()) {
+                Gerrit.display(PageLinks.toChange(changeId));
               }
             }
           });
@@ -89,6 +115,18 @@
     return null;
   }
 
+  private static String getVoteId(NativeEvent event) {
+    Element e = event.getEventTarget().cast();
+    while (e != null) {
+      String v = e.getAttribute(DATA_VOTE);
+      if (!v.isEmpty()) {
+        return v;
+      }
+      e = e.getParentElement();
+    }
+    return null;
+  }
+
   private ChangeScreen.Style style;
 
   void init(ChangeScreen.Style style) {
@@ -97,6 +135,7 @@
 
   void set(ChangeInfo info) {
     List<String> names = new ArrayList<>(info.labels());
+    Set<Integer> removable = info.removableReviewerIds();
     Collections.sort(names);
 
     resize(names.size(), 2);
@@ -106,14 +145,14 @@
       LabelInfo label = info.label(name);
       setText(row, 0, name);
       if (label.all() != null) {
-        setWidget(row, 1, renderUsers(label));
+        setWidget(row, 1, renderUsers(label, removable));
       }
       getCellFormatter().setStyleName(row, 0, style.labelName());
       getCellFormatter().addStyleName(row, 0, getStyleForLabel(label));
     }
   }
 
-  private Widget renderUsers(LabelInfo label) {
+  private Widget renderUsers(LabelInfo label, Set<Integer> removable) {
     Map<Integer, List<ApprovalInfo>> m = new HashMap<>(4);
     int approved = 0;
     int rejected = 0;
@@ -150,8 +189,8 @@
         html.setStyleName(style.label_reject());
       }
       html.append(val).append(" ");
-      html.append(formatUserList(style, m.get(v),
-          Collections.<Integer> emptySet(), null));
+      html.append(formatUserList(style, m.get(v), removable,
+          label.name(), null));
       html.closeSpan();
     }
     return html.toBlockWidget();
@@ -198,6 +237,7 @@
   static SafeHtml formatUserList(ChangeScreen.Style style,
       Collection<? extends AccountInfo> in,
       Set<Integer> removable,
+      String label,
       Map<Integer, VotableInfo> votable) {
     List<AccountInfo> users = new ArrayList<>(in);
     Collections.sort(users, new Comparator<AccountInfo>() {
@@ -239,17 +279,21 @@
 
       String votableCategories = "";
       if (votable != null) {
-        Set<String> s = votable.get(ai._accountId()).votableLabels();
-        if (!s.isEmpty()) {
-          StringBuilder sb = new StringBuilder(Util.C.votable());
-          sb.append(" ");
-          for (Iterator<String> it = s.iterator(); it.hasNext();) {
-            sb.append(it.next());
-            if (it.hasNext()) {
-              sb.append(", ");
+        VotableInfo vi = votable.get(ai._accountId());
+        if (vi != null) {
+          Set<String> s = vi.votableLabels();
+          if (!s.isEmpty()) {
+            StringBuilder sb = new StringBuilder(Util.C.votable());
+            sb.append(" ");
+            for (Iterator<String> it = vi.votableLabels().iterator();
+                it.hasNext();) {
+              sb.append(it.next());
+              if (it.hasNext()) {
+                sb.append(", ");
+              }
             }
+            votableCategories = sb.toString();
           }
-          votableCategories = sb.toString();
         }
       }
       html.openSpan()
@@ -257,6 +301,9 @@
           .setAttribute(DATA_ID, ai._accountId())
           .setAttribute("title", getTitle(ai, votableCategories))
           .setStyleName(style.label_user());
+      if (label != null) {
+        html.setAttribute(DATA_VOTE, label);
+      }
       if (img != null) {
         html.openElement("img")
             .setStyleName(style.avatar())
@@ -271,10 +318,15 @@
       }
       html.append(name);
       if (removable.contains(ai._accountId())) {
-        html.openElement("button")
-            .setAttribute("title", Util.M.removeReviewer(name))
-            .setAttribute("onclick", REMOVE + "(event)")
-            .append("×")
+        html.openElement("button");
+        if (label != null) {
+          html.setAttribute("title", Util.M.removeVote(label))
+              .setAttribute("onclick", REMOVE_VOTE + "(event)");
+        } else {
+          html.setAttribute("title", Util.M.removeReviewer(name))
+              .setAttribute("onclick", REMOVE_REVIEWER + "(event)");
+        }
+        html.append("×")
             .closeElement("button");
       }
       html.closeSpan();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
index fc14587..a0f8858 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.change;
 
 import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.api.ApiGlue;
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.diff.DisplaySide;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
@@ -56,14 +57,14 @@
       ps = defaultPs;
       psLoc.removeFromParent();
       psLoc = null;
-      psNum= null;
+      psNum = null;
     } else {
       ps = defaultPs;
       sideLoc.removeFromParent();
       sideLoc = null;
       psLoc.removeFromParent();
       psLoc = null;
-      psNum= null;
+      psNum = null;
     }
 
     if (info.hasLine()) {
@@ -82,11 +83,12 @@
     if (info.message() != null) {
       message.setInnerSafeHtml(clp.apply(new SafeHtmlBuilder()
           .append(info.message().trim()).wikify()));
+      ApiGlue.fireEvent("comment", message);
     }
   }
 
   private static String url(PatchSet.Id ps, CommentInfo info) {
-    return Dispatcher.toSideBySide(null, ps, info.path(),
+    return Dispatcher.toPatch(null, ps, info.path(),
         info.side() == Side.PARENT ? DisplaySide.A : DisplaySide.B,
         info.line());
   }
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
new file mode 100644
index 0000000..f6022f9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java
@@ -0,0 +1,255 @@
+// 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.client.change;
+
+import com.google.gerrit.client.changes.CommentApi;
+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;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.user.client.Cookies;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class LocalComments {
+  private final Change.Id changeId;
+  private final PatchSet.Id psId;
+  private final StorageBackend storage;
+
+  private static class InlineComment {
+    final PatchSet.Id psId;
+    final CommentInfo commentInfo;
+
+    InlineComment(PatchSet.Id psId, CommentInfo commentInfo) {
+      this.psId = psId;
+      this.commentInfo = commentInfo;
+    }
+  }
+
+  private static class StorageBackend {
+    private final Storage storageBackend;
+
+    StorageBackend() {
+      storageBackend = (Storage.isLocalStorageSupported())
+          ? Storage.getLocalStorageIfSupported()
+          : Storage.getSessionStorageIfSupported();
+    }
+
+    String getItem(String key) {
+      if (storageBackend == null) {
+        return Cookies.getCookie(key);
+      }
+      return storageBackend.getItem(key);
+    }
+
+    void setItem(String key, String value) {
+      if (storageBackend == null) {
+        Cookies.setCookie(key, value);
+        return;
+      }
+      storageBackend.setItem(key, value);
+    }
+
+    void removeItem(String key) {
+      if (storageBackend == null) {
+        Cookies.removeCookie(key);
+        return;
+      }
+      storageBackend.removeItem(key);
+    }
+
+    Collection<String> getKeys() {
+      if (storageBackend == null) {
+        return Cookies.getCookieNames();
+      }
+      ArrayList<String> result = new ArrayList<>(storageBackend.getLength());
+      for (int i = 0; i < storageBackend.getLength(); i++) {
+        result.add(storageBackend.key(i));
+      }
+      return result;
+    }
+  }
+
+  public LocalComments(Change.Id changeId) {
+    this.changeId = changeId;
+    this.psId = null;
+    this.storage = new StorageBackend();
+  }
+
+  public LocalComments(PatchSet.Id psId) {
+    this.changeId = psId.getParentKey();
+    this.psId = psId;
+    this.storage = new StorageBackend();
+  }
+
+  public String getReplyComment() {
+    String comment = storage.getItem(getReplyCommentName());
+    storage.removeItem(getReplyCommentName());
+    return comment;
+  }
+
+  public void setReplyComment(String comment) {
+    storage.setItem(getReplyCommentName(), comment.trim());
+  }
+
+  public boolean hasReplyComment() {
+    return storage.getKeys().contains(getReplyCommentName());
+  }
+
+  public void removeReplyComment() {
+    if (hasReplyComment()) {
+      storage.removeItem(getReplyCommentName());
+    }
+  }
+
+  private String getReplyCommentName() {
+    return "savedReplyComment-" + changeId.toString();
+  }
+
+  public static void saveInlineComments() {
+    final StorageBackend storage = new StorageBackend();
+    for (final String cookie : storage.getKeys()) {
+      if (isInlineComment(cookie)) {
+        InlineComment input = getInlineComment(cookie);
+        if (input.commentInfo.id() == null) {
+          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, 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);
+                  }
+                }
+              });
+        }
+      }
+    }
+  }
+
+  public void setInlineComment(CommentInfo comment) {
+    String name = getInlineCommentName(comment);
+    if (name == null) {
+      // Failed to get the store key -- so we can't continue.
+      return;
+    }
+    storage.setItem(name, comment.message().trim());
+  }
+
+  public boolean hasInlineComments() {
+    for (String cookie : storage.getKeys()) {
+      if (isInlineComment(cookie)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static boolean isInlineComment(String key) {
+    return key.startsWith("patchCommentEdit-") || key.startsWith("patchReply-")
+        || key.startsWith("patchComment-");
+  }
+
+  private static InlineComment getInlineComment(String key) {
+    String path;
+    Side side = Side.PARENT;
+    int line = 0;
+    CommentRange range;
+    StorageBackend storage = new StorageBackend();
+
+    String[] elements = key.split("-");
+    int offset = 1;
+    if (key.startsWith("patchReply-") || key.startsWith("patchCommentEdit-")) {
+      offset = 2;
+    }
+    Change.Id changeId = new Change.Id(Integer.parseInt(elements[offset + 0]));
+    PatchSet.Id psId =
+        new PatchSet.Id(changeId, Integer.parseInt(elements[offset + 1]));
+    path = atob(elements[offset + 2]);
+    side = (Side.PARENT.toString().equals(elements[offset + 3])) ? Side.PARENT
+        : Side.REVISION;
+    range = null;
+    if (elements[offset + 4].startsWith("R")) {
+      String rangeStart = elements[offset + 4].substring(1);
+      String rangeEnd = elements[offset + 5];
+      String[] split = rangeStart.split(",");
+      int sl = Integer.parseInt(split[0]);
+      int sc = Integer.parseInt(split[1]);
+      split = rangeEnd.split(",");
+      int el = Integer.parseInt(split[0]);
+      int ec = Integer.parseInt(split[1]);
+      range = CommentRange.create(sl, sc, el, ec);
+      line = sl;
+    } else {
+      line = Integer.parseInt(elements[offset + 4]);
+    }
+    CommentInfo info = CommentInfo.create(path, side, line, range);
+    info.message(storage.getItem(key));
+    if (key.startsWith("patchReply-")) {
+      info.inReplyTo(elements[1]);
+    } else if (key.startsWith("patchCommentEdit-")) {
+      info.id(elements[1]);
+    }
+    InlineComment inlineComment = new InlineComment(psId, info);
+    return inlineComment;
+  }
+
+  private String getInlineCommentName(CommentInfo comment) {
+    if (psId == null) {
+      return null;
+    }
+    String result = "patchComment-";
+    if (comment.id() != null) {
+      result = "patchCommentEdit-" + comment.id() + "-";
+    } else if (comment.inReplyTo() != null) {
+      result = "patchReply-" + comment.inReplyTo() + "-";
+    }
+    result += changeId + "-" + psId.getId() + "-" + btoa(comment.path()) + "-"
+        + comment.side() + "-";
+    if (comment.hasRange()) {
+      result += "R" + comment.range().startLine() + ","
+          + comment.range().startCharacter() + "-" + comment.range().endLine()
+          + "," + comment.range().endCharacter();
+    } else {
+      result += comment.line();
+    }
+    return result;
+  }
+
+  private static native String btoa(String a) /*-{ return btoa(a); }-*/;
+
+  private static native String atob(String b) /*-{ return atob(b); }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
index 8b559e3..6c27ed9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.AvatarImage;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.api.ApiGlue;
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
@@ -49,7 +50,7 @@
   interface Binder extends UiBinder<HTMLPanel, Message> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
-  static interface Style extends CssResource {
+  interface Style extends CssResource {
     String closed();
   }
 
@@ -96,6 +97,7 @@
       summary.setInnerText(msg);
       message.setInnerSafeHtml(history.getCommentLinkProcessor()
         .apply(new SafeHtmlBuilder().append(msg).wikify()));
+      ApiGlue.fireEvent("comment", message);
     } else {
       reply.getElement().getStyle().setVisibility(Visibility.HIDDEN);
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
index 1d76612..1753c7b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
@@ -63,7 +63,7 @@
     init(OPEN);
   }
 
-  private static final native void init(String o) /*-{
+  private static native void init(String o) /*-{
     $wnd[o] = $entry(function(e,i) {
       return @com.google.gerrit.client.change.PatchSetsBox::onOpen(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i);
     });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
index 3be3486..0d0dba7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
@@ -137,7 +137,7 @@
     abstract String getTitle(int count);
     abstract String getTitle(String count);
 
-    private Tab(String defaultTitle, String tooltip) {
+    Tab(String defaultTitle, String tooltip) {
       this.defaultTitle = defaultTitle;
       this.tooltip = tooltip;
     }
@@ -215,10 +215,12 @@
         EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT),
         new TabChangeListCallback(Tab.CHERRY_PICKS, info.project(), revision));
 
-    // TODO(sbeller): show only on latest revision
-    ChangeApi.change(info.legacyId().get()).view("submitted_together")
-        .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
-            info.project(), revision));
+    if (info.currentRevision() != null
+        && info.currentRevision().equals(revision)) {
+      ChangeApi.change(info.legacyId().get()).view("submitted_together")
+          .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
+              info.project(), revision));
+    }
 
     if (!Gerrit.info().change().isSubmitWholeTopicEnabled()
         && info.topic() != null && !"".equals(info.topic())) {
@@ -385,7 +387,7 @@
       String s = statusRaw();
       return s != null ? Change.Status.valueOf(s) : null;
     }
-    private final native String statusRaw() /*-{ return this.status; }-*/;
+    private native String statusRaw() /*-{ return this.status; }-*/;
 
     final native void setId(String i)
     /*-{ if(i)this.change_id=i; }-*/;
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 23959e7..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
@@ -18,10 +18,8 @@
 import com.google.gerrit.client.change.RelatedChanges.ChangeAndCommit;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.info.GitwebInfo;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.Scheduler;
@@ -68,7 +66,7 @@
   private static final SafeHtml POINTER_HTML =
       AbstractImagePrototype.create(Gerrit.RESOURCES.arrowRight()).getSafeHtml();
 
-  private static final native String init(String o) /*-{
+  private static native String init(String o) /*-{
     $wnd[o] = $entry(@com.google.gerrit.client.change.RelatedChangesTab::onOpen(
       Lcom/google/gwt/dom/client/NativeEvent;Lcom/google/gwt/dom/client/Element;));
     return o + '(event,this)';
@@ -286,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);
@@ -307,12 +307,7 @@
       sb.closeSpan();
 
       sb.openSpan();
-      GitwebInfo gw = Gerrit.info().gitweb();
-      if (gw != null && (!info.hasChangeNumber() || !info.hasRevisionNumber())) {
-        sb.setStyleName(RelatedChanges.R.css().gitweb());
-        sb.setAttribute("title", gw.getLinkName());
-        sb.append('\u25CF'); // Unicode 'BLACK CIRCLE'
-      } else if (info.status() != null && !info.status().isOpen()) {
+      if (info.status() != null && !info.status().isOpen()) {
         sb.setStyleName(RelatedChanges.R.css().gitweb());
         sb.setAttribute("title", Util.toLongString(info.status()));
         sb.append('\u25CF'); // Unicode 'BLACK CIRCLE'
@@ -339,15 +334,7 @@
 
     private String url() {
       if (info.hasChangeNumber() && info.hasRevisionNumber()) {
-        PatchSet.Id id = info.patchSetId();
-        return "#" + PageLinks.toChange(
-            id.getParentKey(),
-            id.getId());
-      }
-
-      GitwebInfo gw = Gerrit.info().gitweb();
-      if (gw != null && project != null) {
-        return gw.toRevision(project, info.commit().commit());
+        return "#" + PageLinks.toChange(info.patchSetId());
       }
       return null;
     }
@@ -603,7 +590,7 @@
     }
   }
 
-  private static final native Node createDocumentFragment() /*-{
+  private static native Node createDocumentFragment() /*-{
     return $doc.createDocumentFragment();
   }-*/;
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
index 2ec4b6b..e29048a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.change;
 
 import static com.google.gwt.event.dom.client.KeyCodes.KEY_ENTER;
+import static com.google.gwt.event.dom.client.KeyCodes.KEY_MAC_ENTER;
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
@@ -28,6 +29,7 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.LabelValue;
@@ -42,6 +44,8 @@
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
 import com.google.gwt.event.dom.client.MouseOutEvent;
@@ -75,7 +79,7 @@
 import java.util.Set;
 import java.util.TreeSet;
 
-class ReplyBox extends Composite {
+public class ReplyBox extends Composite {
   interface Binder extends UiBinder<HTMLPanel, ReplyBox> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
@@ -90,6 +94,7 @@
   private final String revision;
   private ReviewInput in = ReviewInput.create();
   private int labelHelpColumn;
+  private LocalComments lc;
 
   @UiField Styles style;
   @UiField TextArea message;
@@ -109,6 +114,7 @@
     this.clp = clp;
     this.psId = psId;
     this.revision = revision;
+    this.lc = new LocalComments(psId.getParentKey());
     initWidget(uiBinder.createAndBindUi(this));
 
     List<String> names = new ArrayList<>(permitted.keySet());
@@ -120,12 +126,13 @@
     }
 
     addDomHandler(
-      new KeyPressHandler() {
+      new KeyDownHandler() {
         @Override
-        public void onKeyPress(KeyPressEvent e) {
+        public void onKeyDown(KeyDownEvent e) {
           e.stopPropagation();
-          if ((e.getCharCode() == '\n' || e.getCharCode() == KEY_ENTER)
-              && e.isControlKeyDown()) {
+          if ((e.getNativeKeyCode() == KEY_ENTER
+              || e.getNativeKeyCode() == KEY_MAC_ENTER)
+              && (e.isControlKeyDown() || e.isMetaKeyDown())) {
             e.preventDefault();
             if (post.isEnabled()) {
               onPost(null);
@@ -133,6 +140,14 @@
           }
         }
       },
+      KeyDownEvent.getType());
+    addDomHandler(
+      new KeyPressHandler() {
+        @Override
+        public void onKeyPress(KeyPressEvent e) {
+          e.stopPropagation();
+        }
+      },
       KeyPressEvent.getType());
   }
 
@@ -140,6 +155,10 @@
   protected void onLoad() {
     commentsPanel.setVisible(false);
     post.setEnabled(false);
+    if (lc.hasReplyComment()) {
+      message.setText(lc.getReplyComment());
+      lc.removeReplyComment();
+    }
     ChangeApi.drafts(psId.getParentKey().get())
         .get(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
           @Override
@@ -197,9 +216,14 @@
       .post(in, new GerritCallback<ReviewInput>() {
         @Override
         public void onSuccess(ReviewInput result) {
-          Gerrit.display(PageLinks.toChange(
-              psId.getParentKey(),
-              String.valueOf(psId.get())));
+          Gerrit.display(PageLinks.toChange(psId));
+        }
+        @Override
+        public void onFailure(final Throwable caught) {
+          if (RestApi.isNotSignedIn(caught)) {
+            lc.setReplyComment(message.getText());
+          }
+          super.onFailure(caught);
         }
       });
     hide();
@@ -214,7 +238,7 @@
   void replyTo(MessageInfo msg) {
     if (msg.message() != null) {
       String t = message.getText();
-      String m = quote(msg);
+      String m = quote(removePatchSetHeaderLine(msg.message()));
       if (t == null || t.isEmpty()) {
         t = m;
       } else if (t.endsWith("\n\n")) {
@@ -224,20 +248,25 @@
       } else {
         t += "\n\n" + m;
       }
-      message.setText(t + "\n\n");
+      message.setText(t);
     }
   }
 
-  private static String quote(MessageInfo msg) {
-    String m = msg.message().trim();
-    if (m.startsWith("Patch Set ")) {
-      int i = m.indexOf('\n');
+  private static String removePatchSetHeaderLine(String msg) {
+    msg = msg.trim();
+    if (msg.startsWith("Patch Set ")) {
+      int i = msg.indexOf('\n');
       if (i > 0) {
-        m = m.substring(i + 1).trim();
+        msg = msg.substring(i + 1).trim();
       }
     }
+    return msg;
+  }
+
+  public static String quote(String msg) {
+    msg = msg.trim();
     StringBuilder quotedMsg = new StringBuilder();
-    for (String line : m.split("\\n")) {
+    for (String line : msg.split("\\n")) {
       line = line.trim();
       while (line.length() > 67) {
         int i = line.lastIndexOf(' ', 67);
@@ -253,7 +282,8 @@
       }
       quotedMsg.append(" > ").append(line).append("\n");
     }
-    return quotedMsg.toString().substring(0, quotedMsg.length() - 1); // remove last '\n'
+    quotedMsg.append("\n");
+    return quotedMsg.toString();
   }
 
   private void hide() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
index 52f6b6a..6903b91 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
@@ -38,7 +38,10 @@
       position: absolute;
       bottom: 5px;
       right: 5px;
+      background-color: #eee;
+      background-image: -webkit-linear-gradient(top, #eee, #eee);
     }
+    .cancel div { color: #444; }
     .comments {
       max-height: 275px;
       width: 526px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
index 4fc76c1..fdfaf61 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
@@ -19,9 +19,9 @@
 import com.google.gwt.resources.client.CssResource;
 
 public interface Resources extends ClientBundle {
-  public static final Resources I = GWT.create(Resources.class);
-  static final ChangeConstants C = GWT.create(ChangeConstants.class);
-  static final ChangeMessages M = GWT.create(ChangeMessages.class);
+  Resources I = GWT.create(Resources.class);
+  ChangeConstants C = GWT.create(ChangeConstants.class);
+  ChangeMessages M = GWT.create(ChangeMessages.class);
 
   @Source("common.css") Style style();
 
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..a852fa0 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.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/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index 3ba7ea5..b69d1c0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.ConfirmationCallback;
 import com.google.gerrit.client.ConfirmationDialog;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.NotSignedInDialog;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.info.AccountInfo;
@@ -28,12 +29,14 @@
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.RemoteSuggestBox;
+import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.logical.shared.CloseEvent;
 import com.google.gwt.event.logical.shared.CloseHandler;
 import com.google.gwt.event.logical.shared.SelectionEvent;
@@ -45,12 +48,14 @@
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
+import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -60,7 +65,7 @@
   private static final Binder uiBinder = GWT.create(Binder.class);
 
   @UiField Element reviewersText;
-  @UiField Button openForm;
+  @UiField Image addReviewerIcon;
   @UiField Button addMe;
   @UiField Element form;
   @UiField Element error;
@@ -92,6 +97,14 @@
     });
 
     initWidget(uiBinder.createAndBindUi(this));
+    addReviewerIcon.addDomHandler(
+        new ClickHandler() {
+          @Override
+          public void onClick(ClickEvent event) {
+            onOpenForm();
+          }
+        },
+        ClickEvent.getType());
   }
 
   void init(ChangeScreen.Style style, Element ccText) {
@@ -103,18 +116,13 @@
     this.changeId = info.legacyId();
     display(info);
     reviewerSuggestOracle.setChange(changeId);
-    openForm.setVisible(Gerrit.isSignedIn());
-  }
-
-  @UiHandler("openForm")
-  void onOpenForm(@SuppressWarnings("unused") ClickEvent e) {
-    onOpenForm();
+    addReviewerIcon.setVisible(Gerrit.isSignedIn());
   }
 
   void onOpenForm() {
     UIObject.setVisible(form, true);
     UIObject.setVisible(error, false);
-    openForm.setVisible(false);
+    addReviewerIcon.setVisible(false);
     suggestBox.setFocus(true);
   }
 
@@ -131,7 +139,7 @@
 
   @UiHandler("cancel")
   void onCancel(@SuppressWarnings("unused") ClickEvent e) {
-    openForm.setVisible(true);
+    addReviewerIcon.setVisible(true);
     UIObject.setVisible(form, false);
     suggestBox.setFocus(false);
     suggestBox.setText("");
@@ -178,10 +186,14 @@
 
           @Override
           public void onFailure(Throwable err) {
-            UIObject.setVisible(error, true);
-            error.setInnerText(err instanceof StatusCodeException
-                ? ((StatusCodeException) err).getEncodedResponse()
-                : err.getMessage());
+            if (isSigninFailure(err)) {
+              new NotSignedInDialog().center();
+            } else {
+              UIObject.setVisible(error, true);
+              error.setInnerText(err instanceof StatusCodeException
+                  ? ((StatusCodeException) err).getEncodedResponse()
+                  : err.getMessage());
+            }
           }
         });
   }
@@ -197,33 +209,20 @@
   }
 
   private void display(ChangeInfo info) {
-    Map<Integer, AccountInfo> r = new HashMap<>();
-    Map<Integer, AccountInfo> cc = new HashMap<>();
-    for (LabelInfo label : Natives.asList(info.allLabels().values())) {
-      if (label.all() != null) {
-        for (ApprovalInfo ai : Natives.asList(label.all())) {
-          (ai.value() != 0 ? r : cc).put(ai._accountId(), ai);
-        }
-      }
-    }
+    Map<ReviewerState, List<AccountInfo>> reviewers = info.reviewers();
+    Map<Integer, AccountInfo> r = byAccount(reviewers, ReviewerState.REVIEWER);
+    Map<Integer, AccountInfo> cc = byAccount(reviewers, ReviewerState.CC);
     for (Integer i : r.keySet()) {
       cc.remove(i);
     }
     cc.remove(info.owner()._accountId());
-
-    Set<Integer> removable = new HashSet<>();
-    if (info.removableReviewers() != null) {
-      for (AccountInfo a : Natives.asList(info.removableReviewers())) {
-        removable.add(a._accountId());
-      }
-    }
-
+    Set<Integer> removable = info.removableReviewerIds();
     Map<Integer, VotableInfo> votable = votable(info);
 
     SafeHtml rHtml = Labels.formatUserList(style,
-        r.values(), removable, votable);
+        r.values(), removable, null, votable);
     SafeHtml ccHtml = Labels.formatUserList(style,
-        cc.values(), removable, votable);
+        cc.values(), removable, null, votable);
 
     reviewersText.setInnerSafeHtml(rHtml);
     ccText.setInnerSafeHtml(ccHtml);
@@ -236,6 +235,19 @@
     }
   }
 
+  private static Map<Integer, AccountInfo> byAccount(
+      Map<ReviewerState, List<AccountInfo>> reviewers, ReviewerState state) {
+    List<AccountInfo> accounts = reviewers.get(state);
+    if (accounts == null) {
+      return Collections.emptyMap();
+    }
+    Map<Integer, AccountInfo> result = new HashMap<>();
+    for (AccountInfo a : accounts) {
+      result.put(a._accountId(), a);
+    }
+    return result;
+  }
+
   private static Map<Integer, VotableInfo> votable(ChangeInfo change) {
     Map<Integer, VotableInfo> d = new HashMap<>();
     for (String name : change.labels()) {
@@ -281,7 +293,7 @@
       return Natives.keys(_approvals());
     }
     final native String approval(String l) /*-{ return this.approvals[l]; }-*/;
-    private final native NativeMap<NativeString> _approvals() /*-{ return this.approvals; }-*/;
+    private native NativeMap<NativeString> _approvals() /*-{ return this.approvals; }-*/;
 
     protected ReviewerInfo() {
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
index 22e35e2..cf506e5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
@@ -19,26 +19,9 @@
     xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:u='urn:import:com.google.gerrit.client.ui'>
+  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
   <ui:style gss='false'>
-    button.openAdd {
-      margin: 3px 3px 0 0;
-      float: right;
-      color: rgba(0, 0, 0, 0.15);
-      background-color: #f5f5f5;
-      background-image: none;
-      -webkit-border-radius: 2px;
-      -moz-border-radius: 2px;
-      border-radius: 2px;
-      -webkit-box-sizing: content-box;
-      -moz-box-sizing: content-box;
-      box-sizing: content-box;
-    }
-    button.openAdd div {
-      width: auto;
-      color: #444;
-    }
-
     .suggestBox {
       margin-bottom: 2px;
     }
@@ -48,21 +31,19 @@
       font-weight: bold;
     }
 
+    .addReviewer,
     .cancel {
+      cursor: pointer;
       float: right;
     }
   </ui:style>
   <g:HTMLPanel>
     <div>
       <span ui:field='reviewersText'/>
-      <g:Button ui:field='openForm'
-         title='Add reviewers to this change'
-         styleName='{res.style.button}'
-         addStyleNames='{style.openAdd}'
-         visible='false'>
-       <ui:attribute name='title'/>
-       <div><ui:msg>Add&#8230;</ui:msg></div>
-      </g:Button>
+      <g:Image ui:field='addReviewerIcon'
+          resource='{ico.addUser}'
+          styleName='{style.addReviewer}'
+          title='Add Reviewer'/>
     </div>
     <div ui:field='form' style='display: none' aria-hidden='true'>
       <u:RemoteSuggestBox ui:field='suggestBox' styleName='{style.suggestBox}'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
index bd9bec3..025668f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
@@ -83,10 +83,9 @@
 
   private void initTopicLink(ChangeInfo info) {
     if (info.topic() != null && !info.topic().isEmpty()) {
-      text.setText(info.topic());
-      text.setTargetHistoryToken(
-          PageLinks.toChangeQuery(
-              PageLinks.op("topic", info.topic())));
+      String topic = info.topic();
+      text.setText(topic);
+      text.setTargetHistoryToken(PageLinks.topicQuery(info.status(), topic));
     }
   }
 
@@ -130,9 +129,7 @@
         new GerritCallback<String>() {
           @Override
           public void onSuccess(String result) {
-            Gerrit.display(PageLinks.toChange(
-                psId.getParentKey(),
-                String.valueOf(psId.get())));
+            Gerrit.display(PageLinks.toChange(psId));
           }
         });
     onCancel(null);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
index e7e24b4..c2a6fbd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
@@ -22,11 +22,11 @@
   <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
   <ui:style gss='false'>
-    .show { cursor: pointer; }
+    .edit { cursor: pointer; }
     .edit, .cancel { float: right; }
   </ui:style>
   <g:HTMLPanel>
-    <div ui:field='show' styleName='{style.show}'>
+    <div ui:field='show'>
       <x:InlineHyperlink ui:field='text'
           title='Search for changes on this topic'/>
       <g:Image ui:field='editIcon'
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index f53ecf8..62c14cb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -95,7 +95,7 @@
   }
 
   private static String queryIncoming(String who) {
-    return "is:open reviewer:" + who + " -owner:" + who;
+    return "is:open reviewer:" + who + " -owner:" + who + " -star:ignore";
   }
 
   private static String queryClosed(String who) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index 9cafeec..b181341 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -97,6 +97,14 @@
     return call(id, "detail");
   }
 
+  public static RestApi blame(PatchSet.Id id, String path, boolean base) {
+    return revision(id)
+        .view("files")
+        .id(path)
+        .view("blame")
+        .addParameter("base", base);
+  }
+
   public static RestApi actions(int id, String revision) {
     if (revision == null || revision.equals("")) {
       revision = "current";
@@ -155,6 +163,10 @@
         .addParameter("n", n);
   }
 
+  public static RestApi vote(int id, int reviewer, String vote) {
+    return reviewer(id, reviewer).view("votes").id(vote);
+  }
+
   public static RestApi reviewer(int id, int reviewer) {
     return change(id).view("reviewers").id(reviewer);
   }
@@ -166,7 +178,7 @@
   public static RestApi hashtags(int changeId) {
     return change(changeId).view("hashtags");
   }
-  public static RestApi hashtag(int changeId, String hashtag){
+  public static RestApi hashtag(int changeId, String hashtag) {
     return change(changeId).view("hashtags").id(hashtag);
   }
 
@@ -188,8 +200,7 @@
 
   /** Submit a specific revision of a change. */
   public static void submit(int id, String commit, AsyncCallback<SubmitInfo> cb) {
-    SubmitInput in = SubmitInput.create();
-    in.waitForMerge(true);
+    JavaScriptObject in = JavaScriptObject.createObject();
     call(id, commit, "submit").post(in, cb);
   }
 
@@ -283,17 +294,6 @@
     }
   }
 
-  private static class SubmitInput extends JavaScriptObject {
-    final native void waitForMerge(boolean b) /*-{ this.wait_for_merge=b; }-*/;
-
-    static SubmitInput create() {
-      return (SubmitInput) createObject();
-    }
-
-    protected SubmitInput() {
-    }
-  }
-
   private static RestApi call(int id, String action) {
     return change(id).view(action);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 1fb997f..b2334d1d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -33,18 +33,10 @@
   String outgoingReviews();
   String recentlyClosed();
 
-  String starredHeading();
-  String watchedHeading();
-  String draftsHeading();
-  String allOpenChanges();
-  String allAbandonedChanges();
-  String allMergedChanges();
-
   String changeTableColumnSubject();
   String changeTableColumnSize();
   String changeTableColumnStatus();
   String changeTableColumnOwner();
-  String changeTableColumnReviewers();
   String changeTableColumnProject();
   String changeTableColumnBranch();
   String changeTableColumnLastUpdate();
@@ -57,9 +49,6 @@
   String changeTablePagePrev();
   String changeTablePageNext();
   String upToChangeList();
-  String expandCollapseDependencies();
-  String previousPatchSet();
-  String nextPatchSet();
   String keyReloadChange();
   String keyNextPatchSet();
   String keyPreviousPatchSet();
@@ -73,87 +62,27 @@
   String patchTableColumnName();
   String patchTableColumnComments();
   String patchTableColumnSize();
-  String patchTableColumnDiff();
-  String patchTableDiffSideBySide();
-  String patchTableDiffUnified();
-  String patchTableDownloadPreImage();
-  String patchTableDownloadPostImage();
-  String patchTableBinary();
   String commitMessage();
-  String fileCommentHeader();
 
   String patchTablePrev();
   String patchTableNext();
   String patchTableOpenDiff();
-  String patchTableOpenUnifiedDiff();
-  String upToChangeIconLink();
-  String prevPatchLinkIcon();
-  String nextPatchLinkIcon();
 
-  String changeScreenIncludedIn();
-  String changeScreenDependencies();
-  String changeScreenDependsOn();
-  String changeScreenNeededBy();
-  String changeScreenComments();
-  String changeScreenAddComment();
-
-  String approvalTableReviewer();
-  String approvalTableAddReviewer();
-  String approvalTableRemoveNotPermitted();
-  String approvalTableCouldNotRemove();
   String approvalTableAddReviewerHint();
   String approvalTableAddManyReviewersConfirmationDialogTitle();
 
-  String changeInfoBlockOwner();
-  String changeInfoBlockProject();
-  String changeInfoBlockBranch();
-  String changeInfoBlockTopic();
-  String changeInfoBlockTopicAlterTopicToolTip();
   String changeInfoBlockUploaded();
   String changeInfoBlockUpdated();
-  String changeInfoBlockStatus();
-  String changeInfoBlockSubmitType();
-  String changePermalink();
-  String changeInfoBlockCanMerge();
-  String changeInfoBlockCanMergeYes();
-  String changeInfoBlockCanMergeNo();
-
-  String buttonAlterTopic();
-  String buttonAlterTopicBegin();
-  String buttonAlterTopicSend();
-  String buttonAlterTopicCancel();
-  String headingAlterTopicMessage();
-  String alterTopicTitle();
-  String alterTopicLabel();
-
-  String includedInTableBranch();
-  String includedInTableTag();
 
   String messageNoAuthor();
-  String messageExpandMostRecent();
-  String messageExpandRecent();
-  String messageExpandAll();
-  String messageCollapseAll();
-  String messageNeedsRebaseOrHasDependency();
 
   String sideBySide();
   String unifiedDiff();
 
-  String patchSetInfoAuthor();
-  String patchSetInfoCommitter();
-  String patchSetInfoDownload();
-  String patchSetInfoParents();
-  String patchSetWithDraftCommentsToolTip();
-  String initialCommit();
-
-  String buttonRebaseChange();
-
-  String buttonRevertChangeBegin();
   String buttonRevertChangeSend();
   String headingRevertMessage();
   String revertChangeTitle();
 
-  String buttonCherryPickChangeBegin();
   String buttonCherryPickChangeSend();
   String headingCherryPickBranch();
   String cherryPickCommitMessage();
@@ -165,41 +94,13 @@
   String rebasePlaceholderMessage();
   String rebaseTitle();
 
-  String buttonAbandonChangeBegin();
-  String buttonAbandonChangeSend();
-  String headingAbandonMessage();
-  String abandonChangeTitle();
-  String referenceVersion();
   String baseDiffItem();
   String autoMerge();
 
-  String buttonReview();
-  String buttonPublishCommentsSend();
-  String buttonPublishCommentsCancel();
-  String headingCoverMessage();
-  String headingPatchComments();
-
-  String buttonRestoreChangeBegin();
-  String restoreChangeTitle();
-  String headingRestoreMessage();
-  String buttonRestoreChangeSend();
-
-  String buttonPublishPatchSet();
-
-  String buttonDeleteDraftChange();
-  String buttonDeleteDraftPatchSet();
-
   String pagedChangeListPrev();
   String pagedChangeListNext();
 
-  String draftPatchSetLabel();
-
-  String reviewed();
   String submitFailed();
-  String buttonClose();
-
-  String diffAllSideBySide();
-  String diffAllUnified();
 
   String votable();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index a5fa7b4..b7e2677 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -8,23 +8,16 @@
 notCurrent = Not Current
 changeEdit = Change Edit
 
-starredHeading = Starred Changes
-watchedHeading = Open Changes of Watched Projects
-draftsHeading = Changes with unpublished drafts
 myDashboardTitle = My Reviews
 unknownDashboardTitle = Code Review Dashboard
 incomingReviews = Incoming reviews
 outgoingReviews = Outgoing reviews
 recentlyClosed = Recently closed
-allOpenChanges = All open changes
-allAbandonedChanges = All abandoned changes
-allMergedChanges = All merged changes
 
 changeTableColumnSubject = Subject
 changeTableColumnSize = Size
 changeTableColumnStatus = Status
 changeTableColumnOwner = Owner
-changeTableColumnReviewers = Reviewers
 changeTableColumnProject = Project
 changeTableColumnBranch = Branch
 changeTableColumnLastUpdate = Updated
@@ -37,9 +30,6 @@
 changeTablePagePrev = Previous page of changes
 changeTablePageNext = Next page of changes
 upToChangeList = Up to change list
-expandCollapseDependencies = Expands / Collapses dependencies section
-previousPatchSet = Previous patch set
-nextPatchSet = Next patch set
 keyReloadChange = Reload change
 keyNextPatchSet = Next patch set
 keyPreviousPatchSet = Previous patch set
@@ -54,92 +44,30 @@
 patchTableColumnName = File Path
 patchTableColumnComments = Comments
 patchTableColumnSize = Size
-patchTableColumnDiff = Diff
-patchTableDiffSideBySide = Side-by-Side
-patchTableDiffUnified = Unified
-patchTableDownloadPreImage = old
-patchTableDownloadPostImage = new
-patchTableBinary = Binary
 commitMessage = Commit Message
-fileCommentHeader = File Comment:
 
 patchTablePrev = Previous file
 patchTableNext = Next file
 patchTableOpenDiff = Open diff
-patchTableOpenUnifiedDiff = Open unified diff
 
-changeScreenIncludedIn =  Included in
-changeScreenDependencies =  Dependencies
-changeScreenDependsOn = Depends On
-changeScreenNeededBy = Needed By
-changeScreenComments = Comments
-changeScreenAddComment = Add Comment
-
-approvalTableReviewer = Reviewer
-approvalTableAddReviewer = Add Reviewer
-approvalTableRemoveNotPermitted = Not allowed to remove reviewer
-approvalTableCouldNotRemove = Could not remove reviewer
 approvalTableAddReviewerHint = Name or Email or Group
 approvalTableAddManyReviewersConfirmationDialogTitle = Adding Group Members as Reviewers
 
-changeInfoBlockOwner = Owner
-changeInfoBlockProject = Project
-changeInfoBlockBranch = Branch
-changeInfoBlockTopic = Topic
-changeInfoBlockTopicAlterTopicToolTip = Edit Topic
 changeInfoBlockUploaded = Uploaded
 changeInfoBlockUpdated = Updated
-changeInfoBlockStatus = Status
-changeInfoBlockSubmitType = Submit Type
-changePermalink = Permalink
-changeInfoBlockCanMerge = Can Merge
-changeInfoBlockCanMergeYes = Yes
-changeInfoBlockCanMergeNo = No
-
-buttonAlterTopic = Edit Topic
-buttonAlterTopicBegin = Edit Topic
-buttonAlterTopicSend = Update Topic
-buttonAlterTopicCancel = Cancel
-headingAlterTopicMessage = Edit Topic Message:
-alterTopicTitle = Code Review - Edit Topic
-alterTopicLabel = New Topic Name:
-
-includedInTableBranch = Branch Name
-includedInTableTag = Tag Name
 
 messageNoAuthor = Gerrit Code Review
-messageExpandMostRecent = Expand Most Recent
-messageExpandRecent = Expand Recent
-messageExpandAll = Expand All
-messageCollapseAll = Collapse All
-messageNeedsRebaseOrHasDependency = Need Rebase or Has Dependency
 
 sideBySide = Side by Side
 unifiedDiff = Unified Diff
 
-patchSetInfoAuthor = Author
-patchSetInfoCommitter = Committer
-patchSetInfoDownload = Download
-patchSetInfoParents = Parent(s)
-patchSetWithDraftCommentsToolTip = Draft comment(s) inside
-initialCommit = Initial Commit
-
-buttonAbandonChangeBegin = Abandon Change
-buttonAbandonChangeSend = Abandon Change
-headingAbandonMessage = Abandon Message:
-abandonChangeTitle = Code Review - Abandon Change
-referenceVersion = Reference Version:
 baseDiffItem = Base
 autoMerge = Auto Merge
 
-buttonRebaseChange = Rebase Change
-
-buttonRevertChangeBegin = Revert Change
 buttonRevertChangeSend = Revert Change
 headingRevertMessage = Revert Commit Message:
 revertChangeTitle = Code Review - Revert Merged Change
 
-buttonCherryPickChangeBegin = Cherry Pick To
 buttonCherryPickChangeSend = Cherry Pick Change
 headingCherryPickBranch = Cherry Pick to Branch:
 cherryPickCommitMessage = Cherry Pick Commit Message:
@@ -151,37 +79,10 @@
 rebasePlaceholderMessage = (subject, change number, or leave empty)
 rebaseTitle = Code Review - Rebase Change
 
-buttonRestoreChangeBegin = Restore Change
-restoreChangeTitle = Code Review - Restore Change
-headingRestoreMessage = Restore Message:
-buttonRestoreChangeSend = Restore Change
-
-buttonReview = Review
-buttonPublishCommentsSend = Publish Comments
-buttonPublishCommentsCancel = Cancel
-headingCoverMessage = Cover Message:
-headingPatchComments = Patch Comments:
-
-buttonPublishPatchSet = Publish
-
-buttonDeleteDraftChange = Delete Draft Change
-buttonDeleteDraftPatchSet = Delete Draft Patch Set
-
 pagedChangeListPrev = &#x21e6;Prev
 pagedChangeListNext = Next&#x21e8;
 
-draftPatchSetLabel = (DRAFT)
-
-upToChangeIconLink = &#x21e7;Up to change
-prevPatchLinkIcon = &#x21e6;
-nextPatchLinkIcon = &#x21e8;
-
-reviewed = Reviewed
 submitFailed = Submit Failed
-buttonClose = Close
-
-diffAllSideBySide = All Side-by-Side
-diffAllUnified = All Unified
 
 votable = Votable:
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
index 0928cd8..f86ddf7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
@@ -28,7 +28,7 @@
 /** REST API helpers to remotely edit a change. */
 public class ChangeEditApi {
   /** Get file (or commit message) contents. */
-  public static void get(PatchSet.Id id, String path,
+  public static void get(PatchSet.Id id, String path, boolean base,
       HttpCallback<NativeString> cb) {
     RestApi api;
     if (id.get() != 0) {
@@ -36,13 +36,19 @@
       // exist for the caller, or is not currently active.
       api = ChangeApi.revision(id).view("files").id(path).view("content");
     } else if (Patch.COMMIT_MSG.equals(path)) {
-      api = editMessage(id.getParentKey().get());
+      api = editMessage(id.getParentKey().get()).addParameter("base", base);
     } else {
-      api = editFile(id.getParentKey().get(), path);
+      api = editFile(id.getParentKey().get(), path).addParameter("base", base);
     }
     api.get(cb);
   }
 
+  /** Get file (or commit message) contents of the edit. */
+  public static void get(PatchSet.Id id, String path,
+      HttpCallback<NativeString> cb) {
+    get(id, path, false, cb);
+  }
+
   /** Get meta info for change edit. */
   public static void getMeta(PatchSet.Id id, String path,
       AsyncCallback<EditFileInfo> cb) {
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 ef74a65..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
@@ -18,49 +18,30 @@
 
 public interface ChangeMessages extends Messages {
   String accountDashboardTitle(String fullName);
-  String changesOpenInProject(String string);
-  String changesMergedInProject(String string);
-  String changesAbandonedInProject(String string);
 
   String revertChangeDefaultMessage(String commitMsg, String commitId);
 
   String cherryPickedChangeDefaultMessage(String commitMsg, String commitId);
   String changeScreenTitleId(String changeId);
-  String outdatedHeader(int outdated);
-  String patchSetHeader(int id);
   String loadingPatchSet(int id);
-  String submitPatchSet(int id);
 
-  String patchTableComments(@PluralCount int count);
-  String patchTableDrafts(@PluralCount int count);
   String patchTableSize_Modify(int insertions, int deletions);
   String patchTableSize_ModifyBinaryFiles(String bytesInserted,
       String bytesDeleted);
+  String patchTableSize_ModifyBinaryFilesWithPercentages(String bytesInserted,
+      String percentageInserted, String bytesDeleted, String percentageDeleted);
   String patchTableSize_LongModify(int insertions, int deletions);
-  String patchTableSize_Lines(@PluralCount int insertions);
 
-  String removeHashtag(String name);
   String removeReviewer(String fullName);
-  String messageWrittenOn(String date);
+  String removeVote(String label);
 
-  String renamedFrom(String sourcePath);
-  String copiedFrom(String sourcePath);
-  String otherFrom(String sourcePath);
-
-  String needApproval(String labelName);
-  String publishComments(String changeId, int ps);
-  String lineHeader(int line);
+  String blockedOn(String labelName);
+  String needs(String labelName);
 
   String changeQueryWindowTitle(String query);
   String changeQueryPageTitle(String query);
 
-  String reviewerNotFound(String who);
-  String accountInactive(String who);
-  String changeNotVisibleTo(String who);
-  String groupIsEmpty(String group);
-  String groupIsNotAllowed(String group);
-  String groupHasTooManyMembers(String group);
-  String groupManyMembersConfirmation(String group, int memberCount);
-
   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 67ef2c3..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
@@ -1,47 +1,27 @@
 # Changes to this file should also be made in
 # gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties
 accountDashboardTitle = Code Review Dashboard for {0}
-changesOpenInProject = Open Changes In {0}
-changesMergedInProject = Merged Changes In {0}
-changesAbandonedInProject = Abandoned Changes In {0}
 
 revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}.
 cherryPickedChangeDefaultMessage = {0}\n(cherry picked from commit {1})
 
 changeScreenTitleId = Change {0}
-outdatedHeader = Change depends on {0} outdated change(s) and should be rebased on the latest patch sets.
-patchSetHeader = Patch Set {0}
 loadingPatchSet = Loading Patch Set {0} ...
-submitPatchSet = Submit Patch Set {0}
 
-patchTableComments = {0} comments
-patchTableDrafts = {0} drafts
 patchTableSize_Modify = +{0}, -{1}
 patchTableSize_ModifyBinaryFiles = +{0}, -{1}
+patchTableSize_ModifyBinaryFilesWithPercentages = +{0} (+{1}), -{2} (-{3})
 patchTableSize_LongModify = {0} inserted, {1} deleted
-patchTableSize_Lines = {0} lines
 
-removeHashtag = Remove hashtag {0}
 removeReviewer = Remove reviewer {0}
-messageWrittenOn = on {0}
+removeVote = Remove vote {0}
 
-renamedFrom = renamed from {0}
-copiedFrom = copied from {0}
-otherFrom = from {0}
-
-needApproval = Need {0}
-publishComments = Change {0} - Patch Set {1}: Publish Comments
-lineHeader = Line {0}:
+blockedOn = Blocked on {0} Label
+needs = Needs {0} Label
 
 changeQueryWindowTitle = {0}
 changeQueryPageTitle = Search for {0}
 
-reviewerNotFound = {0} is neither a registered user nor a group.
-accountInactive = {0} is not an active user.
-changeNotVisibleTo = {0} cannot access the change.
-groupIsEmpty = The group {0} does not have any members to add as reviewers.
-groupIsNotAllowed =  The group {0} cannot be added as reviewer.
-groupHasTooManyMembers = The group {0} has too many members to add them all as reviewers.
-groupManyMembersConfirmation = The group {0} has {1} members. Do you want to add them all as reviewers?
-
 insertionsAndDeletions = +{0}, -{1}
+
+diffBaseParent = Parent {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages_en.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages_en.properties
deleted file mode 100644
index d9c7059..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages_en.properties
+++ /dev/null
@@ -1,8 +0,0 @@
-patchTableComments[one] = 1 comment
-patchTableComments = {0} comments
-
-patchTableDrafts[one] = 1 draft
-patchTableDrafts = {0} drafts
-
-patchTableSize_Lines[one] = 1 line
-patchTableSize_Lines = {0} lines
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 6136825..9c78955 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -28,8 +28,8 @@
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
 import com.google.gerrit.client.ui.ProjectLink;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.ReviewCategoryStrategy;
 import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -350,6 +350,7 @@
         return accountInfo.username();
       case ABBREV:
         return getAbbreviation(accountInfo.name(), " ");
+      case NONE:
       default:
         return null;
     }
@@ -492,7 +493,7 @@
       if (titleText != null) {
         setTitleText(titleText);
         return true;
-      } else if(titleWidget != null) {
+      } else if (titleWidget != null) {
         setTitleWidget(titleWidget);
         return true;
       }
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 9088c1c..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());
@@ -77,7 +85,9 @@
   public final void side(Side side) {
     sideRaw(side.toString());
   }
-  private final native void sideRaw(String s) /*-{ this.side = s }-*/;
+  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 }-*/;
@@ -90,7 +100,8 @@
         ? Side.valueOf(s)
         : Side.REVISION;
   }
-  private final native String sideRaw() /*-{ return this.side }-*/;
+  private native String sideRaw() /*-{ return this.side }-*/;
+  public final native int parent() /*-{ return this.parent }-*/;
 
   public final Timestamp updated() {
     Timestamp r = updatedTimestamp();
@@ -103,9 +114,9 @@
     }
     return r;
   }
-  private final native String updatedRaw() /*-{ return this.updated }-*/;
-  private final native Timestamp updatedTimestamp() /*-{ return this._ts }-*/;
-  private final native void updatedTimestamp(Timestamp t) /*-{ this._ts = t }-*/;
+  private native String updatedRaw() /*-{ return this.updated }-*/;
+  private native Timestamp updatedTimestamp() /*-{ return this._ts }-*/;
+  private native void updatedTimestamp(Timestamp t) /*-{ this._ts = t }-*/;
 
   public final native AccountInfo author() /*-{ return this.author }-*/;
   public final native int line() /*-{ return this.line || 0 }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index 2b3c6ae..80117be 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
@@ -20,11 +20,23 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gwt.regexp.shared.RegExp;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 
 public class QueryScreen extends PagedSingleListScreen implements
     ChangeListScreen {
+  // Legacy numeric identifier.
+  private static final RegExp NUMERIC_ID = RegExp.compile("^[1-9][0-9]*$");
+  // Commit SHA1 hash
+  private static final RegExp COMMIT_SHA1 =
+      RegExp.compile("^([0-9a-fA-F]{4," + RevId.LEN + "})$");
+  // Change-Id
+  private static final String ID_PATTERN = "[iI][0-9a-f]{4,}$";
+  private static final RegExp CHANGE_ID = RegExp.compile("^" + ID_PATTERN);
+  private static final RegExp CHANGE_ID_TRIPLET =
+      RegExp.compile("^(.)+~(.)+~" + ID_PATTERN);
+
   public static QueryScreen forQuery(String query) {
     return forQuery(query, 0);
   }
@@ -51,7 +63,7 @@
   protected AsyncCallback<ChangeList> loadCallback() {
     return new GerritCallback<ChangeList>() {
       @Override
-      public final void onSuccess(ChangeList result) {
+      public void onSuccess(ChangeList result) {
         if (isAttached()) {
           if (result.length() == 1 && isSingleQuery(query)) {
             ChangeInfo c = result.get(0);
@@ -80,24 +92,9 @@
   }
 
   private static boolean isSingleQuery(String query) {
-    if (query.matches("^[1-9][0-9]*$")) {
-      // Legacy numeric identifier.
-      //
-      return true;
-    }
-
-    if (query.matches("^[iI][0-9a-f]{4,}$")) {
-      // Newer style Change-Id.
-      //
-      return true;
-    }
-
-    if (query.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$")) {
-      // Commit SHA-1 of any change.
-      //
-      return true;
-    }
-
-    return false;
+    return NUMERIC_ID.test(query)
+        || CHANGE_ID.test(query)
+        || CHANGE_ID_TRIPLET.test(query)
+        || COMMIT_SHA1.test(query);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
index 096dbd0..a127e7f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
@@ -19,11 +19,11 @@
 import com.google.gwt.core.client.JsArray;
 
 public class ReviewInput extends JavaScriptObject {
-  public static enum NotifyHandling {
+  public enum NotifyHandling {
     NONE, OWNER, OWNER_REVIEWERS, ALL
   }
 
-  public static enum DraftHandling {
+  public enum DraftHandling {
     DELETE, PUBLISH, KEEP, PUBLISH_ALL_REVISIONS
   }
 
@@ -42,14 +42,14 @@
   public final void notify(NotifyHandling e) {
     _notify(e.name());
   }
-  private final native void _notify(String n) /*-{ this.notify=n; }-*/;
+  private native void _notify(String n) /*-{ this.notify=n; }-*/;
 
   public final void drafts(DraftHandling e) {
     _drafts(e.name());
   }
-  private final native void _drafts(String n) /*-{ this.drafts=n; }-*/;
+  private native void _drafts(String n) /*-{ this.drafts=n; }-*/;
 
-  private final native void init() /*-{
+  private native void init() /*-{
     this.labels = {};
     this.strict_labels = true;
   }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
index be64a3d..43b3b80 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
@@ -37,8 +37,8 @@
   private static final Event.Type<ChangeStarHandler> TYPE = new Event.Type<>();
 
   /** Handler that can receive notifications of a change's starred status. */
-  public static interface ChangeStarHandler {
-    public void onChangeStar(ChangeStarEvent event);
+  public interface ChangeStarHandler {
+    void onChangeStar(ChangeStarEvent event);
   }
 
   /** Event fired when a star changes status. The new status is reported. */
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
index 6d8bc30..7a24774 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
@@ -22,7 +22,7 @@
     return Change.Status.valueOf(statusRaw());
   }
 
-  private final native String statusRaw() /*-{ return this.status; }-*/;
+  private native String statusRaw() /*-{ return this.status; }-*/;
 
   protected SubmitInfo() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
index 28812ac..47393a7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.config;
 
 import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gerrit.client.info.GeneralPreferences;
 import com.google.gerrit.client.info.ServerInfo;
 import com.google.gerrit.client.info.TopMenuList;
 import com.google.gerrit.client.rpc.NativeMap;
@@ -37,7 +37,7 @@
     new RestApi("/config/server/top-menus").get(cb);
   }
 
-  public static void defaultPreferences(AsyncCallback<AccountPreferencesInfo> cb) {
+  public static void defaultPreferences(AsyncCallback<GeneralPreferences> cb) {
     new RestApi("/config/server/preferences").get(cb);
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
index 3b168e1..47c0359 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
@@ -84,7 +84,7 @@
     });
 
     String ref = null;
-    for(DashboardInfo d : list) {
+    for (DashboardInfo d : list) {
       if (!d.ref().equals(ref)) {
         ref = d.ref();
         insertTitleRow(table.getRowCount(), ref);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
index 8ff11e8..5257ae0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
@@ -15,230 +15,63 @@
 package com.google.gerrit.client.diff;
 
 import static com.google.gerrit.client.diff.DisplaySide.A;
-import static com.google.gerrit.client.diff.DisplaySide.B;
 
-import com.google.gerrit.client.diff.DiffInfo.Region;
-import com.google.gerrit.client.diff.DiffInfo.Span;
-import com.google.gerrit.client.rpc.Natives;
 import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.EventListener;
 
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.CodeMirror.LineClassWhere;
-import net.codemirror.lib.Configuration;
-import net.codemirror.lib.LineWidget;
 import net.codemirror.lib.Pos;
 import net.codemirror.lib.TextMarker;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 
-/** Colors modified regions for {@link SideBySide}. */
-class ChunkManager {
-  private static final String DATA_LINES = "_cs2h";
-  private static double guessedLineHeightPx = 15;
-  private static final JavaScriptObject focusA = initOnClick(A);
-  private static final JavaScriptObject focusB = initOnClick(B);
-  private static final native JavaScriptObject initOnClick(DisplaySide s) /*-{
-    return $entry(function(e){
-      @com.google.gerrit.client.diff.ChunkManager::focus(
-        Lcom/google/gwt/dom/client/NativeEvent;
-        Lcom/google/gerrit/client/diff/DisplaySide;)(e,s)
-    });
-  }-*/;
-
-  private static void focus(NativeEvent event, DisplaySide side) {
-    Element e = Element.as(event.getEventTarget());
-    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
-      EventListener l = DOM.getEventListener(e);
-      if (l instanceof SideBySide) {
-        ((SideBySide) l).getCmFromSide(side).focus();
-        event.stopPropagation();
-      }
-    }
-  }
-
-  static void focusOnClick(Element e, DisplaySide side) {
-    onClick(e, side == A ? focusA : focusB);
-  }
-
-  private static final native void onClick(Element e, JavaScriptObject f)
+/** Colors modified regions for {@link SideBySide} and {@link Unified}. */
+abstract class ChunkManager {
+  static final native void onClick(Element e, JavaScriptObject f)
   /*-{ e.onclick = f }-*/;
 
-  private final SideBySide host;
-  private final CodeMirror cmA;
-  private final CodeMirror cmB;
-  private final Scrollbar scrollbar;
-  private final LineMapper mapper;
+  final Scrollbar scrollbar;
+  final LineMapper lineMapper;
 
-  private List<DiffChunkInfo> chunks;
   private List<TextMarker> markers;
   private List<Runnable> undo;
-  private List<LineWidget> padding;
-  private List<Element> paddingDivs;
 
-  ChunkManager(SideBySide host,
-      CodeMirror cmA,
-      CodeMirror cmB,
-      Scrollbar scrollbar) {
-    this.host = host;
-    this.cmA = cmA;
-    this.cmB = cmB;
+  ChunkManager(Scrollbar scrollbar) {
     this.scrollbar = scrollbar;
-    this.mapper = new LineMapper();
+    this.lineMapper = new LineMapper();
   }
 
-  LineMapper getLineMapper() {
-    return mapper;
-  }
+  abstract DiffChunkInfo getFirst();
 
-  DiffChunkInfo getFirst() {
-    return !chunks.isEmpty() ? chunks.get(0) : null;
+  List<TextMarker> getMarkers() {
+    return markers;
   }
 
   void reset() {
-    mapper.reset();
+    lineMapper.reset();
     for (TextMarker m : markers) {
       m.clear();
     }
     for (Runnable r : undo) {
       r.run();
     }
-    for (LineWidget w : padding) {
-      w.clear();
-    }
   }
 
-  void render(DiffInfo diff) {
-    chunks = new ArrayList<>();
+  abstract void render(DiffInfo diff);
+
+  void render() {
     markers = new ArrayList<>();
     undo = new ArrayList<>();
-    padding = new ArrayList<>();
-    paddingDivs = new ArrayList<>();
-
-    String diffColor = diff.metaA() == null || diff.metaB() == null
-        ? DiffTable.style.intralineBg()
-        : DiffTable.style.diff();
-
-    for (Region current : Natives.asList(diff.content())) {
-      if (current.ab() != null) {
-        mapper.appendCommon(current.ab().length());
-      } else if (current.skip() > 0) {
-        mapper.appendCommon(current.skip());
-      } else if (current.common()) {
-        mapper.appendCommon(current.b().length());
-      } else {
-        render(current, diffColor);
-      }
-    }
-
-    if (paddingDivs.isEmpty()) {
-      paddingDivs = null;
-    }
   }
 
-  void adjustPadding() {
-    if (paddingDivs != null) {
-      double h = cmB.extras().lineHeightPx();
-      for (Element div : paddingDivs) {
-        int lines = div.getPropertyInt(DATA_LINES);
-        div.getStyle().setHeight(lines * h, Unit.PX);
-      }
-      for (LineWidget w : padding) {
-        w.changed();
-      }
-      paddingDivs = null;
-      guessedLineHeightPx = h;
-    }
-  }
-
-  private void render(Region region, String diffColor) {
-    int startA = mapper.getLineA();
-    int startB = mapper.getLineB();
-
-    JsArrayString a = region.a();
-    JsArrayString b = region.b();
-    int aLen = a != null ? a.length() : 0;
-    int bLen = b != null ? b.length() : 0;
-
-    String color = a == null || b == null
-        ? diffColor
-        : DiffTable.style.intralineBg();
-
-    colorLines(cmA, color, startA, aLen);
-    colorLines(cmB, color, startB, bLen);
-    markEdit(cmA, startA, a, region.editA());
-    markEdit(cmB, startB, b, region.editB());
-    addPadding(cmA, startA + aLen - 1, bLen - aLen);
-    addPadding(cmB, startB + bLen - 1, aLen - bLen);
-    addGutterTag(region, startA, startB);
-    mapper.appendReplace(aLen, bLen);
-
-    int endA = mapper.getLineA() - 1;
-    int endB = mapper.getLineB() - 1;
-    if (aLen > 0) {
-      addDiffChunk(cmB, endA, aLen, bLen > 0);
-    }
-    if (bLen > 0) {
-      addDiffChunk(cmA, endB, bLen, aLen > 0);
-    }
-  }
-
-  private void addGutterTag(Region region, int startA, int startB) {
-    if (region.a() == null) {
-      scrollbar.insert(cmB, startB, region.b().length());
-    } else if (region.b() == null) {
-      scrollbar.delete(cmA, cmB, startA, region.a().length());
-    } else {
-      scrollbar.edit(cmB, startB, region.b().length());
-    }
-  }
-
-  private void markEdit(CodeMirror cm, int startLine,
-      JsArrayString lines, JsArray<Span> edits) {
-    if (lines == null || edits == null) {
-      return;
-    }
-
-    EditIterator iter = new EditIterator(lines, startLine);
-    Configuration bg = Configuration.create()
-        .set("className", DiffTable.style.intralineBg())
-        .set("readOnly", true);
-
-    Configuration diff = Configuration.create()
-        .set("className", DiffTable.style.diff())
-        .set("readOnly", true);
-
-    Pos last = Pos.create(0, 0);
-    for (Span span : Natives.asList(edits)) {
-      Pos from = iter.advance(span.skip());
-      Pos to = iter.advance(span.mark());
-      if (from.line() == last.line()) {
-        markers.add(cm.markText(last, from, bg));
-      } else {
-        markers.add(cm.markText(Pos.create(from.line(), 0), from, bg));
-      }
-      markers.add(cm.markText(from, to, diff));
-      last = to;
-      colorLines(cm, LineClassWhere.BACKGROUND,
-          DiffTable.style.diff(),
-          from.line(), to.line());
-    }
-  }
-
-  private void colorLines(CodeMirror cm, String color, int line, int cnt) {
+  void colorLines(CodeMirror cm, String color, int line, int cnt) {
     colorLines(cm, LineClassWhere.WRAP, color, line, line + cnt);
   }
 
-  private void colorLines(final CodeMirror cm, final LineClassWhere where,
+  void colorLines(final CodeMirror cm, final LineClassWhere where,
       final String className, final int start, final int end) {
     if (start < end) {
       for (int line = start; line < end; line++) {
@@ -255,78 +88,38 @@
     }
   }
 
-  /**
-   * Insert a new padding div below the given line.
-   *
-   * @param cm parent CodeMirror to add extra space into.
-   * @param line line to put the padding below.
-   * @param len number of lines to pad. Padding is inserted only if
-   *        {@code len >= 1}.
-   */
-  private void addPadding(CodeMirror cm, int line, final int len) {
-    if (0 < len) {
-      Element pad = DOM.createDiv();
-      pad.setClassName(DiffTable.style.padding());
-      pad.setPropertyInt(DATA_LINES, len);
-      pad.getStyle().setHeight(guessedLineHeightPx * len, Unit.PX);
-      focusOnClick(pad, cm.side());
-      paddingDivs.add(pad);
-      padding.add(cm.addLineWidget(
-        line == -1 ? 0 : line,
-        pad,
-        Configuration.create()
-          .set("coverGutter", true)
-          .set("noHScroll", true)
-          .set("above", line == -1)));
+  abstract Runnable diffChunkNav(final CodeMirror cm, final Direction dir);
+
+  void diffChunkNavHelper(List<? extends DiffChunkInfo> chunks,
+      DiffScreen host, int res, Direction dir) {
+    if (res < 0) {
+      res = -res - (dir == Direction.PREV ? 1 : 2);
     }
-  }
+    res = res + (dir == Direction.PREV ? -1 : 1);
+    if (res < 0 || chunks.size() <= res) {
+      return;
+    }
 
-  private void addDiffChunk(CodeMirror cmToPad, int lineOnOther,
-      int chunkSize, boolean edit) {
-    chunks.add(new DiffChunkInfo(host.otherCm(cmToPad).side(),
-        lineOnOther - chunkSize + 1, lineOnOther, edit));
-  }
-
-  Runnable diffChunkNav(final CodeMirror cm, final Direction dir) {
-    return new Runnable() {
-      @Override
-      public void run() {
-        int line = cm.extras().hasActiveLine()
-            ? cm.getLineNumber(cm.extras().activeLine())
-            : 0;
-        int res = Collections.binarySearch(
-                chunks,
-                new DiffChunkInfo(cm.side(), line, 0, false),
-                getDiffChunkComparator());
-        if (res < 0) {
-          res = -res - (dir == Direction.PREV ? 1 : 2);
-        }
-        res = res + (dir == Direction.PREV ? -1 : 1);
-        if (res < 0 || chunks.size() <= res) {
-          return;
-        }
-
-        DiffChunkInfo lookUp = chunks.get(res);
-        // If edit, skip the deletion chunk and set focus on the insertion one.
-        if (lookUp.isEdit() && lookUp.getSide() == A) {
-          res = res + (dir == Direction.PREV ? -1 : 1);
-          if (res < 0 || chunks.size() <= res) {
-            return;
-          }
-        }
-
-        DiffChunkInfo target = chunks.get(res);
-        CodeMirror targetCm = host.getCmFromSide(target.getSide());
-        targetCm.setCursor(Pos.create(target.getStart(), 0));
-        targetCm.focus();
-        targetCm.scrollToY(
-            targetCm.heightAtLine(target.getStart(), "local") -
-            0.5 * cmB.scrollbarV().getClientHeight());
+    DiffChunkInfo lookUp = chunks.get(res);
+    // If edit, skip the deletion chunk and set focus on the insertion one.
+    if (lookUp.isEdit() && lookUp.getSide() == A) {
+      res = res + (dir == Direction.PREV ? -1 : 1);
+      if (res < 0 || chunks.size() <= res) {
+        return;
       }
-    };
+    }
+
+    DiffChunkInfo target = chunks.get(res);
+    CodeMirror targetCm = host.getCmFromSide(target.getSide());
+    int cmLine = getCmLine(target.getStart(), target.getSide());
+    targetCm.setCursor(Pos.create(cmLine));
+    targetCm.focus();
+    targetCm.scrollToY(
+        targetCm.heightAtLine(cmLine, "local")
+        - 0.5 * targetCm.scrollbarV().getClientHeight());
   }
 
-  private Comparator<DiffChunkInfo> getDiffChunkComparator() {
+  Comparator<DiffChunkInfo> getDiffChunkComparator() {
     // Chunks are ordered by their starting line. If it's a deletion,
     // use its corresponding line on the revision side for comparison.
     // In the edit case, put the deletion chunk right before the
@@ -337,35 +130,17 @@
         if (a.getSide() == b.getSide()) {
           return a.getStart() - b.getStart();
         } else if (a.getSide() == A) {
-          int comp = mapper.lineOnOther(a.getSide(), a.getStart())
+          int comp = lineMapper.lineOnOther(a.getSide(), a.getStart())
               .getLine() - b.getStart();
           return comp == 0 ? -1 : comp;
         } else {
           int comp = a.getStart() -
-              mapper.lineOnOther(b.getSide(), b.getStart()).getLine();
+              lineMapper.lineOnOther(b.getSide(), b.getStart()).getLine();
           return comp == 0 ? 1 : comp;
         }
       }
     };
   }
 
-  DiffChunkInfo getDiffChunk(DisplaySide side, int line) {
-    int res = Collections.binarySearch(
-        chunks,
-        new DiffChunkInfo(side, line, 0, false), // Dummy DiffChunkInfo
-        getDiffChunkComparator());
-    if (res >= 0) {
-      return chunks.get(res);
-    } else { // The line might be within a DiffChunk
-      res = -res - 1;
-      if (res > 0) {
-        DiffChunkInfo info = chunks.get(res - 1);
-        if (info.getSide() == side && info.getStart() <= line &&
-            line <= info.getEnd()) {
-          return info;
-        }
-      }
-    }
-    return null;
-  }
+  abstract int getCmLine(int line, DisplaySide side);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
index 0e85a2f..70ef947 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
@@ -24,6 +24,7 @@
 
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.Configuration;
+import net.codemirror.lib.Pos;
 import net.codemirror.lib.TextMarker;
 import net.codemirror.lib.TextMarker.FromTo;
 
@@ -56,12 +57,18 @@
   CommentBox(CommentGroup group, CommentRange range) {
     this.group = group;
     if (range != null) {
-      fromTo = FromTo.create(range);
+      DiffScreen screen = group.getManager().host;
+      int startCmLine =
+          screen.getCmLine(range.startLine() - 1, group.getSide());
+      int endCmLine = screen.getCmLine(range.endLine() - 1, group.getSide());
+      fromTo = FromTo.create(
+          Pos.create(startCmLine, range.startCharacter()),
+          Pos.create(endCmLine, range.endCharacter()));
       rangeMarker = group.getCm().markText(
           fromTo.from(),
           fromTo.to(),
           Configuration.create()
-              .set("className", DiffTable.style.range()));
+              .set("className", Resources.I.diffTableStyle().range()));
     }
     addDomHandler(new MouseOverHandler() {
       @Override
@@ -109,7 +116,7 @@
             fromTo.from(),
             fromTo.to(),
             Configuration.create()
-                .set("className", DiffTable.style.rangeHighlight()));
+                .set("className", Resources.I.diffTableStyle().rangeHighlight()));
       } else if (!highlight && rangeHighlightMarker != null) {
         rangeHighlightMarker.clear();
         rangeHighlightMarker = null;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
index a8a56fc..1d198ec 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
@@ -14,9 +14,6 @@
 
 package com.google.gerrit.client.diff;
 
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -30,39 +27,29 @@
 /**
  * LineWidget attached to a CodeMirror container.
  *
- * When a comment is placed on a line a CommentWidget is created on both sides.
- * The group tracks all comment boxes on that same line, and also includes an
- * empty padding element to keep subsequent lines vertically aligned.
+ * When a comment is placed on a line a CommentWidget is created.
  */
-class CommentGroup extends Composite {
-  static void pair(CommentGroup a, CommentGroup b) {
-    a.peer = b;
-    b.peer = a;
-  }
+abstract class CommentGroup extends Composite {
+
+  final DisplaySide side;
+  final int line;
 
   private final CommentManager manager;
   private final CodeMirror cm;
-  private final int line;
   private final FlowPanel comments;
-  private final Element padding;
   private LineWidget lineWidget;
   private Timer resizeTimer;
-  private CommentGroup peer;
 
-  CommentGroup(CommentManager manager, CodeMirror cm, int line) {
+  CommentGroup(CommentManager manager, CodeMirror cm, DisplaySide side, int line) {
     this.manager = manager;
     this.cm = cm;
+    this.side = side;
     this.line = line;
 
     comments = new FlowPanel();
     comments.setStyleName(Resources.I.style().commentWidgets());
     comments.setVisible(false);
     initWidget(new SimplePanel(comments));
-
-    padding = DOM.createDiv();
-    padding.setClassName(DiffTable.style.padding());
-    ChunkManager.focusOnClick(padding, cm.side());
-    getElement().appendChild(padding);
   }
 
   CommentManager getCommentManager() {
@@ -73,14 +60,14 @@
     return cm;
   }
 
-  CommentGroup getPeer() {
-    return peer;
-  }
-
   int getLine() {
     return line;
   }
 
+  DisplaySide getSide() {
+    return side;
+  }
+
   void add(PublishedBox box) {
     comments.add(box);
     comments.setVisible(true);
@@ -138,33 +125,19 @@
   void remove(DraftBox box) {
     comments.remove(box);
     comments.setVisible(0 < getBoxCount());
-
-    if (0 < getBoxCount() || 0 < peer.getBoxCount()) {
-      resize();
-    } else {
-      detach();
-      peer.detach();
-    }
   }
 
-  private void detach() {
+  void detach() {
     if (lineWidget != null) {
       lineWidget.clear();
       lineWidget = null;
       updateSelection();
     }
-    manager.clearLine(cm.side(), line, this);
+    manager.clearLine(side, line, this);
     removeFromParent();
   }
 
-  void attachPair(DiffTable parent) {
-    if (lineWidget == null && peer.lineWidget == null) {
-      this.attach(parent);
-      peer.attach(parent);
-    }
-  }
-
-  private void attach(DiffTable parent) {
+  void attach(DiffTable parent) {
     parent.add(this);
     lineWidget = cm.addLineWidget(Math.max(0, line - 1), getElement(),
         Configuration.create()
@@ -174,33 +147,6 @@
           .set("insertAt", 0));
   }
 
-  void handleRedraw() {
-    lineWidget.onRedraw(new Runnable() {
-      @Override
-      public void run() {
-        if (canComputeHeight() && peer.canComputeHeight()) {
-          if (resizeTimer != null) {
-            resizeTimer.cancel();
-            resizeTimer = null;
-          }
-          adjustPadding(CommentGroup.this, peer);
-        } else if (resizeTimer == null) {
-          resizeTimer = new Timer() {
-            @Override
-            public void run() {
-              if (canComputeHeight() && peer.canComputeHeight()) {
-                cancel();
-                resizeTimer = null;
-                adjustPadding(CommentGroup.this, peer);
-              }
-            }
-          };
-          resizeTimer.scheduleRepeating(5);
-        }
-      }
-    });
-  }
-
   @Override
   protected void onUnload() {
     super.onUnload();
@@ -209,13 +155,7 @@
     }
   }
 
-  void resize() {
-    if (lineWidget != null) {
-      adjustPadding(this, peer);
-    }
-  }
-
-  private void updateSelection() {
+  void updateSelection() {
     if (cm.somethingSelected()) {
       FromTo r = cm.getSelectedRange();
       if (r.to().line() >= line) {
@@ -224,27 +164,37 @@
     }
   }
 
-  private boolean canComputeHeight() {
+  boolean canComputeHeight() {
     return !comments.isVisible() || comments.getOffsetHeight() > 0;
   }
 
-  private int computeHeight() {
-    if (comments.isVisible()) {
-      // Include margin-bottom: 5px from CSS class.
-      return comments.getOffsetHeight() + 5;
-    }
-    return 0;
+  LineWidget getLineWidget() {
+    return lineWidget;
   }
 
-  private static void adjustPadding(CommentGroup a, CommentGroup b) {
-    int apx = a.computeHeight();
-    int bpx = b.computeHeight();
-    int h = Math.max(apx, bpx);
-    a.padding.getStyle().setHeight(Math.max(0, h - apx), Unit.PX);
-    b.padding.getStyle().setHeight(Math.max(0, h - bpx), Unit.PX);
-    a.lineWidget.changed();
-    b.lineWidget.changed();
-    a.updateSelection();
-    b.updateSelection();
+  void setLineWidget(LineWidget widget) {
+    lineWidget = widget;
   }
+
+  Timer getResizeTimer() {
+    return resizeTimer;
+  }
+
+  void setResizeTimer(Timer timer) {
+    resizeTimer = timer;
+  }
+
+  FlowPanel getComments() {
+    return comments;
+  }
+
+  CommentManager getManager() {
+    return manager;
+  }
+
+  abstract void init(DiffTable parent);
+
+  abstract void handleRedraw();
+
+  abstract void resize();
 }
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 4e1a3e1..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
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// 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.
@@ -25,11 +25,11 @@
 import com.google.gwt.core.client.JsArray;
 
 import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.LineHandle;
 import net.codemirror.lib.Pos;
 import net.codemirror.lib.TextMarker.FromTo;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -38,24 +38,25 @@
 import java.util.SortedMap;
 import java.util.TreeMap;
 
-/** Tracks comment widgets for {@link SideBySide}. */
-class CommentManager {
-  private final SideBySide host;
+/** Tracks comment widgets for {@link DiffScreen}. */
+abstract class CommentManager {
   private final PatchSet.Id base;
   private final PatchSet.Id revision;
   private final String path;
   private final CommentLinkProcessor commentLinkProcessor;
-
+  final SortedMap<Integer, CommentGroup> sideA;
+  final SortedMap<Integer, CommentGroup> sideB;
   private final Map<String, PublishedBox> published;
-  private final SortedMap<Integer, CommentGroup> sideA;
-  private final SortedMap<Integer, CommentGroup> sideB;
   private final Set<DraftBox> unsavedDrafts;
+  final DiffScreen host;
   private boolean attached;
   private boolean expandAll;
   private boolean open;
 
-  CommentManager(SideBySide host,
-      PatchSet.Id base, PatchSet.Id revision,
+  CommentManager(
+      DiffScreen host,
+      PatchSet.Id base,
+      PatchSet.Id revision,
       String path,
       CommentLinkProcessor clp,
       boolean open) {
@@ -67,17 +68,194 @@
     this.open = open;
 
     published = new HashMap<>();
+    unsavedDrafts = new HashSet<>();
     sideA = new TreeMap<>();
     sideB = new TreeMap<>();
-    unsavedDrafts = new HashSet<>();
   }
 
-  SideBySide getSideBySide() {
-    return host;
+  void setAttached(boolean attached) {
+    this.attached = attached;
+  }
+
+  boolean isAttached() {
+    return attached;
+  }
+
+  void setExpandAll(boolean expandAll) {
+    this.expandAll = expandAll;
+  }
+
+  boolean isExpandAll() {
+    return expandAll;
+  }
+
+  boolean isOpen() {
+    return open;
+  }
+
+  String getPath() {
+    return path;
+  }
+
+  Map<String, PublishedBox> getPublished() {
+    return published;
+  }
+
+  CommentLinkProcessor getCommentLinkProcessor() {
+    return commentLinkProcessor;
+  }
+
+  void renderDrafts(DisplaySide forSide, JsArray<CommentInfo> in) {
+    for (CommentInfo info : Natives.asList(in)) {
+      DisplaySide side = displaySide(info, forSide);
+      if (side != null) {
+        addDraftBox(side, info);
+      }
+    }
+  }
+
+  void setUnsaved(DraftBox box, boolean isUnsaved) {
+    if (isUnsaved) {
+      unsavedDrafts.add(box);
+    } else {
+      unsavedDrafts.remove(box);
+    }
+  }
+
+  void saveAllDrafts(CallbackGroup cb) {
+    for (DraftBox box : unsavedDrafts) {
+      box.save(cb);
+    }
+  }
+
+  Side getStoredSideFromDisplaySide(DisplaySide side) {
+    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) {
+    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 || base.get() < 0) ? DisplaySide.A : null;
+    }
+    return forSide;
+  }
+
+  static FromTo adjustSelection(CodeMirror cm) {
+    FromTo fromTo = cm.getSelectedRange();
+    Pos to = fromTo.to();
+    if (to.ch() == 0) {
+      to.line(to.line() - 1);
+      to.ch(cm.getLine(to.line()).length());
+    }
+    return fromTo;
+  }
+
+  abstract CommentGroup group(DisplaySide side, int cmLinePlusOne);
+
+  /**
+   * Create a new {@link DraftBox} at the specified line and focus it.
+   *
+   * @param side which side the draft will appear on.
+   * @param line the line the draft will be at. Lines are 1-based. Line 0 is a
+   *        special case creating a file level comment.
+   */
+  void insertNewDraft(DisplaySide side, int line) {
+    if (line == 0) {
+      host.skipManager.ensureFirstLineIsVisible();
+    }
+
+    CommentGroup group = group(side, line);
+    if (0 < group.getBoxCount()) {
+      CommentBox last = group.getCommentBox(group.getBoxCount() - 1);
+      if (last instanceof DraftBox) {
+        ((DraftBox)last).setEdit(true);
+      } else {
+        ((PublishedBox)last).doReply();
+      }
+    } else {
+      addDraftBox(side, CommentInfo.create(
+          getPath(),
+          getStoredSideFromDisplaySide(side),
+          getParentNumFromDisplaySide(side),
+          line,
+          null)).setEdit(true);
+    }
+  }
+
+  abstract String getTokenSuffixForActiveLine(CodeMirror cm);
+
+  Runnable signInCallback(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        String token = host.getToken();
+        if (cm.extras().hasActiveLine()) {
+          token += "@" + getTokenSuffixForActiveLine(cm);
+        }
+        Gerrit.doSignIn(token);
+      }
+    };
+  }
+
+  abstract void newDraft(CodeMirror cm);
+
+  Runnable newDraftCallback(final CodeMirror cm) {
+    if (!Gerrit.isSignedIn()) {
+      return signInCallback(cm);
+    }
+
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (cm.extras().hasActiveLine()) {
+          newDraft(cm);
+        }
+      }
+    };
+  }
+
+  DraftBox addDraftBox(DisplaySide side, CommentInfo info) {
+    int cmLinePlusOne = host.getCmLine(info.line() - 1, side) + 1;
+    CommentGroup group = group(side, cmLinePlusOne);
+    DraftBox box = new DraftBox(
+        group,
+        getCommentLinkProcessor(),
+        getPatchSetIdFromSide(side),
+        info,
+        isExpandAll());
+
+    if (info.inReplyTo() != null) {
+      PublishedBox r = getPublished().get(info.inReplyTo());
+      if (r != null) {
+        r.setReplyBox(box);
+      }
+    }
+
+    group.add(box);
+    box.setAnnotation(host.getDiffTable().scrollbar.draft(
+        host.getCmFromSide(side),
+        Math.max(0, cmLinePlusOne - 1)));
+    return box;
   }
 
   void setExpandAllComments(boolean b) {
-    expandAll = b;
+    setExpandAll(b);
     for (CommentGroup g : sideA.values()) {
       g.setOpenAll(b);
     }
@@ -86,6 +264,8 @@
     }
   }
 
+  abstract SortedMap<Integer, CommentGroup> getMapForNav(DisplaySide side);
+
   Runnable commentNav(final CodeMirror src, final Direction dir) {
     return new Runnable() {
       @Override
@@ -93,27 +273,38 @@
         // Every comment appears in both side maps as a linked pair.
         // It is only necessary to search one side to find a comment
         // on either side of the editor pair.
-        SortedMap<Integer, CommentGroup> map = map(src.side());
+        SortedMap<Integer, CommentGroup> map = getMapForNav(src.side());
         int line = src.extras().hasActiveLine()
             ? src.getLineNumber(src.extras().activeLine()) + 1
             : 0;
+
+        CommentGroup g;
         if (dir == Direction.NEXT) {
           map = map.tailMap(line + 1);
           if (map.isEmpty()) {
             return;
           }
-          line = map.firstKey();
+          g = map.get(map.firstKey());
+          while (g.getBoxCount() == 0) {
+            map = map.tailMap(map.firstKey() + 1);
+            if (map.isEmpty()) {
+              return;
+            }
+            g = map.get(map.firstKey());
+          }
         } else {
           map = map.headMap(line);
           if (map.isEmpty()) {
             return;
           }
-          line = map.lastKey();
-        }
-
-        CommentGroup g = map.get(line);
-        if (g.getBoxCount() == 0) {
-          g = g.getPeer();
+          g = map.get(map.lastKey());
+          while (g.getBoxCount() == 0) {
+            map = map.headMap(map.lastKey());
+            if (map.isEmpty()) {
+              return;
+            }
+            g = map.get(map.lastKey());
+          }
         }
 
         CodeMirror cm = g.getCm();
@@ -125,6 +316,13 @@
     };
   }
 
+  void clearLine(DisplaySide side, int line, CommentGroup group) {
+    SortedMap<Integer, CommentGroup> map = map(side);
+    if (map.get(line) == group) {
+      map.remove(line);
+    }
+  }
+
   void render(CommentsCollections in, boolean expandAll) {
     if (in.publishedBase != null) {
       renderPublished(DisplaySide.A, in.publishedBase);
@@ -142,119 +340,60 @@
       setExpandAllComments(true);
     }
     for (CommentGroup g : sideA.values()) {
-      g.attachPair(host.diffTable);
+      g.init(host.getDiffTable());
     }
     for (CommentGroup g : sideB.values()) {
-      g.attachPair(host.diffTable);
+      g.init(host.getDiffTable());
       g.handleRedraw();
     }
-    attached = true;
+    setAttached(true);
   }
 
-  private void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in) {
+  void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in) {
     for (CommentInfo info : Natives.asList(in)) {
       DisplaySide side = displaySide(info, forSide);
       if (side != null) {
-        CommentGroup group = group(side, info.line());
+        int cmLinePlusOne = host.getCmLine(info.line() - 1, side) + 1;
+        CommentGroup group = group(side, cmLinePlusOne);
         PublishedBox box = new PublishedBox(
             group,
-            commentLinkProcessor,
+            getCommentLinkProcessor(),
             getPatchSetIdFromSide(side),
             info,
-            open);
+            side,
+            isOpen());
         group.add(box);
-        box.setAnnotation(host.diffTable.scrollbar.comment(
+        box.setAnnotation(host.getDiffTable().scrollbar.comment(
             host.getCmFromSide(side),
-            Math.max(0, info.line() - 1)));
-        published.put(info.id(), box);
+            cmLinePlusOne - 1));
+        getPublished().put(info.id(), box);
       }
     }
   }
 
-  private void renderDrafts(DisplaySide forSide, JsArray<CommentInfo> in) {
-    for (CommentInfo info : Natives.asList(in)) {
-      DisplaySide side = displaySide(info, forSide);
-      if (side != null) {
-        addDraftBox(side, info);
-      }
+  abstract Collection<Integer> getLinesWithCommentGroups();
+
+  private static void checkAndAddSkip(List<SkippedLine> out, SkippedLine s) {
+    if (s.getSize() > 1) {
+      out.add(s);
     }
   }
 
-  /**
-   * Create a new {@link DraftBox} at the specified line and focus it.
-   *
-   * @param side which side the draft will appear on.
-   * @param line the line the draft will be at. Lines are 1-based. Line 0 is a
-   *        special case creating a file level comment.
-   */
-  void insertNewDraft(DisplaySide side, int line) {
-    if (line == 0) {
-      host.getSkipManager().ensureFirstLineIsVisible();
-    }
-
-    CommentGroup group = group(side, line);
-    if (0 < group.getBoxCount()) {
-      CommentBox last = group.getCommentBox(group.getBoxCount() - 1);
-      if (last instanceof DraftBox) {
-        ((DraftBox)last).setEdit(true);
-      } else {
-        ((PublishedBox)last).doReply();
-      }
-    } else {
-      addDraftBox(side, CommentInfo.create(
-          path,
-          getStoredSideFromDisplaySide(side),
-          line,
-          null)).setEdit(true);
-    }
-  }
-
-  DraftBox addDraftBox(DisplaySide side, CommentInfo info) {
-    CommentGroup group = group(side, info.line());
-    DraftBox box = new DraftBox(
-        group,
-        commentLinkProcessor,
-        getPatchSetIdFromSide(side),
-        info,
-        expandAll);
-
-    if (info.inReplyTo() != null) {
-      PublishedBox r = published.get(info.inReplyTo());
-      if (r != null) {
-        r.setReplyBox(box);
-      }
-    }
-
-    group.add(box);
-    box.setAnnotation(host.diffTable.scrollbar.draft(
-        host.getCmFromSide(side),
-        Math.max(0, info.line() - 1)));
-    return box;
-  }
-
-  private DisplaySide displaySide(CommentInfo info, DisplaySide forSide) {
-    if (info.side() == Side.PARENT) {
-      return base == null ? DisplaySide.A : null;
-    }
-    return forSide;
-  }
-
   List<SkippedLine> splitSkips(int context, List<SkippedLine> skips) {
-    if (sideB.containsKey(0)) {
+    if (sideA.containsKey(0) || sideB.containsKey(0)) {
       // Special case of file comment; cannot skip first line.
       for (SkippedLine skip : skips) {
-        if (skip.getStartB() == 0) {
+        if (skip.getStartA() == 0) {
           skip.incrementStart(1);
+          break;
         }
       }
     }
 
-    // TODO: This is not optimal, but shouldn't be too costly in most cases.
-    // Maybe rewrite after done keeping track of diff chunk positions.
-    for (int boxLine : sideB.tailMap(1).keySet()) {
+    for (int boxLine : getLinesWithCommentGroups()) {
       List<SkippedLine> temp = new ArrayList<>(skips.size() + 2);
       for (SkippedLine skip : skips) {
-        int startLine = skip.getStartB();
+        int startLine = host.getCmLine(skip.getStartB(), DisplaySide.B);
         int deltaBefore = boxLine - startLine;
         int deltaAfter = startLine + skip.getSize() - boxLine;
         if (deltaBefore < -context || deltaAfter < -context) {
@@ -282,29 +421,18 @@
     return skips;
   }
 
-  private static void checkAndAddSkip(List<SkippedLine> out, SkippedLine s) {
-    if (s.getSize() > 1) {
-      out.add(s);
-    }
-  }
+  abstract void newDraftOnGutterClick(CodeMirror cm, String gutterClass,
+      int line);
 
-  void clearLine(DisplaySide side, int line, CommentGroup group) {
-    SortedMap<Integer, CommentGroup> map = map(side);
-    if (map.get(line) == group) {
-      map.remove(line);
-    }
-  }
+  abstract CommentGroup getCommentGroupOnActiveLine(CodeMirror cm);
 
   Runnable toggleOpenBox(final CodeMirror cm) {
     return new Runnable() {
       @Override
       public void run() {
-        if (cm.extras().hasActiveLine()) {
-          CommentGroup w = map(cm.side()).get(
-              cm.getLineNumber(cm.extras().activeLine()) + 1);
-          if (w != null) {
-            w.openCloseLast();
-          }
+        CommentGroup group = getCommentGroupOnActiveLine(cm);
+        if (group != null) {
+          group.openCloseLast();
         }
       }
     };
@@ -314,123 +442,15 @@
     return new Runnable() {
       @Override
       public void run() {
-        if (cm.extras().hasActiveLine()) {
-          CommentGroup w = map(cm.side()).get(
-              cm.getLineNumber(cm.extras().activeLine()) + 1);
-          if (w != null) {
-            w.openCloseAll();
-          }
+        CommentGroup group = getCommentGroupOnActiveLine(cm);
+        if (group != null) {
+          group.openCloseAll();
         }
       }
     };
   }
 
-  Runnable insertNewDraft(final CodeMirror cm) {
-    if (!Gerrit.isSignedIn()) {
-      return new Runnable() {
-        @Override
-        public void run() {
-          String token = host.getToken();
-          if (cm.extras().hasActiveLine()) {
-            LineHandle handle = cm.extras().activeLine();
-            int line = cm.getLineNumber(handle) + 1;
-            token += "@" + (cm.side() == DisplaySide.A ? "a" : "") + line;
-          }
-          Gerrit.doSignIn(token);
-        }
-     };
-    }
-
-    return new Runnable() {
-      @Override
-      public void run() {
-        if (cm.extras().hasActiveLine()) {
-          newDraft(cm, cm.getLineNumber(cm.extras().activeLine()) + 1);
-        }
-      }
-    };
-  }
-
-  void newDraft(CodeMirror cm, int line) {
-    if (cm.somethingSelected()) {
-      FromTo fromTo = cm.getSelectedRange();
-      Pos end = fromTo.to();
-      if (end.ch() == 0) {
-        end.line(end.line() - 1);
-        end.ch(cm.getLine(end.line()).length());
-      }
-
-      addDraftBox(cm.side(), CommentInfo.create(
-              path,
-              getStoredSideFromDisplaySide(cm.side()),
-              line,
-              CommentRange.create(fromTo))).setEdit(true);
-      cm.setSelection(cm.getCursor());
-    } else {
-      insertNewDraft(cm.side(), line);
-    }
-  }
-
-  void setUnsaved(DraftBox box, boolean isUnsaved) {
-    if (isUnsaved) {
-      unsavedDrafts.add(box);
-    } else {
-      unsavedDrafts.remove(box);
-    }
-  }
-
-  void saveAllDrafts(CallbackGroup cb) {
-    for (DraftBox box : unsavedDrafts) {
-      box.save(cb);
-    }
-  }
-
-  private CommentGroup group(DisplaySide side, int line) {
-    CommentGroup w = map(side).get(line);
-    if (w != null) {
-      return w;
-    }
-
-    int lineA;
-    int lineB;
-    if (line == 0) {
-      lineA = lineB = 0;
-    } else if (side == DisplaySide.A) {
-      lineA = line;
-      lineB = host.lineOnOther(side, line - 1).getLine() + 1;
-    } else {
-      lineA = host.lineOnOther(side, line - 1).getLine() + 1;
-      lineB = line;
-    }
-
-    CommentGroup a = newGroup(DisplaySide.A, lineA);
-    CommentGroup b = newGroup(DisplaySide.B, lineB);
-    CommentGroup.pair(a, b);
-
-    sideA.put(lineA, a);
-    sideB.put(lineB, b);
-
-    if (attached) {
-      a.attachPair(host.diffTable);
-      b.handleRedraw();
-    }
-
-    return side == DisplaySide.A ? a : b;
-  }
-
-  private CommentGroup newGroup(DisplaySide side, int line) {
-    return new CommentGroup(this, host.getCmFromSide(side), line);
-  }
-
-  private SortedMap<Integer, CommentGroup> map(DisplaySide side) {
+  SortedMap<Integer, CommentGroup> map(DisplaySide side) {
     return side == DisplaySide.A ? sideA : sideB;
   }
-
-  private Side getStoredSideFromDisplaySide(DisplaySide side) {
-    return side == DisplaySide.A && base == null ? Side.PARENT : Side.REVISION;
-  }
-
-  private PatchSet.Id getPatchSetIdFromSide(DisplaySide side) {
-    return side == DisplaySide.A && base != null ? base : revision;
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
index 9f46e9b..d3c150d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
@@ -43,7 +43,7 @@
   public final native int endLine() /*-{ return this.end_line; }-*/;
   public final native int endCharacter() /*-{ return this.end_character; }-*/;
 
-  private final native void set(int sl, int sc, int el, int ec) /*-{
+  private native void set(int sl, int sc, int el, int ec) /*-{
     this.start_line = sl;
     this.start_character = sc;
     this.end_line = el;
@@ -52,4 +52,4 @@
 
   protected CommentRange() {
   }
-}
\ No newline at end of file
+}
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 b23a8cf..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;
@@ -29,34 +30,55 @@
 
 /** Collection of published and draft comments loaded from the server. */
 class CommentsCollections {
-  private String path;
-
+  private final String path;
+  private final PatchSet.Id base;
+  private final PatchSet.Id revision;
+  private NativeMap<JsArray<CommentInfo>> publishedBaseAll;
+  private NativeMap<JsArray<CommentInfo>> publishedRevisionAll;
   JsArray<CommentInfo> publishedBase;
   JsArray<CommentInfo> publishedRevision;
   JsArray<CommentInfo> draftsBase;
   JsArray<CommentInfo> draftsRevision;
 
-  void load(PatchSet.Id base, PatchSet.Id revision, String path,
-      CallbackGroup group) {
+  CommentsCollections(PatchSet.Id base, PatchSet.Id revision, String path) {
     this.path = path;
+    this.base = base;
+    this.revision = revision;
+  }
 
-    if (base != null) {
+  void load(CallbackGroup group) {
+    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()));
     }
   }
 
+  boolean hasCommentForPath(String filePath) {
+    if (base != null && base.get() > 0) {
+      JsArray<CommentInfo> forBase = publishedBaseAll.get(filePath);
+      if (forBase != null && forBase.length() > 0) {
+        return true;
+      }
+    }
+    JsArray<CommentInfo> forRevision = publishedRevisionAll.get(filePath);
+    if (forRevision != null && forRevision.length() > 0) {
+      return true;
+    }
+    return false;
+  }
+
   private AsyncCallback<NativeMap<JsArray<CommentInfo>>> publishedBase() {
     return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
       @Override
       public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+        publishedBaseAll = result;
         publishedBase = sort(result.get(path));
       }
 
@@ -70,6 +92,10 @@
     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));
       }
 
@@ -79,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
@@ -96,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 6d795d3..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
@@ -14,7 +14,10 @@
 
 package com.google.gerrit.client.diff;
 
+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;
@@ -23,15 +26,15 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 public class DiffApi {
-  public enum IgnoreWhitespace {
-    NONE, TRAILING, CHANGED, ALL
-  }
-
-  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));
   }
@@ -40,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));
   }
@@ -59,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;
   }
@@ -70,22 +81,8 @@
   }
 
   public DiffApi ignoreWhitespace(DiffPreferencesInfo.Whitespace w) {
-    switch (w) {
-      default:
-      case IGNORE_NONE:
-        return ignoreWhitespace(IgnoreWhitespace.NONE);
-      case IGNORE_TRAILING:
-        return ignoreWhitespace(IgnoreWhitespace.TRAILING);
-      case IGNORE_LEADING_AND_TRAILING:
-        return ignoreWhitespace(IgnoreWhitespace.CHANGED);
-      case IGNORE_ALL:
-        return ignoreWhitespace(IgnoreWhitespace.ALL);
-    }
-  }
-
-  public DiffApi ignoreWhitespace(IgnoreWhitespace w) {
-    if (w != null && w != IgnoreWhitespace.NONE) {
-      call.addParameter("ignore-whitespace", w);
+    if (w != null && w != IGNORE_ALL) {
+      call.addParameter("whitespace", w);
     }
     return this;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
index 1e5c5e5..3b1b346 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
@@ -43,4 +43,4 @@
   boolean isEdit() {
     return edit;
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
index 923a995..b7910f5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.client.DiffWebLinkInfo;
 import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
@@ -27,9 +27,6 @@
 import java.util.List;
 
 public class DiffInfo extends JavaScriptObject {
-  public static final String GITLINK = "x-git/gitlink";
-  public static final String SYMLINK = "x-git/symlink";
-
   public final native FileMeta metaA() /*-{ return this.meta_a; }-*/;
   public final native FileMeta metaB() /*-{ return this.meta_b; }-*/;
   public final native JsArrayString diffHeader() /*-{ return this.diff_header; }-*/;
@@ -45,7 +42,7 @@
     return filterWebLinks(DiffView.UNIFIED_DIFF);
   }
 
-  private final List<WebLinkInfo> filterWebLinks(DiffView diffView) {
+  private List<WebLinkInfo> filterWebLinks(DiffView diffView) {
     List<WebLinkInfo> filteredDiffWebLinks = new LinkedList<>();
     List<DiffWebLinkInfo> allDiffWebLinks = Natives.asList(webLinks());
     if (allDiffWebLinks != null) {
@@ -66,7 +63,7 @@
   public final ChangeType changeType() {
     return ChangeType.valueOf(changeTypeRaw());
   }
-  private final native String changeTypeRaw()
+  private native String changeTypeRaw()
   /*-{ return this.change_type }-*/;
 
   public final IntraLineStatus intralineStatus() {
@@ -75,7 +72,7 @@
         ? IntraLineStatus.valueOf(s)
         : IntraLineStatus.OFF;
   }
-  private final native String intralineStatusRaw()
+  private native String intralineStatusRaw()
   /*-{ return this.intraline_status }-*/;
 
   public final boolean hasSkip() {
@@ -118,6 +115,26 @@
     return s.toString();
   }
 
+  public final String textUnified() {
+    StringBuilder s = new StringBuilder();
+    JsArray<Region> c = content();
+    for (int i = 0; i < c.length(); i++) {
+      Region r = c.get(i);
+      if (r.ab() != null) {
+        append(s, r.ab());
+      } else {
+        if (r.a() != null) {
+          append(s, r.a());
+        }
+        if (r.b() != null) {
+          append(s, r.b());
+        }
+      }
+      // TODO skip may need to be handled
+    }
+    return s.toString();
+  }
+
   private static void append(StringBuilder s, JsArrayString lines) {
     for (int i = 0; i < lines.length(); i++) {
       s.append(lines.get(i)).append('\n');
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
new file mode 100644
index 0000000..de19b35
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
@@ -0,0 +1,1013 @@
+// Copyright (C) 2013 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 impl ied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import static com.google.gerrit.extensions.client.DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
+import static java.lang.Double.POSITIVE_INFINITY;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.DiffPreferences;
+import com.google.gerrit.client.change.ChangeScreen;
+import com.google.gerrit.client.change.FileTable;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeList;
+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;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.projects.ConfigInfoCache;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtexpui.globalkey.client.ShowHelpCommand;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.CodeMirror.BeforeSelectionChangeHandler;
+import net.codemirror.lib.CodeMirror.GutterClickHandler;
+import net.codemirror.lib.CodeMirror.LineHandle;
+import net.codemirror.lib.KeyMap;
+import net.codemirror.lib.Pos;
+import net.codemirror.mode.ModeInfo;
+import net.codemirror.mode.ModeInjector;
+import net.codemirror.theme.ThemeLoader;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+/** Base class for SideBySide and Unified */
+abstract class DiffScreen extends Screen {
+  private static final KeyMap RENDER_ENTIRE_FILE_KEYMAP = KeyMap.create()
+      .propagate("Ctrl-F").propagate("Ctrl-G").propagate("Shift-Ctrl-G");
+
+  enum FileSize {
+    SMALL(0),
+    LARGE(500),
+    HUGE(4000);
+
+    final int lines;
+
+    FileSize(int n) {
+      this.lines = n;
+    }
+  }
+
+  private final Change.Id changeId;
+  final PatchSet.Id base;
+  final PatchSet.Id revision;
+  final String path;
+  final DiffPreferences prefs;
+  final SkipManager skipManager;
+
+  private DisplaySide startSide;
+  private int startLine;
+  private Change.Status changeStatus;
+
+  private HandlerRegistration resizeHandler;
+  private DiffInfo diff;
+  private FileSize fileSize;
+  private EditInfo edit;
+
+  private KeyCommandSet keysNavigation;
+  private KeyCommandSet keysAction;
+  private KeyCommandSet keysComment;
+  private List<HandlerRegistration> handlers;
+  private PreferencesAction prefsAction;
+  private int reloadVersionId;
+  private int parents;
+
+  @UiField(provided = true)
+  Header header;
+
+  DiffScreen(
+      PatchSet.Id base,
+      PatchSet.Id revision,
+      String path,
+      DisplaySide startSide,
+      int startLine,
+      DiffView diffScreenType) {
+    this.base = base;
+    this.revision = revision;
+    this.changeId = revision.getParentKey();
+    this.path = path;
+    this.startSide = startSide;
+    this.startLine = startLine;
+
+    prefs = DiffPreferences.create(Gerrit.getDiffPreferences());
+    handlers = new ArrayList<>(6);
+    keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
+    header = new Header(
+        keysNavigation, base, revision, path, diffScreenType, prefs);
+    skipManager = new SkipManager(this);
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    setHeaderVisible(false);
+    setWindowTitle(FileInfo.getFileName(path));
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+
+    CallbackGroup group1 = new CallbackGroup();
+    final CallbackGroup group2 = new CallbackGroup();
+
+    CodeMirror.initLibrary(group1.add(new AsyncCallback<Void>() {
+      final AsyncCallback<Void> themeCallback = group2.addEmpty();
+
+      @Override
+      public void onSuccess(Void result) {
+        // Load theme after CM library to ensure theme can override CSS.
+        ThemeLoader.loadTheme(prefs.theme(), themeCallback);
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+      }
+    }));
+
+    DiffApi.diff(revision, path)
+      .base(base)
+      .wholeFile()
+      .intraline(prefs.intralineDifference())
+      .ignoreWhitespace(prefs.ignoreWhitespace())
+      .get(group1.addFinal(new GerritCallback<DiffInfo>() {
+        final AsyncCallback<Void> modeInjectorCb = group2.addEmpty();
+
+        @Override
+        public void onSuccess(DiffInfo diffInfo) {
+          diff = diffInfo;
+          fileSize = bucketFileSize(diffInfo);
+
+          if (prefs.syntaxHighlighting()) {
+            if (fileSize.compareTo(FileSize.SMALL) > 0) {
+              modeInjectorCb.onSuccess(null);
+            } else {
+              injectMode(diffInfo, modeInjectorCb);
+            }
+          } else {
+            modeInjectorCb.onSuccess(null);
+          }
+        }
+      }));
+
+    if (Gerrit.isSignedIn()) {
+      ChangeApi.edit(changeId.get(), group2.add(
+          new AsyncCallback<EditInfo>() {
+            @Override
+            public void onSuccess(EditInfo result) {
+              edit = result;
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+          }));
+    }
+
+    final CommentsCollections comments =
+        new CommentsCollections(base, revision, path);
+    comments.load(group2);
+
+    countParents(group2);
+
+    RestApi call = ChangeApi.detail(changeId.get());
+    ChangeList.addOptions(call, EnumSet.of(
+        ListChangesOption.ALL_REVISIONS));
+    call.get(group2.add(new AsyncCallback<ChangeInfo>() {
+      @Override
+      public void onSuccess(ChangeInfo info) {
+        changeStatus = info.status();
+        info.revisions().copyKeysIntoChildren("name");
+        if (edit != null) {
+          edit.setName(edit.commit().commit());
+          info.setEdit(edit);
+          info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
+        }
+        String currentRevision = info.currentRevision();
+        boolean current = currentRevision != null &&
+            revision.get() == info.revision(currentRevision)._number();
+        JsArray<RevisionInfo> list = info.revisions().values();
+        RevisionInfo.sortRevisionInfoByNumber(list);
+        getDiffTable().set(prefs, list, parents, diff, edit != null, current,
+            changeStatus.isOpen(), diff.binary());
+        header.setChangeInfo(info);
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+      }
+    }));
+
+    ConfigInfoCache.get(changeId, group2.addFinal(
+        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();
+
+    Window.enableScrolling(false);
+    if (prefs.hideTopMenu()) {
+      Gerrit.setHeaderVisible(false);
+    }
+    resizeHandler = Window.addResizeHandler(new ResizeHandler() {
+      @Override
+      public void onResize(ResizeEvent event) {
+        resizeCodeMirror();
+      }
+    });
+  }
+
+  KeyCommandSet getKeysNavigation() {
+    return keysNavigation;
+  }
+
+  KeyCommandSet getKeysAction() {
+    return keysAction;
+  }
+
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+
+    removeKeyHandlerRegistrations();
+    if (getCommentManager() != null) {
+      CallbackGroup group = new CallbackGroup();
+      getCommentManager().saveAllDrafts(group);
+      group.done();
+    }
+    if (resizeHandler != null) {
+      resizeHandler.removeHandler();
+      resizeHandler = null;
+    }
+    for (CodeMirror cm : getCms()) {
+      if (cm != null) {
+        cm.getWrapperElement().removeFromParent();
+      }
+    }
+    if (prefsAction != null) {
+      prefsAction.hide();
+    }
+
+    Window.enableScrolling(true);
+    Gerrit.setHeaderVisible(true);
+  }
+
+  private void removeKeyHandlerRegistrations() {
+    for (HandlerRegistration h : handlers) {
+      h.removeHandler();
+    }
+    handlers.clear();
+  }
+
+  void registerCmEvents(final CodeMirror cm) {
+    cm.on("cursorActivity", updateActiveLine(cm));
+    cm.on("focus", updateActiveLine(cm));
+    KeyMap keyMap = KeyMap.create()
+        .on("A", upToChange(true))
+        .on("U", upToChange(false))
+        .on("'['", header.navigate(Direction.PREV))
+        .on("']'", header.navigate(Direction.NEXT))
+        .on("R", header.toggleReviewed())
+        .on("O", getCommentManager().toggleOpenBox(cm))
+        .on("N", maybeNextVimSearch(cm))
+        .on("Ctrl-Alt-E", openEditScreen(cm))
+        .on("P", getChunkManager().diffChunkNav(cm, Direction.PREV))
+        .on("Shift-M", header.reviewedAndNext())
+        .on("Shift-N", maybePrevVimSearch(cm))
+        .on("Shift-P", getCommentManager().commentNav(cm, Direction.PREV))
+        .on("Shift-O", getCommentManager().openCloseAll(cm))
+        .on("I", new Runnable() {
+          @Override
+          public void run() {
+            switch (getIntraLineStatus()) {
+              case OFF:
+              case OK:
+                toggleShowIntraline();
+                break;
+              case FAILURE:
+              case TIMEOUT:
+              default:
+                break;
+            }
+          }
+        })
+        .on("','", new Runnable() {
+          @Override
+          public void run() {
+            prefsAction.show();
+          }
+        })
+        .on("Shift-/", new Runnable() {
+          @Override
+          public void run() {
+            new ShowHelpCommand().onKeyPress(null);
+          }
+        })
+        .on("Space", new Runnable() {
+          @Override
+          public void run() {
+            cm.vim().handleKey("<C-d>");
+          }
+        })
+        .on("Shift-Space", new Runnable() {
+          @Override
+          public void run() {
+            cm.vim().handleKey("<C-u>");
+          }
+        })
+        .on("Ctrl-F", new Runnable() {
+          @Override
+          public void run() {
+            cm.execCommand("find");
+          }
+        })
+        .on("Ctrl-G", new Runnable() {
+          @Override
+          public void run() {
+            cm.execCommand("findNext");
+          }
+        })
+        .on("Enter", maybeNextCmSearch(cm))
+        .on("Shift-Ctrl-G", new Runnable() {
+          @Override
+          public void run() {
+            cm.execCommand("findPrev");
+          }
+        })
+        .on("Shift-Enter", new Runnable() {
+          @Override
+          public void run() {
+            cm.execCommand("findPrev");
+          }
+        })
+        .on("Esc", new Runnable() {
+          @Override
+          public void run() {
+            cm.setCursor(cm.getCursor());
+            cm.execCommand("clearSearch");
+            cm.vim().handleEx("nohlsearch");
+          }
+        })
+        .on("Ctrl-A", new Runnable() {
+          @Override
+          public void run() {
+            cm.execCommand("selectAll");
+          }
+        })
+        .on("G O", new Runnable() {
+          @Override
+          public void run() {
+            Gerrit.display(PageLinks.toChangeQuery("status:open"));
+          }
+        })
+        .on("G M", new Runnable() {
+          @Override
+          public void run() {
+            Gerrit.display(PageLinks.toChangeQuery("status:merged"));
+          }
+        })
+        .on("G A", new Runnable() {
+          @Override
+          public void run() {
+            Gerrit.display(PageLinks.toChangeQuery("status:abandoned"));
+          }
+        });
+        if (Gerrit.isSignedIn()) {
+          keyMap.on("G I", new Runnable() {
+            @Override
+            public void run() {
+              Gerrit.display(PageLinks.MINE);
+            }
+          })
+          .on("G D", new Runnable() {
+            @Override
+            public void run() {
+              Gerrit.display(PageLinks.toChangeQuery("owner:self is:draft"));
+            }
+          })
+          .on("G C", new Runnable() {
+            @Override
+            public void run() {
+              Gerrit.display(PageLinks.toChangeQuery("has:draft"));
+            }
+          })
+          .on("G W", new Runnable() {
+            @Override
+            public void run() {
+              Gerrit.display(
+                  PageLinks.toChangeQuery("is:watched status:open"));
+            }
+          })
+          .on("G S", new Runnable() {
+            @Override
+            public void run() {
+              Gerrit.display(PageLinks.toChangeQuery("is:starred"));
+            }
+          });
+        }
+
+    if (revision.get() != 0) {
+      cm.on("beforeSelectionChange", onSelectionChange(cm));
+      cm.on("gutterClick", onGutterClick(cm));
+      keyMap.on("C", getCommentManager().newDraftCallback(cm));
+    }
+    CodeMirror.normalizeKeyMap(keyMap); // Needed to for multi-stroke keymaps
+    cm.addKeyMap(keyMap);
+  }
+
+  void maybeRegisterRenderEntireFileKeyMap(CodeMirror cm) {
+    if (renderEntireFile()) {
+      cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
+    }
+  }
+
+  private BeforeSelectionChangeHandler onSelectionChange(final CodeMirror cm) {
+    return new BeforeSelectionChangeHandler() {
+      private InsertCommentBubble bubble;
+
+      @Override
+      public void handle(CodeMirror cm, Pos anchor, Pos head) {
+        if (anchor.equals(head)) {
+          if (bubble != null) {
+            bubble.setVisible(false);
+          }
+          return;
+        } else if (bubble == null) {
+          init(anchor);
+        } else {
+          bubble.setVisible(true);
+        }
+        bubble.position(cm.charCoords(head, "local"));
+      }
+
+      private void init(Pos anchor) {
+        bubble = new InsertCommentBubble(getCommentManager(), cm);
+        add(bubble);
+        cm.addWidget(anchor, bubble.getElement());
+      }
+    };
+  }
+
+  @Override
+  public void registerKeys() {
+    super.registerKeys();
+
+    keysNavigation.add(new UpToChangeCommand(revision, 0, 'u'));
+    keysNavigation.add(
+        new NoOpKeyCommand(0, 'j', PatchUtil.C.lineNext()),
+        new NoOpKeyCommand(0, 'k', PatchUtil.C.linePrev()));
+    keysNavigation.add(
+        new NoOpKeyCommand(0, 'n', PatchUtil.C.chunkNext()),
+        new NoOpKeyCommand(0, 'p', PatchUtil.C.chunkPrev()));
+    keysNavigation.add(
+        new NoOpKeyCommand(KeyCommand.M_SHIFT, 'n', PatchUtil.C.commentNext()),
+        new NoOpKeyCommand(KeyCommand.M_SHIFT, 'p', PatchUtil.C.commentPrev()));
+    keysNavigation.add(
+        new NoOpKeyCommand(KeyCommand.M_CTRL, 'f', Gerrit.C.keySearch()));
+
+    keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
+    keysAction.add(new NoOpKeyCommand(0, KeyCodes.KEY_ENTER,
+        PatchUtil.C.expandComment()));
+    keysAction.add(new NoOpKeyCommand(0, 'o', PatchUtil.C.expandComment()));
+    keysAction.add(new NoOpKeyCommand(
+        KeyCommand.M_SHIFT, 'o', PatchUtil.C.expandAllCommentsOnCurrentLine()));
+    if (Gerrit.isSignedIn()) {
+      keysAction.add(new KeyCommand(0, 'r', PatchUtil.C.toggleReviewed()) {
+        @Override
+        public void onKeyPress(KeyPressEvent event) {
+          header.toggleReviewed().run();
+        }
+      });
+      keysAction.add(new NoOpKeyCommand(KeyCommand.M_CTRL | KeyCommand.M_ALT,
+          'e', Gerrit.C.keyEditor()));
+    }
+    keysAction.add(new KeyCommand(
+        KeyCommand.M_SHIFT, 'm', PatchUtil.C.markAsReviewedAndGoToNext()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        header.reviewedAndNext().run();
+      }
+    });
+    keysAction.add(new KeyCommand(0, 'a', PatchUtil.C.openReply()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        upToChange(true).run();
+      }
+    });
+    keysAction.add(new KeyCommand(0, ',', PatchUtil.C.showPreferences()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        prefsAction.show();
+      }
+    });
+    if (getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF
+        || getIntraLineStatus() == DiffInfo.IntraLineStatus.OK) {
+      keysAction.add(new KeyCommand(0, 'i', PatchUtil.C.toggleIntraline()) {
+        @Override
+        public void onKeyPress(KeyPressEvent event) {
+          toggleShowIntraline();
+        }
+      });
+    }
+
+    if (Gerrit.isSignedIn()) {
+      keysAction.add(new NoOpKeyCommand(0, 'c', PatchUtil.C.commentInsert()));
+      keysComment = new KeyCommandSet(PatchUtil.C.commentEditorSet());
+      keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 's',
+          PatchUtil.C.commentSaveDraft()));
+      keysComment.add(new NoOpKeyCommand(0, KeyCodes.KEY_ESCAPE,
+          PatchUtil.C.commentCancelEdit()));
+    } else {
+      keysComment = null;
+    }
+  }
+
+  void registerHandlers() {
+    removeKeyHandlerRegistrations();
+    handlers.add(GlobalKey.add(this, keysAction));
+    handlers.add(GlobalKey.add(this, keysNavigation));
+    if (keysComment != null) {
+      handlers.add(GlobalKey.add(this, keysComment));
+    }
+    handlers.add(ShowHelpCommand.addFocusHandler(getFocusHandler()));
+  }
+
+  void setupSyntaxHighlighting() {
+    if (prefs.syntaxHighlighting() && fileSize.compareTo(FileSize.SMALL) > 0) {
+      Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
+        @Override
+        public boolean execute() {
+          if (prefs.syntaxHighlighting() && isAttached()) {
+            setSyntaxHighlighting(prefs.syntaxHighlighting());
+          }
+          return false;
+        }
+      }, 250);
+    }
+  }
+
+  abstract CodeMirror newCm(
+      DiffInfo.FileMeta meta, String contents, Element parent);
+
+  void render(DiffInfo diff) {
+    header.setNoDiff(diff);
+    getChunkManager().render(diff);
+  }
+
+  void setShowLineNumbers(boolean b) {
+    if (b) {
+      getDiffTable().addStyleName(
+          Resources.I.diffTableStyle().showLineNumbers());
+    } else {
+      getDiffTable().removeStyleName(
+          Resources.I.diffTableStyle().showLineNumbers());
+    }
+  }
+
+  void setShowIntraline(boolean b) {
+    if (b && getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF) {
+      reloadDiffInfo();
+    } else if (b) {
+      getDiffTable().removeStyleName(Resources.I.diffTableStyle().noIntraline());
+    } else {
+      getDiffTable().addStyleName(Resources.I.diffTableStyle().noIntraline());
+    }
+  }
+
+  private void toggleShowIntraline() {
+    prefs.intralineDifference(!Boolean.valueOf(prefs.intralineDifference()));
+    setShowIntraline(prefs.intralineDifference());
+    prefsAction.update();
+  }
+
+  abstract void setSyntaxHighlighting(boolean b);
+
+  void setContext(final int context) {
+    operation(new Runnable() {
+      @Override
+      public void run() {
+        skipManager.removeAll();
+        skipManager.render(context, diff);
+        updateRenderEntireFile();
+      }
+    });
+  }
+
+  private int adjustCommitMessageLine(int line) {
+    /* When commit messages are shown in the diff screen they include
+      a header block that looks like this:
+
+      1 Parent:     deadbeef (Parent commit title)
+      2 Author:     A. U. Thor <author@example.com>
+      3 AuthorDate: 2015-02-27 19:20:52 +0900
+      4 Commit:     A. U. Thor <author@example.com>
+      5 CommitDate: 2015-02-27 19:20:52 +0900
+      6 [blank line]
+      7 Commit message title
+      8
+      9 Commit message body
+     10 ...
+     11 ...
+
+    If the commit is a merge commit, both parent commits are listed in the
+    first two lines instead of a 'Parent' line:
+
+      1 Merge Of:   deadbeef (Parent 1 commit title)
+      2             beefdead (Parent 2 commit title)
+
+    */
+
+    // Offset to compensate for header lines until the blank line
+    // after 'CommitDate'
+    int offset = 6;
+
+    // Adjust for merge commits, which have two parent lines
+    if (diff.textB().startsWith("Merge")) {
+      offset += 1;
+    }
+
+    // If the cursor is inside the header line, reset to the first line of the
+    // commit message. Otherwise if the cursor is on an actual line of the commit
+    // message, adjust the line number to compensate for the header lines, so the
+    // focus is on the correct line.
+    if (line <= offset) {
+      return 1;
+    }
+    return line - offset;
+  }
+
+  private Runnable openEditScreen(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        LineHandle handle = cm.extras().activeLine();
+        int line = cm.getLineNumber(handle) + 1;
+        if (Patch.COMMIT_MSG.equals(path)) {
+          line = adjustCommitMessageLine(line);
+        }
+        String token = Dispatcher.toEditScreen(revision, path, line);
+        if (!Gerrit.isSignedIn()) {
+          Gerrit.doSignIn(token);
+        } else {
+          Gerrit.display(token);
+        }
+      }
+    };
+  }
+
+  void updateRenderEntireFile() {
+    boolean entireFile = renderEntireFile();
+    for (CodeMirror cm : getCms()) {
+      cm.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
+      if (entireFile) {
+        cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
+      }
+      cm.setOption("viewportMargin", entireFile ? POSITIVE_INFINITY : 10);
+    }
+  }
+
+  void resizeCodeMirror() {
+    int height = header.getOffsetHeight() + getDiffTable().getHeaderHeight();
+    for (CodeMirror cm : getCms()) {
+      cm.adjustHeight(height);
+    }
+  }
+
+  abstract ChunkManager getChunkManager();
+
+  abstract CommentManager getCommentManager();
+
+  Change.Status getChangeStatus() {
+    return changeStatus;
+  }
+
+  int getStartLine() {
+    return startLine;
+  }
+
+  void setStartLine(int startLine) {
+    this.startLine = startLine;
+  }
+
+  DisplaySide getStartSide() {
+    return startSide;
+  }
+
+  void setStartSide(DisplaySide startSide) {
+    this.startSide = startSide;
+  }
+
+  DiffInfo getDiff() {
+    return diff;
+  }
+
+  FileSize getFileSize() {
+    return fileSize;
+  }
+
+  PreferencesAction getPrefsAction() {
+    return prefsAction;
+  }
+
+  void setPrefsAction(PreferencesAction prefsAction) {
+    this.prefsAction = prefsAction;
+  }
+
+  abstract void operation(final Runnable apply);
+
+  private Runnable upToChange(final boolean openReplyBox) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        CallbackGroup group = new CallbackGroup();
+        getCommentManager().saveAllDrafts(group);
+        group.done();
+        group.addListener(new GerritCallback<Void>() {
+          @Override
+          public void onSuccess(Void result) {
+            String b = base != null ? String.valueOf(base.get()) : null;
+            String rev = String.valueOf(revision.get());
+            Gerrit.display(
+              PageLinks.toChange(changeId, b, rev),
+              new ChangeScreen(changeId, b, rev, openReplyBox,
+                  FileTable.Mode.REVIEW));
+          }
+        });
+      }
+    };
+  }
+
+  private Runnable maybePrevVimSearch(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (cm.vim().hasSearchHighlight()) {
+          cm.vim().handleKey("N");
+        } else {
+          getCommentManager().commentNav(cm, Direction.NEXT).run();
+        }
+      }
+    };
+  }
+
+  private Runnable maybeNextVimSearch(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (cm.vim().hasSearchHighlight()) {
+          cm.vim().handleKey("n");
+        } else {
+          getChunkManager().diffChunkNav(cm, Direction.NEXT).run();
+        }
+      }
+    };
+  }
+
+  Runnable maybeNextCmSearch(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (cm.hasSearchHighlight()) {
+          cm.execCommand("findNext");
+        } else {
+          cm.execCommand("clearSearch");
+          getCommentManager().toggleOpenBox(cm).run();
+        }
+      }
+    };
+  }
+
+  boolean renderEntireFile() {
+    return prefs.renderEntireFile() && canRenderEntireFile(prefs);
+  }
+
+  boolean canRenderEntireFile(DiffPreferences prefs) {
+    // CodeMirror is too slow to layout an entire huge file.
+    return fileSize.compareTo(FileSize.HUGE) < 0
+        || (prefs.context() != WHOLE_FILE_CONTEXT && prefs.context() < 100);
+  }
+
+  DiffInfo.IntraLineStatus getIntraLineStatus() {
+    return diff.intralineStatus();
+  }
+
+  void setThemeStyles(boolean d) {
+    if (d) {
+      getDiffTable().addStyleName(Resources.I.diffTableStyle().dark());
+    } else {
+      getDiffTable().removeStyleName(Resources.I.diffTableStyle().dark());
+    }
+  }
+
+  void setShowTabs(boolean show) {
+    for (CodeMirror cm : getCms()) {
+      cm.extras().showTabs(show);
+    }
+  }
+
+  void setLineLength(int columns) {
+    for (CodeMirror cm : getCms()) {
+      cm.extras().lineLength(columns);
+    }
+  }
+
+  String getContentType(DiffInfo.FileMeta meta) {
+    if (prefs.syntaxHighlighting() && meta != null
+        && meta.contentType() != null) {
+     ModeInfo m = ModeInfo.findMode(meta.contentType(), path);
+     return m != null ? m.mime() : null;
+   }
+   return null;
+  }
+
+  String getContentType() {
+    return getContentType(diff.metaB());
+  }
+
+  void injectMode(DiffInfo diffInfo, AsyncCallback<Void> cb) {
+    new ModeInjector()
+        .add(getContentType(diffInfo.metaA()))
+        .add(getContentType(diffInfo.metaB()))
+        .inject(cb);
+  }
+
+  abstract void setAutoHideDiffHeader(boolean hide);
+
+  void prefetchNextFile() {
+    String nextPath = header.getNextPath();
+    if (nextPath != null) {
+      DiffApi.diff(revision, nextPath)
+        .base(base)
+        .wholeFile()
+        .intraline(prefs.intralineDifference())
+        .ignoreWhitespace(prefs.ignoreWhitespace())
+        .get(new AsyncCallback<DiffInfo>() {
+          @Override
+          public void onSuccess(DiffInfo info) {
+            new ModeInjector()
+              .add(getContentType(info.metaA()))
+              .add(getContentType(info.metaB()))
+              .inject(CallbackGroup.<Void> emptyCallback());
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        });
+    }
+  }
+
+  void reloadDiffInfo() {
+    final int id = ++reloadVersionId;
+    DiffApi.diff(revision, path)
+      .base(base)
+      .wholeFile()
+      .intraline(prefs.intralineDifference())
+      .ignoreWhitespace(prefs.ignoreWhitespace())
+      .get(new GerritCallback<DiffInfo>() {
+        @Override
+        public void onSuccess(DiffInfo diffInfo) {
+          if (id == reloadVersionId && isAttached()) {
+            diff = diffInfo;
+            operation(new Runnable() {
+              @Override
+              public void run() {
+                skipManager.removeAll();
+                getChunkManager().reset();
+                getDiffTable().scrollbar.removeDiffAnnotations();
+                setShowIntraline(prefs.intralineDifference());
+                render(diff);
+                skipManager.render(prefs.context(), diff);
+              }
+            });
+          }
+        }
+      });
+  }
+
+  private static FileSize bucketFileSize(DiffInfo diff) {
+    FileMeta a = diff.metaA();
+    FileMeta b = diff.metaB();
+    FileSize[] sizes = FileSize.values();
+    for (int i = sizes.length - 1; 0 <= i; i--) {
+      FileSize s = sizes[i];
+      if ((a != null && s.lines <= a.lines())
+          || (b != null && s.lines <= b.lines())) {
+        return s;
+      }
+    }
+    return FileSize.SMALL;
+  }
+
+  abstract Runnable updateActiveLine(CodeMirror cm);
+
+  private GutterClickHandler onGutterClick(final CodeMirror cm) {
+    return new GutterClickHandler() {
+      @Override
+      public void handle(CodeMirror instance, final int line,
+          final String gutterClass, NativeEvent clickEvent) {
+        if (Element.as(clickEvent.getEventTarget())
+                .hasClassName(getLineNumberClassName())
+            && clickEvent.getButton() == NativeEvent.BUTTON_LEFT
+            && !clickEvent.getMetaKey()
+            && !clickEvent.getAltKey()
+            && !clickEvent.getCtrlKey()
+            && !clickEvent.getShiftKey()) {
+          cm.setCursor(Pos.create(line));
+          Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+            @Override
+            public void execute() {
+              getCommentManager().newDraftOnGutterClick(
+                  cm, gutterClass, line + 1);
+            }
+          });
+        }
+      }
+    };
+  }
+
+  abstract FocusHandler getFocusHandler();
+
+  abstract CodeMirror[] getCms();
+
+  abstract CodeMirror getCmFromSide(DisplaySide side);
+
+  abstract DiffTable getDiffTable();
+
+  abstract int getCmLine(int line, DisplaySide side);
+
+  abstract String getLineNumberClassName();
+
+  LineOnOtherInfo lineOnOther(DisplaySide side, int line) {
+    return getChunkManager().lineMapper.lineOnOther(side, line);
+  }
+
+  abstract ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback(
+      CommentsCollections comments);
+
+  abstract boolean isSideBySide();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css
new file mode 100644
index 0000000..7569cf5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css
@@ -0,0 +1,41 @@
+/* Copyright (C) 2013 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.
+ */
+.range {
+  background-color: #ffd500 !important;
+}
+.rangeHighlight {
+  background-color: #ffff00 !important;
+}
+
+.fullscreen {
+  background-color: #f7f7f7;
+  border-bottom: 1px solid #ddd;
+}
+
+@external .diffHeader;
+.diffHeader {
+  font-size: 12px;
+  font-weight: bold;
+  color: #5252ad;
+}
+
+.diffHeader pre {
+  margin: 0 0 3px 0;
+}
+
+@external .dark, .noIntraline, .showLineNumbers;
+.dark {}
+.noIntraline {}
+.showLineNumbers {}
\ No newline at end of file
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 4b073f5..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
@@ -18,54 +18,43 @@
 import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.Widget;
 
 import net.codemirror.lib.CodeMirror;
 
 /**
- * A table with one row and two columns to hold the two CodeMirrors displaying
- * the files to be diffed.
+ * Base class for SideBySideTable2 and UnifiedTable2
  */
-class DiffTable extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, DiffTable> {}
-  private static final Binder uiBinder = GWT.create(Binder.class);
+abstract class DiffTable extends Composite {
+  static {
+    Resources.I.diffTableStyle().ensureInjected();
+  }
 
-  interface DiffTableStyle extends CssResource {
+  interface Style extends CssResource {
     String fullscreen();
-    String intralineBg();
     String dark();
-    String diff();
     String noIntraline();
     String range();
     String rangeHighlight();
+    String diffHeader();
     String showLineNumbers();
-    String hideA();
-    String hideB();
-    String padding();
   }
 
-  @UiField Element cmA;
-  @UiField Element cmB;
-  Scrollbar scrollbar;
   @UiField Element patchSetNavRow;
   @UiField Element patchSetNavCellA;
   @UiField Element patchSetNavCellB;
   @UiField Element diffHeaderRow;
   @UiField Element diffHeaderText;
   @UiField FlowPanel widgets;
-  @UiField static DiffTableStyle style;
 
   @UiField(provided = true)
   PatchSetSelectBox patchSetSelectBoxA;
@@ -73,88 +62,58 @@
   @UiField(provided = true)
   PatchSetSelectBox patchSetSelectBoxB;
 
-  private SideBySide parent;
   private boolean header;
-  private boolean visibleA;
   private ChangeType changeType;
+  Scrollbar scrollbar;
 
-  DiffTable(SideBySide parent, PatchSet.Id base, PatchSet.Id revision,
-      String path) {
+  DiffTable(DiffScreen parent, PatchSet.Id base, PatchSet.Id revision, String path) {
     patchSetSelectBoxA = new PatchSetSelectBox(
         parent, DisplaySide.A, revision.getParentKey(), base, path);
     patchSetSelectBoxB = new PatchSetSelectBox(
         parent, DisplaySide.B, revision.getParentKey(), revision, path);
     PatchSetSelectBox.link(patchSetSelectBoxA, patchSetSelectBoxB);
 
-    initWidget(uiBinder.createAndBindUi(this));
     this.scrollbar = new Scrollbar(this);
-    this.parent = parent;
-    this.visibleA = true;
   }
 
-  boolean isVisibleA() {
-    return visibleA;
-  }
-
-  void setVisibleA(boolean show) {
-    visibleA = show;
-    if (show) {
-      removeStyleName(style.hideA());
-      parent.syncScroll(DisplaySide.B); // match B's viewport
-    } else {
-      addStyleName(style.hideA());
-    }
-  }
-
-  Runnable toggleA() {
-    return new Runnable() {
-      @Override
-      public void run() {
-        setVisibleA(!isVisibleA());
-      }
-    };
-  }
-
-  void setVisibleB(boolean show) {
-    if (show) {
-      removeStyleName(style.hideB());
-      parent.syncScroll(DisplaySide.A); // match A's viewport
-    } else {
-      addStyleName(style.hideB());
-    }
-  }
+  abstract boolean isVisibleA();
 
   void setHeaderVisible(boolean show) {
+    DiffScreen parent = getDiffScreen();
     if (show != UIObject.isVisible(patchSetNavRow)) {
       UIObject.setVisible(patchSetNavRow, show);
       UIObject.setVisible(diffHeaderRow, show && header);
       if (show) {
-        parent.header.removeStyleName(style.fullscreen());
+        parent.header.removeStyleName(Resources.I.diffTableStyle().fullscreen());
       } else {
-        parent.header.addStyleName(style.fullscreen());
+        parent.header.addStyleName(Resources.I.diffTableStyle().fullscreen());
       }
       parent.resizeCodeMirror();
     }
   }
 
-  int getHeaderHeight() {
-    int h = patchSetSelectBoxA.getOffsetHeight();
-    if (header) {
-      h += diffHeaderRow.getOffsetHeight();
-    }
-    return h;
-  }
+  abstract int getHeaderHeight();
 
   ChangeType getChangeType() {
     return changeType;
   }
 
-  void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info,
+  void setUpBlameIconA(CodeMirror cm, boolean isBase, PatchSet.Id rev,
+      String path) {
+    patchSetSelectBoxA.setUpBlame(cm, isBase, rev, path);
+  }
+
+  void setUpBlameIconB(CodeMirror cm, PatchSet.Id rev,
+      String path) {
+    patchSetSelectBoxB.setUpBlame(cm, false, rev, path);
+  }
+
+  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();
@@ -162,10 +121,11 @@
       StringBuilder b = new StringBuilder();
       for (int i = 1; i < hdr.length(); i++) {
         String s = hdr.get(i);
-        if (s.startsWith("diff --git ")
+        if (!info.binary()
+            && (s.startsWith("diff --git ")
             || s.startsWith("index ")
             || s.startsWith("+++ ")
-            || s.startsWith("--- ")) {
+            || s.startsWith("--- "))) {
           continue;
         }
         b.append(s).append('\n');
@@ -182,17 +142,11 @@
     setHideEmptyPane(prefs.hideEmptyPane());
   }
 
-  void setHideEmptyPane(boolean hide) {
-    if (changeType == ChangeType.ADDED) {
-      setVisibleA(!hide);
-    } else if (changeType == ChangeType.DELETED) {
-      setVisibleB(!hide);
-    }
-  }
+  abstract void setHideEmptyPane(boolean hide);
 
   void refresh() {
     if (header) {
-      CodeMirror cm = parent.getCmFromSide(DisplaySide.A);
+      CodeMirror cm = getDiffScreen().getCmFromSide(DisplaySide.A);
       diffHeaderText.getStyle().setMarginLeft(
           cm.getGutterElement().getOffsetWidth(),
           Unit.PX);
@@ -202,4 +156,10 @@
   void add(Widget widget) {
     widgets.add(widget);
   }
+
+  abstract DiffScreen getDiffScreen();
+
+  boolean hasHeader() {
+    return header;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
deleted file mode 100644
index e72cf27..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
+++ /dev/null
@@ -1,167 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2013 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.
--->
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:d='urn:import:com.google.gerrit.client.diff'>
-  <ui:style gss='false' type='com.google.gerrit.client.diff.DiffTable.DiffTableStyle'>
-    @external .CodeMirror, .CodeMirror-selectedtext;
-    @external .CodeMirror-linenumber;
-    @external .CodeMirror-overlayscroll-vertical, .CodeMirror-scroll;
-    @external .CodeMirror-dialog-bottom;
-    @external .CodeMirror-cursor;
-
-    .fullscreen {
-      background-color: #f7f7f7;
-      border-bottom: 1px solid #ddd;
-    }
-
-    .difftable .patchSetNav,
-    .difftable .CodeMirror {
-      -webkit-touch-callout: none;
-      -webkit-user-select: none;
-      -khtml-user-select: none;
-      -moz-user-select: none;
-      -ms-user-select: none;
-    }
-
-    .difftable .CodeMirror pre {
-      overflow: hidden;
-      border-right: 0;
-      width: auto;
-    }
-
-    /* Preserve space for underscores. If this changes
-     * see ChunkManager.addPadding() and adjust there.
-     */
-    .difftable .CodeMirror pre,
-    .difftable .CodeMirror pre span {
-      padding-bottom: 1px;
-    }
-
-    .hideA .psNavA,
-    .hideA .a {
-      display: none;
-    }
-
-    .hideB .psNavB,
-    .hideB .b {
-      display: none;
-    }
-
-    .table {
-      width: 100%;
-      table-layout: fixed;
-      border-spacing: 0;
-    }
-    .table td { padding: 0 }
-    .a, .b { width: 50% }
-    .hideA .psNavB, .hideA .b { width: 100% }
-    .hideB .psNavA, .hideB .a { width: 100% }
-
-    /* Hide scrollbars on A, B controls both views. */
-    .a .CodeMirror-scroll { margin-right: -36px; }
-    .a .CodeMirror-overlayscroll-vertical { display: none !important; }
-
-    .showLineNumbers .b { border-left: none; }
-    .b { border-left: 1px solid #ddd; }
-
-    .a .diff { background-color: #faa; }
-    /* Set min-width for lineWrapping to make sure it gets enough width
-       before lineWrapping and to make sure it dosent do a ugly line wrap */
-    .b .diff { background-color: #9f9; min-width: 60em; }
-    .a .intralineBg { background-color: #fee; }
-    .b .intralineBg { background-color: #dfd; }
-    .noIntraline .a .intralineBg { background-color: #faa; }
-    .noIntraline .b .intralineBg { background-color: #9f9; }
-
-    .dark .a .diff { background-color: #400; }
-    .dark .b .diff { background-color: #444; }
-
-    .dark .a .intralineBg { background-color: #888; }
-    .dark .b .intralineBg { background-color: #bbb; }
-    .dark .noIntraline .a .intralineBg { background-color: #400; }
-    .dark .noIntraline .b .intralineBg { background-color: #444; }
-
-    .patchSetNav, .diff_header {
-      background-color: #f7f7f7;
-      line-height: 1;
-    }
-    .fileCommentCell {
-      overflow-x: auto;
-    }
-
-    .range {
-      background-color: #ffd500 !important;
-    }
-    .rangeHighlight {
-      background-color: #ffff00 !important;
-    }
-    .difftable .CodeMirror-selectedtext {
-      background-color: inherit !important;
-    }
-    .difftable .CodeMirror-linenumber {
-      height: 1.11em;
-      cursor: pointer;
-    }
-    .difftable .CodeMirror div.CodeMirror-cursor {
-      border-left: 2px solid black;
-    }
-    .difftable .CodeMirror-dialog-bottom {
-      border-top: 0;
-      border-left: 1px solid #000;
-      border-bottom: 1px solid #000;
-      background-color: #f7f7f7;
-      top: 0;
-      right: 0;
-      bottom: auto;
-      left: auto;
-    }
-    .showLineNumbers .padding {
-      margin-left: 21px;
-      border-left: 2px solid #d64040;
-    }
-
-    .diff_header {
-      font-size: 12px;
-      font-weight: bold;
-      color: #5252ad;
-    }
-    .diff_header pre {
-      margin: 0 0 3px 0;
-    }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.difftable}'>
-    <table class='{style.table}'>
-      <tr ui:field='patchSetNavRow' class='{style.patchSetNav}'>
-        <td ui:field='patchSetNavCellA' class='{style.psNavA}'>
-          <d:PatchSetSelectBox ui:field='patchSetSelectBoxA' />
-        </td>
-        <td ui:field='patchSetNavCellB' class='{style.psNavB}'>
-          <d:PatchSetSelectBox ui:field='patchSetSelectBoxB' />
-        </td>
-      </tr>
-      <tr ui:field='diffHeaderRow' class='{style.diff_header}'>
-        <td colspan='2'><pre ui:field='diffHeaderText' /></td>
-      </tr>
-      <tr>
-        <td ui:field='cmA' class='{style.a}' />
-        <td ui:field='cmB' class='{style.b}' />
-      </tr>
-    </table>
-    <g:FlowPanel ui:field='widgets' visible='false'/>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java
index 4dfcd8c..c7ee678 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java
@@ -16,5 +16,9 @@
 
 /** Enum representing the side on a side-by-side view */
 public enum DisplaySide {
-  A, B
+  A, B;
+
+  DisplaySide otherSide() {
+    return this == A ? B : A;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
index 21b2f50..d814a57 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.client.diff;
 
 import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.change.LocalComments;
 import com.google.gerrit.client.changes.CommentApi;
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
@@ -190,7 +192,7 @@
     setRangeHighlight(edit);
     if (edit) {
       String msg = comment.message() != null
-          ? comment.message().trim()
+          ? comment.message()
           : "";
       editArea.setValue(msg);
       cancel.setVisible(!isNew());
@@ -289,6 +291,7 @@
     enableEdit(false);
 
     pendingGroup = group;
+    final LocalComments lc = new LocalComments(psId);
     GerritCallback<CommentInfo> cb = new GerritCallback<CommentInfo>() {
       @Override
       public void onSuccess(CommentInfo result) {
@@ -306,6 +309,11 @@
       public void onFailure(Throwable e) {
         enableEdit(true);
         pendingGroup = null;
+        if (RestApi.isNotSignedIn(e)) {
+          CommentInfo saved = CommentInfo.copy(comment);
+          saved.message(editArea.getValue().trim());
+          lc.setInlineComment(saved);
+        }
         super.onFailure(e);
       }
     };
@@ -380,14 +388,13 @@
         removeUI();
         restoreSelection();
         return;
-      } else {
-        setEdit(false);
-        if (autoClosed) {
-          setOpen(false);
-        }
-        getCm().focus();
-        return;
       }
+      setEdit(false);
+      if (autoClosed) {
+        setOpen(false);
+      }
+      getCm().focus();
+      return;
     }
     expandTimer.schedule(250);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
index eec3d0c..f377038 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
@@ -16,14 +16,13 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.DiffPreferences;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ReviewInfo;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.diff.DiffInfo.Region;
 import com.google.gerrit.client.info.ChangeInfo;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.info.FileInfo;
-import com.google.gerrit.client.info.GitwebInfo;
 import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.CallbackGroup;
@@ -33,7 +32,9 @@
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
 import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArray;
@@ -67,13 +68,15 @@
     Resources.I.style().ensureInjected();
   }
 
-  private static enum ReviewedState {
+  private enum ReviewedState {
     AUTO_REVIEW, LOADED
   }
 
   @UiField CheckBox reviewed;
   @UiField Element project;
   @UiField Element filePath;
+  @UiField Element fileNumber;
+  @UiField Element fileCount;
 
   @UiField Element noDiff;
   @UiField FlowPanel linkPanel;
@@ -87,80 +90,69 @@
   private final PatchSet.Id base;
   private final PatchSet.Id patchSetId;
   private final String path;
+  private final DiffView diffScreenType;
+  private final DiffPreferences prefs;
   private boolean hasPrev;
   private boolean hasNext;
   private String nextPath;
+  private JsArray<FileInfo> files;
   private PreferencesAction prefsAction;
   private ReviewedState reviewedState;
 
   Header(KeyCommandSet keys, PatchSet.Id base, PatchSet.Id patchSetId,
-      String path) {
+      String path, DiffView diffSreenType, DiffPreferences prefs) {
     initWidget(uiBinder.createAndBindUi(this));
     this.keys = keys;
     this.base = base;
     this.patchSetId = patchSetId;
     this.path = path;
+    this.diffScreenType = diffSreenType;
+    this.prefs = prefs;
 
     if (!Gerrit.isSignedIn()) {
       reviewed.getElement().getStyle().setVisibility(Visibility.HIDDEN);
     }
-    SafeHtml.setInnerHTML(filePath, formatPath(path, null, null));
+    SafeHtml.setInnerHTML(filePath, formatPath(path));
     up.setTargetHistoryToken(PageLinks.toChange(
         patchSetId.getParentKey(),
         base != null ? base.getId() : null, patchSetId.getId()));
   }
 
-  public static SafeHtml formatPath(String path, String project, String commit) {
+  public static SafeHtml formatPath(String path) {
     SafeHtmlBuilder b = new SafeHtmlBuilder();
     if (Patch.COMMIT_MSG.equals(path)) {
       return b.append(Util.C.commitMessage());
     }
 
-    GitwebInfo gw = (project != null && commit != null)
-        ? Gerrit.info().gitweb() : null;
     int s = path.lastIndexOf('/') + 1;
-    if (gw != null && s > 0) {
-      String base = path.substring(0, s - 1);
-      b.openAnchor()
-          .setAttribute("href", gw.toFile(project, commit, base))
-          .setAttribute("title", gw.getLinkName())
-          .append(base)
-          .closeAnchor()
-          .append('/');
-    } else {
-      b.append(path.substring(0, s));
-    }
+    b.append(path.substring(0, s));
     b.openElement("b");
     b.append(path.substring(s));
     b.closeElement("b");
     return b;
   }
 
+  private int findCurrentFileIndex(JsArray<FileInfo> files) {
+    int currIndex = 0;
+    for (int i = 0; i < files.length(); i++) {
+      if (path.equals(files.get(i).path())) {
+        currIndex = i;
+        break;
+      }
+    }
+    return currIndex;
+  }
+
   @Override
   protected void onLoad() {
     DiffApi.list(patchSetId, base, new GerritCallback<NativeMap<FileInfo>>() {
       @Override
       public void onSuccess(NativeMap<FileInfo> result) {
-        JsArray<FileInfo> files = result.values();
+        files = result.values();
         FileInfo.sortFileInfoByPath(files);
-        int index = 0; // TODO: Maybe use patchIndex.
-        for (int i = 0; i < files.length(); i++) {
-          if (path.equals(files.get(i).path())) {
-            index = i;
-            break;
-          }
-        }
-        FileInfo nextInfo = index == files.length() - 1
-            ? null
-            : files.get(index + 1);
-        KeyCommand p = setupNav(prev, '[', PatchUtil.C.previousFileHelp(),
-            index == 0 ? null : files.get(index - 1));
-        KeyCommand n = setupNav(next, ']', PatchUtil.C.nextFileHelp(),
-            nextInfo);
-        if (p != null && n != null) {
-          keys.pair(p, n);
-        }
-        nextPath = nextInfo != null ? nextInfo.path() : null;
+        fileNumber.setInnerText(
+            Integer.toString(Natives.asList(files).indexOf(result.get(path)) + 1));
+        fileCount.setInnerText(Integer.toString(files.length()));
       }
     });
 
@@ -194,23 +186,6 @@
   }
 
   void setChangeInfo(ChangeInfo info) {
-    GitwebInfo gw = Gerrit.info().gitweb();
-    if (gw != null) {
-      for (RevisionInfo rev : Natives.asList(info.revisions().values())) {
-        if (patchSetId.getId().equals(rev.id())) {
-          String c = rev.name();
-          SafeHtml.setInnerHTML(filePath, formatPath(path, info.project(), c));
-          SafeHtml.setInnerHTML(project, new SafeHtmlBuilder()
-              .openAnchor()
-              .setAttribute("href", gw.toFile(info.project(), c, ""))
-              .setAttribute("title", gw.getLinkName())
-              .append(info.project())
-              .closeAnchor());
-          return;
-        }
-      }
-    }
-
     project.setInnerText(info.project());
   }
 
@@ -262,9 +237,9 @@
   }
 
   private String url(FileInfo info) {
-    return info.binary()
-      ? Dispatcher.toUnified(base, patchSetId, info.path())
-      : Dispatcher.toSideBySide(base, patchSetId, info.path());
+    return diffScreenType == DiffView.UNIFIED_DIFF
+        ? Dispatcher.toUnified(base, patchSetId, info.path())
+        : Dispatcher.toSideBySide(base, patchSetId, info.path());
   }
 
   private KeyCommand setupNav(InlineHyperlink link, char key, String help, FileInfo info) {
@@ -287,11 +262,46 @@
         hasNext = true;
       }
       return k;
-    } else {
-      link.getElement().getStyle().setVisibility(Visibility.HIDDEN);
-      keys.add(new UpToChangeCommand(patchSetId, 0, key));
-      return null;
     }
+    link.getElement().getStyle().setVisibility(Visibility.HIDDEN);
+    keys.add(new UpToChangeCommand(patchSetId, 0, key));
+    return null;
+  }
+
+  private boolean shouldSkipFile(FileInfo curr, CommentsCollections comments) {
+    return prefs.skipDeleted() && ChangeType.DELETED.matches(curr.status())
+        || prefs.skipUnchanged() && ChangeType.RENAMED.matches(curr.status())
+        || prefs.skipUncommented() && !comments.hasCommentForPath(curr.path());
+  }
+
+  void setupPrevNextFiles(CommentsCollections comments) {
+    FileInfo prevInfo = null;
+    FileInfo nextInfo = null;
+    int currIndex = findCurrentFileIndex(files);
+    for (int i = currIndex - 1; i >= 0; i--) {
+      FileInfo curr = files.get(i);
+      if (shouldSkipFile(curr, comments)) {
+        continue;
+      }
+      prevInfo = curr;
+      break;
+    }
+    for (int i = currIndex + 1; i < files.length(); i++) {
+      FileInfo curr = files.get(i);
+      if (shouldSkipFile(curr, comments)) {
+        continue;
+      }
+      nextInfo = curr;
+      break;
+    }
+    KeyCommand p = setupNav(prev, '[', PatchUtil.C.previousFileHelp(),
+        prevInfo);
+    KeyCommand n = setupNav(next, ']', PatchUtil.C.nextFileHelp(),
+        nextInfo);
+    if (p != null && n != null) {
+      keys.pair(p, n);
+    }
+    nextPath = nextInfo != null ? nextInfo.path() : null;
   }
 
   Runnable toggleReviewed() {
@@ -345,9 +355,13 @@
   }
 
   void setNoDiff(DiffInfo diff) {
-    JsArray<Region> regions = diff.content();
-    boolean b = regions.length() == 0
-        || (regions.length() == 1 && regions.get(0).ab() != null);
-    UIObject.setVisible(noDiff, b);
+    if (diff.binary()) {
+      UIObject.setVisible(noDiff, false); // Don't bother showing "No Differences"
+    } else {
+      JsArray<Region> regions = diff.content();
+      boolean b = regions.length() == 0
+          || (regions.length() == 1 && regions.get(0).ab() != null);
+      UIObject.setVisible(noDiff, b);
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
index f13c9a3..39eb6cb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
@@ -34,6 +34,11 @@
   .path {
     white-space: nowrap;
   }
+  .fileCount {
+    white-space: nowrap;
+    position: relative;
+    bottom: 4px;
+  }
   .navigation {
     position: absolute;
     top: 0;
@@ -69,6 +74,9 @@
     <div class='{style.navigation}'>
       <span ui:field='noDiff' class='{style.nodiff}'><ui:msg>No Differences</ui:msg></span>
       <g:FlowPanel ui:field='linkPanel' styleName='{style.linkPanel}'/>
+      <span class='{style.fileCount}'>
+        <ui:msg>File <span ui:field='fileNumber'/> of <span ui:field='fileCount'/></ui:msg>
+      </span>
       <x:InlineHyperlink ui:field='prev' styleName='{res.style.goPrev}'/>
       <x:InlineHyperlink ui:field='up'
           styleName='{res.style.goUp}'
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java
index 7c8bc21..ac295e3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java
@@ -43,7 +43,7 @@
       @Override
       public void onClick(ClickEvent event) {
         setVisible(false);
-        commentManager.insertNewDraft(cm).run();
+        commentManager.newDraftCallback(cm).run();
       }
     }, ClickEvent.getType());
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
index 9ec760c..96128f3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
@@ -118,17 +118,16 @@
     int ret = Collections.binarySearch(lineGaps, new LineGap(line));
     if (ret == -1) {
       return new LineOnOtherInfo(line, true);
-    } else {
-      LineGap lookup = lineGaps.get(0 <= ret ? ret : -ret - 2);
-      int start = lookup.start;
-      int end = lookup.end;
-      int delta = lookup.delta;
-      if (start <= line && line <= end && end != -1) { // Line falls within gap
-        return new LineOnOtherInfo(end + delta, false);
-      } else { // Line after gap
-        return new LineOnOtherInfo(line + delta, true);
-      }
     }
+    LineGap lookup = lineGaps.get(0 <= ret ? ret : -ret - 2);
+    int start = lookup.start;
+    int end = lookup.end;
+    int delta = lookup.delta;
+    if (start <= line && line <= end && end != -1) { // Line falls within gap
+      return new LineOnOtherInfo(end + delta, false);
+    }
+    // Line after gap
+    return new LineOnOtherInfo(line + delta, true);
   }
 
   AlignedPair align(DisplaySide mySide, int line) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java
index c22769e..969b861 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java
@@ -26,4 +26,4 @@
   @Override
   public void onKeyPress(KeyPressEvent event) {
   }
-}
\ No newline at end of file
+}
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 186cd98..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
@@ -16,9 +16,13 @@
 
 import com.google.gerrit.client.Dispatcher;
 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;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.reviewdb.client.Change;
@@ -27,6 +31,7 @@
 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.resources.client.CssResource;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
@@ -39,6 +44,8 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtorm.client.KeyUtil;
 
+import net.codemirror.lib.CodeMirror;
+
 import java.util.List;
 
 /** HTMLPanel to select among patch sets */
@@ -54,7 +61,7 @@
   @UiField HTMLPanel linkPanel;
   @UiField BoxStyle style;
 
-  private SideBySide parent;
+  private DiffScreen parent;
   private DisplaySide side;
   private boolean sideA;
   private String path;
@@ -63,7 +70,7 @@
   private PatchSet.Id idActive;
   private PatchSetSelectBox other;
 
-  PatchSetSelectBox(SideBySide parent,
+  PatchSetSelectBox(DiffScreen parent,
       DisplaySide side,
       Change.Id changeId,
       PatchSet.Id revision,
@@ -81,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);
@@ -100,8 +123,6 @@
     }
     if (selectedLink != null) {
       selectedLink.setStyleName(style.selected());
-    } else if (sideA) {
-      baseLink.setStyleName(style.selected());
     }
 
     if (meta == null) {
@@ -124,6 +145,32 @@
     }
   }
 
+  void setUpBlame(final CodeMirror cm, final boolean isBase,
+      final PatchSet.Id rev, final String path) {
+    if (!Patch.COMMIT_MSG.equals(path) && Gerrit.isSignedIn()
+        && Gerrit.info().change().allowBlame()) {
+      Anchor blameIcon = createBlameIcon();
+      blameIcon.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(ClickEvent clickEvent) {
+          if (cm.extras().getBlameInfo() != null) {
+            cm.extras().toggleAnnotation();
+          } else {
+            ChangeApi.blame(rev, path, isBase)
+              .get(new GerritCallback<JsArray<BlameInfo>>() {
+
+                @Override
+                public void onSuccess(JsArray<BlameInfo> lines) {
+                  cm.extras().toggleAnnotation(lines);
+                }
+              });
+          }
+        }
+      });
+      linkPanel.add(blameIcon);
+    }
+  }
+
   private Widget createEditIcon() {
     PatchSet.Id id = (idActive == null) ? other.idActive : idActive;
     Anchor anchor = new Anchor(
@@ -133,6 +180,13 @@
     return anchor;
   }
 
+  private Anchor createBlameIcon() {
+    Anchor anchor = new Anchor(
+        new ImageResourceRenderer().render(Gerrit.RESOURCES.blame()));
+    anchor.setTitle(PatchUtil.C.blame());
+    return anchor;
+  }
+
   static void link(PatchSetSelectBox a, PatchSetSelectBox b) {
     a.other = b;
     b.other = a;
@@ -143,10 +197,13 @@
     if (sideA) {
       assert other.idActive != null;
     }
-    return new InlineHyperlink(label, Dispatcher.toSideBySide(
-        sideA ? id : other.idActive,
-        sideA ? other.idActive : id,
-        path));
+    PatchSet.Id diffBase = sideA ? id : other.idActive;
+    PatchSet.Id revision = sideA ? other.idActive : id;
+
+    return new InlineHyperlink(label,
+        parent.isSideBySide()
+            ? Dispatcher.toSideBySide(diffBase, revision, path)
+            : Dispatcher.toUnified(diffBase, revision, path));
   }
 
   private Anchor createDownloadLink() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java
index 869b4a3..3f7a0ab 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java
@@ -22,13 +22,13 @@
 import com.google.gwt.user.client.ui.Widget;
 
 class PreferencesAction {
-  private final SideBySide view;
+  private final DiffScreen view;
   private final DiffPreferences prefs;
   private PopupPanel popup;
   private PreferencesBox current;
   private Widget partner;
 
-  PreferencesAction(SideBySide view, DiffPreferences prefs) {
+  PreferencesAction(DiffScreen view, DiffPreferences prefs) {
     this.view = view;
     this.prefs = prefs;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
index c5b1f96..ef1d4bd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
@@ -58,6 +58,7 @@
 import com.google.gwt.user.client.ui.ToggleButton;
 import com.google.gwt.user.client.ui.UIObject;
 
+import net.codemirror.lib.CodeMirror;
 import net.codemirror.mode.ModeInfo;
 import net.codemirror.mode.ModeInjector;
 import net.codemirror.theme.ThemeLoader;
@@ -73,7 +74,7 @@
     String dialog();
   }
 
-  private final SideBySide view;
+  private final DiffScreen view;
   private DiffPreferences prefs;
   private int contextLastValue;
   private Timer updateContextTimer;
@@ -102,13 +103,16 @@
   @UiField ToggleButton renderEntireFile;
   @UiField ToggleButton matchBrackets;
   @UiField ToggleButton lineWrapping;
+  @UiField ToggleButton skipDeleted;
+  @UiField ToggleButton skipUnchanged;
+  @UiField ToggleButton skipUncommented;
   @UiField ListBox theme;
   @UiField Element modeLabel;
   @UiField ListBox mode;
   @UiField Button apply;
   @UiField Button save;
 
-  public PreferencesBox(SideBySide view) {
+  public PreferencesBox(DiffScreen view) {
     this.view = view;
 
     initWidget(uiBinder.createAndBindUi(this));
@@ -168,7 +172,7 @@
 
     setIgnoreWhitespace(prefs.ignoreWhitespace());
     tabWidth.setIntValue(prefs.tabSize());
-    if (view != null && Patch.COMMIT_MSG.equals(view.getPath())) {
+    if (view != null && Patch.COMMIT_MSG.equals(view.path)) {
       lineLength.setEnabled(false);
       lineLength.setIntValue(72);
     } else {
@@ -182,9 +186,9 @@
     lineNumbers.setValue(prefs.showLineNumbers());
     emptyPane.setValue(!prefs.hideEmptyPane());
     if (view != null) {
-      leftSide.setValue(view.diffTable.isVisibleA());
+      leftSide.setValue(view.getDiffTable().isVisibleA());
       leftSide.setEnabled(!(prefs.hideEmptyPane()
-          && view.diffTable.getChangeType() == ChangeType.ADDED));
+          && view.getDiffTable().getChangeType() == ChangeType.ADDED));
     } else {
       UIObject.setVisible(leftSideLabel, false);
       leftSide.setVisible(false);
@@ -195,6 +199,9 @@
     expandAllComments.setValue(prefs.expandAllComments());
     matchBrackets.setValue(prefs.matchBrackets());
     lineWrapping.setValue(prefs.lineWrapping());
+    skipDeleted.setValue(!prefs.skipDeleted());
+    skipUnchanged.setValue(!prefs.skipUnchanged());
+    skipUncommented.setValue(!prefs.skipUncommented());
     setTheme(prefs.theme());
 
     if (view == null || view.canRenderEntireFile(prefs)) {
@@ -253,7 +260,7 @@
 
   @UiHandler("intralineDifference")
   void onIntralineDifference(ValueChangeEvent<Boolean> e) {
-    prefs.intralineDifference(e.getValue());
+    prefs.intralineDifference(Boolean.valueOf(e.getValue()));
     if (view != null) {
       view.setShowIntraline(prefs.intralineDifference());
     }
@@ -317,8 +324,9 @@
           @Override
           public void run() {
             int v = prefs.tabSize();
-            view.getCmFromSide(DisplaySide.A).setOption("tabSize", v);
-            view.getCmFromSide(DisplaySide.B).setOption("tabSize", v);
+            for (CodeMirror cm : view.getCms()) {
+              cm.setOption("tabSize", v);
+            }
           }
         });
       }
@@ -380,21 +388,23 @@
 
   @UiHandler("leftSide")
   void onLeftSide(ValueChangeEvent<Boolean> e) {
-    view.diffTable.setVisibleA(e.getValue());
+    if (view.getDiffTable() instanceof SideBySideTable) {
+      ((SideBySideTable) view.getDiffTable()).setVisibleA(e.getValue());
+    }
   }
 
   @UiHandler("emptyPane")
   void onHideEmptyPane(ValueChangeEvent<Boolean> e) {
     prefs.hideEmptyPane(!e.getValue());
     if (view != null) {
-      view.diffTable.setHideEmptyPane(prefs.hideEmptyPane());
+      view.getDiffTable().setHideEmptyPane(prefs.hideEmptyPane());
       if (prefs.hideEmptyPane()) {
-        if (view.diffTable.getChangeType() == ChangeType.ADDED) {
+        if (view.getDiffTable().getChangeType() == ChangeType.ADDED) {
           leftSide.setValue(false);
           leftSide.setEnabled(false);
         }
       } else {
-        leftSide.setValue(view.diffTable.isVisibleA());
+        leftSide.setValue(view.getDiffTable().isVisibleA());
         leftSide.setEnabled(true);
       }
     }
@@ -470,8 +480,9 @@
         @Override
         public void run() {
           boolean s = prefs.showWhitespaceErrors();
-          view.getCmFromSide(DisplaySide.A).setOption("showTrailingSpace", s);
-          view.getCmFromSide(DisplaySide.B).setOption("showTrailingSpace", s);
+          for (CodeMirror cm : view.getCms()) {
+            cm.setOption("showTrailingSpace", s);
+          }
         }
       });
     }
@@ -503,6 +514,24 @@
         prefs.lineWrapping());
   }
 
+  @UiHandler("skipDeleted")
+  void onSkipDeleted(ValueChangeEvent<Boolean> e) {
+    prefs.skipDeleted(!e.getValue());
+    // TODO: Update the navigation links on the current DiffScreen
+  }
+
+  @UiHandler("skipUnchanged")
+  void onSkipUnchanged(ValueChangeEvent<Boolean> e) {
+    prefs.skipUnchanged(!e.getValue());
+    // TODO: Update the navigation links on the current DiffScreen
+  }
+
+  @UiHandler("skipUncommented")
+  void onSkipUncommented(ValueChangeEvent<Boolean> e) {
+    prefs.skipUncommented(!e.getValue());
+    // TODO: Update the navigation links on the current DiffScreen
+  }
+
   @UiHandler("theme")
   void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
     final Theme newTheme = getSelectedTheme();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
index 7dbbc21..4465d63 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
@@ -305,6 +305,27 @@
         </g:ToggleButton></td>
       </tr>
       <tr>
+        <th><ui:msg>Skip Deleted Files</ui:msg></th>
+        <td><g:ToggleButton ui:field='skipDeleted'>
+          <g:upFace><ui:msg>Yes</ui:msg></g:upFace>
+          <g:downFace><ui:msg>No</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Skip Unchanged Files</ui:msg></th>
+        <td><g:ToggleButton ui:field='skipUnchanged'>
+          <g:upFace><ui:msg>Yes</ui:msg></g:upFace>
+          <g:downFace><ui:msg>No</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Skip Uncommented Files</ui:msg></th>
+        <td><g:ToggleButton ui:field='skipUncommented'>
+          <g:upFace><ui:msg>Yes</ui:msg></g:upFace>
+          <g:downFace><ui:msg>No</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
         <td></td>
         <td>
           <g:Button ui:field='apply' styleName='{style.apply}'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
index 48b4c3c..dc2e3a2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
@@ -18,6 +18,8 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.api.ApiGlue;
+import com.google.gerrit.client.change.ReplyBox;
 import com.google.gerrit.client.changes.CommentApi;
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.changes.Util;
@@ -44,12 +46,13 @@
   interface Binder extends UiBinder<HTMLPanel, PublishedBox> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
-  static interface Style extends CssResource {
+  interface Style extends CssResource {
     String closed();
   }
 
   private final PatchSet.Id psId;
   private final CommentInfo comment;
+  private final DisplaySide displaySide;
   private DraftBox replyBox;
 
   @UiField Style style;
@@ -71,11 +74,13 @@
       CommentLinkProcessor clp,
       PatchSet.Id psId,
       CommentInfo info,
+      DisplaySide displaySide,
       boolean open) {
     super(group, info.range());
 
     this.psId = psId;
     this.comment = info;
+    this.displaySide = displaySide;
 
     if (info.author() != null) {
       avatar = new AvatarImage(info.author());
@@ -99,6 +104,7 @@
       summary.setInnerText(msg);
       message.setInnerSafeHtml(clp.apply(
           new SafeHtmlBuilder().append(msg).wikify()));
+      ApiGlue.fireEvent("comment", message);
     }
 
     fix.setVisible(open);
@@ -143,17 +149,21 @@
     replyBox.setEdit(true);
   }
 
-  void addReplyBox() {
+  void addReplyBox(boolean quote) {
+    CommentInfo commentReply = CommentInfo.createReply(comment);
+    if (quote) {
+      commentReply.message(ReplyBox.quote(comment.message()));
+    }
     getCommentManager().addDraftBox(
-      getCm().side(),
-      CommentInfo.createReply(comment)).setEdit(true);
+      displaySide,
+      commentReply).setEdit(true);
   }
 
   void doReply() {
     if (!Gerrit.isSignedIn()) {
-      Gerrit.doSignIn(getCommentManager().getSideBySide().getToken());
+      Gerrit.doSignIn(getCommentManager().host.getToken());
     } else if (replyBox == null) {
-      addReplyBox();
+      addReplyBox(false);
     } else {
       openReplyBox();
     }
@@ -165,11 +175,20 @@
     doReply();
   }
 
+  @UiHandler("quote")
+  void onQuote(ClickEvent e) {
+    e.stopPropagation();
+    if (!Gerrit.isSignedIn()) {
+      Gerrit.doSignIn(getCommentManager().host.getToken());
+    }
+    addReplyBox(true);
+  }
+
   @UiHandler("done")
   void onReplyDone(ClickEvent e) {
     e.stopPropagation();
     if (!Gerrit.isSignedIn()) {
-      Gerrit.doSignIn(getCommentManager().getSideBySide().getToken());
+      Gerrit.doSignIn(getCommentManager().host.getToken());
     } else if (replyBox == null) {
       done.setEnabled(false);
       CommentInfo input = CommentInfo.createReply(comment);
@@ -180,7 +199,7 @@
             public void onSuccess(CommentInfo result) {
               done.setEnabled(true);
               setOpen(false);
-              getCommentManager().addDraftBox(getCm().side(), result);
+              getCommentManager().addDraftBox(displaySide, result);
             }
           });
     } else {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
index 46b76ca..cbea847 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
@@ -61,6 +61,11 @@
           <ui:attribute name='title'/>
           <div><ui:msg>Reply</ui:msg></div>
         </g:Button>
+        <g:Button ui:field='quote' styleName=''
+            title='Reply to this comment with quoting it'>
+          <ui:attribute name='title'/>
+          <div><ui:msg>Quote</ui:msg></div>
+        </g:Button>
         <g:Button ui:field='done' styleName=''
             title='Reply "Done" to this comment'>
           <ui:attribute name='title'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
index e379d60..2723374 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
@@ -20,11 +20,16 @@
 
 /** Resources used by diff. */
 interface Resources extends ClientBundle {
-  static final Resources I = GWT.create(Resources.class);
+  Resources I = GWT.create(Resources.class);
 
   @Source("CommentBox.css") CommentBox.Style style();
   @Source("Scrollbar.css") Scrollbar.Style scrollbarStyle();
+  @Source("DiffTable.css") DiffTable.Style diffTableStyle();
 
+  /**
+   * tango icon library (public domain):
+   * http://tango.freedesktop.org/Tango_Icon_Library
+   */
   @Source("goPrev.png") ImageResource goPrev();
   @Source("goNext.png") ImageResource goNext();
   @Source("goUp.png") ImageResource goUp();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
index 4ee09d2..13f58db 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
@@ -20,7 +20,7 @@
 import net.codemirror.lib.ScrollInfo;
 
 class ScrollSynchronizer {
-  private DiffTable diffTable;
+  private SideBySideTable diffTable;
   private LineMapper mapper;
   private ScrollCallback active;
   private ScrollCallback callbackA;
@@ -28,7 +28,7 @@
   private CodeMirror cmB;
   private boolean autoHideDiffTableHeader;
 
-  ScrollSynchronizer(DiffTable diffTable,
+  ScrollSynchronizer(SideBySideTable diffTable,
       CodeMirror cmA, CodeMirror cmB,
       LineMapper mapper) {
     this.diffTable = diffTable;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css
index 383f278..26f8ff5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css
@@ -27,12 +27,14 @@
 
 .delete {
   background-color: #faa;
+  min-width: 12px;
 }
 .insert {
   background-color: #9f9;
+  min-width: 12px;
 }
 .edit {
-  border-left: 3px solid #faa;
-  width: 2px !important;
+  border-left: 6px solid #faa;
+  width: 6px !important;
   background-color: #9f9;
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
index 0fdc519..dbe7e5d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
@@ -14,50 +14,26 @@
 
 package com.google.gerrit.client.diff;
 
-import static com.google.gerrit.extensions.client.DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
 import static java.lang.Double.POSITIVE_INFINITY;
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.JumpKeys;
-import com.google.gerrit.client.account.DiffPreferences;
-import com.google.gerrit.client.change.ChangeScreen;
-import com.google.gerrit.client.change.FileTable;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeList;
-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.EditInfo;
-import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.info.FileInfo;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.projects.ConfigInfoCache;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.RepeatingCommand;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.event.dom.client.FocusEvent;
 import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.ResizeEvent;
-import com.google.gwt.event.logical.shared.ResizeHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.client.Window;
@@ -66,77 +42,31 @@
 import com.google.gwt.user.client.ui.ImageResourceRenderer;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtexpui.globalkey.client.ShowHelpCommand;
 
 import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.BeforeSelectionChangeHandler;
-import net.codemirror.lib.CodeMirror.GutterClickHandler;
 import net.codemirror.lib.CodeMirror.LineHandle;
 import net.codemirror.lib.Configuration;
 import net.codemirror.lib.KeyMap;
 import net.codemirror.lib.Pos;
-import net.codemirror.mode.ModeInfo;
-import net.codemirror.mode.ModeInjector;
-import net.codemirror.theme.ThemeLoader;
 
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.List;
 
-public class SideBySide extends Screen {
-  private static final KeyMap RENDER_ENTIRE_FILE_KEYMAP = KeyMap.create()
-      .propagate("Ctrl-F");
-
+public class SideBySide extends DiffScreen {
   interface Binder extends UiBinder<FlowPanel, SideBySide> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
-
-  enum FileSize {
-    SMALL(0),
-    LARGE(500),
-    HUGE(4000);
-
-    final int lines;
-
-    FileSize(int n) {
-      this.lines = n;
-    }
-  }
+  private static final String LINE_NUMBER_CLASSNAME = "CodeMirror-linenumber";
 
   @UiField(provided = true)
-  Header header;
-
-  @UiField(provided = true)
-  DiffTable diffTable;
-
-  private final Change.Id changeId;
-  private final PatchSet.Id base;
-  private final PatchSet.Id revision;
-  private final String path;
-  private DisplaySide startSide;
-  private int startLine;
-  private DiffPreferences prefs;
-  private Change.Status changeStatus;
+  SideBySideTable diffTable;
 
   private CodeMirror cmA;
   private CodeMirror cmB;
 
-  private HandlerRegistration resizeHandler;
   private ScrollSynchronizer scrollSynchronizer;
-  private DiffInfo diff;
-  private FileSize fileSize;
-  private EditInfo edit;
-  private ChunkManager chunkManager;
-  private CommentManager commentManager;
-  private SkipManager skipManager;
 
-  private KeyCommandSet keysNavigation;
-  private KeyCommandSet keysAction;
-  private KeyCommandSet keysComment;
-  private List<HandlerRegistration> handlers;
-  private PreferencesAction prefsAction;
-  private int reloadVersionId;
+  private SideBySideChunkManager chunkManager;
+  private SideBySideCommentManager commentManager;
 
   public SideBySide(
       PatchSet.Id base,
@@ -144,149 +74,34 @@
       String path,
       DisplaySide startSide,
       int startLine) {
-    this.base = base;
-    this.revision = revision;
-    this.changeId = revision.getParentKey();
-    this.path = path;
-    this.startSide = startSide;
-    this.startLine = startLine;
+    super(base, revision, path, startSide, startLine, DiffView.SIDE_BY_SIDE);
 
-    prefs = DiffPreferences.create(Gerrit.getDiffPreferences());
-    handlers = new ArrayList<>(6);
-    keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
-    header = new Header(keysNavigation, base, revision, path);
-    diffTable = new DiffTable(this, base, revision, path);
+    diffTable = new SideBySideTable(this, base, revision, path);
     add(uiBinder.createAndBindUi(this));
     addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
   }
 
   @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    setHeaderVisible(false);
-    setWindowTitle(FileInfo.getFileName(path));
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-
-    CallbackGroup group1 = new CallbackGroup();
-    final CallbackGroup group2 = new CallbackGroup();
-
-    CodeMirror.initLibrary(group1.add(new AsyncCallback<Void>() {
-      final AsyncCallback<Void> themeCallback = group2.addEmpty();
-
+  ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback(
+      final CommentsCollections comments) {
+    return new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide.this) {
       @Override
-      public void onSuccess(Void result) {
-        // Load theme after CM library to ensure theme can override CSS.
-        ThemeLoader.loadTheme(prefs.theme(), themeCallback);
+      protected void preDisplay(ConfigInfoCache.Entry result) {
+        commentManager = new SideBySideCommentManager(
+            SideBySide.this,
+            base, revision, path,
+            result.getCommentLinkProcessor(),
+            getChangeStatus().isOpen());
+        setTheme(result.getTheme());
+        display(comments);
+        header.setupPrevNextFiles(comments);
       }
-
-      @Override
-      public void onFailure(Throwable caught) {
-      }
-    }));
-
-    DiffApi.diff(revision, path)
-      .base(base)
-      .wholeFile()
-      .intraline(prefs.intralineDifference())
-      .ignoreWhitespace(prefs.ignoreWhitespace())
-      .get(group1.addFinal(new GerritCallback<DiffInfo>() {
-        final AsyncCallback<Void> modeInjectorCb = group2.addEmpty();
-
-        @Override
-        public void onSuccess(DiffInfo diffInfo) {
-          diff = diffInfo;
-          fileSize = bucketFileSize(diffInfo);
-
-          if (prefs.syntaxHighlighting()) {
-            if (fileSize.compareTo(FileSize.SMALL) > 0) {
-              modeInjectorCb.onSuccess(null);
-            } else {
-              injectMode(diffInfo, modeInjectorCb);
-            }
-          } else {
-            modeInjectorCb.onSuccess(null);
-          }
-        }
-      }));
-
-    if (Gerrit.isSignedIn()) {
-      ChangeApi.edit(changeId.get(), group2.add(
-          new AsyncCallback<EditInfo>() {
-            @Override
-            public void onSuccess(EditInfo result) {
-              edit = result;
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-            }
-          }));
-    }
-
-    final CommentsCollections comments = new CommentsCollections();
-    comments.load(base, revision, path, group2);
-
-    RestApi call = ChangeApi.detail(changeId.get());
-    ChangeList.addOptions(call, EnumSet.of(
-        ListChangesOption.ALL_REVISIONS));
-    call.get(group2.add(new AsyncCallback<ChangeInfo>() {
-      @Override
-      public void onSuccess(ChangeInfo info) {
-        changeStatus = info.status();
-        info.revisions().copyKeysIntoChildren("name");
-        if (edit != null) {
-          edit.setName(edit.commit().commit());
-          info.setEdit(edit);
-          info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
-        }
-        String currentRevision = info.currentRevision();
-        boolean current = currentRevision != null &&
-            revision.get() == info.revision(currentRevision)._number();
-        JsArray<RevisionInfo> list = info.revisions().values();
-        RevisionInfo.sortRevisionInfoByNumber(list);
-        diffTable.set(prefs, list, diff, edit != null, current,
-            changeStatus.isOpen(), diff.binary());
-        header.setChangeInfo(info);
-      }
-
-      @Override
-      public void onFailure(Throwable caught) {
-      }
-    }));
-
-    ConfigInfoCache.get(changeId, group2.addFinal(
-        new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide.this) {
-          @Override
-          protected void preDisplay(ConfigInfoCache.Entry result) {
-            commentManager = new CommentManager(
-                SideBySide.this,
-                base, revision, path,
-                result.getCommentLinkProcessor(),
-                changeStatus.isOpen());
-            setTheme(result.getTheme());
-            display(comments);
-          }
-        }));
+    };
   }
 
   @Override
   public void onShowView() {
     super.onShowView();
-    Window.enableScrolling(false);
-    JumpKeys.enable(false);
-    if (prefs.hideTopMenu()) {
-      Gerrit.setHeaderVisible(false);
-    }
-    resizeHandler = Window.addResizeHandler(new ResizeHandler() {
-      @Override
-      public void onResize(ResizeEvent event) {
-        resizeCodeMirror();
-      }
-    });
 
     operation(new Runnable() {
       @Override
@@ -300,21 +115,21 @@
     setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
     diffTable.refresh();
 
-    if (startLine == 0) {
+    if (getStartLine() == 0) {
       DiffChunkInfo d = chunkManager.getFirst();
       if (d != null) {
         if (d.isEdit() && d.getSide() == DisplaySide.A) {
-          startSide = DisplaySide.B;
-          startLine = lineOnOther(d.getSide(), d.getStart()).getLine() + 1;
+          setStartSide(DisplaySide.B);
+          setStartLine(lineOnOther(d.getSide(), d.getStart()).getLine() + 1);
         } else {
-          startSide = d.getSide();
-          startLine = d.getStart() + 1;
+          setStartSide(d.getSide());
+          setStartLine(d.getStart() + 1);
         }
       }
     }
-    if (startSide != null && startLine > 0) {
-      CodeMirror cm = getCmFromSide(startSide);
-      cm.scrollToLine(startLine - 1);
+    if (getStartSide() != null && getStartLine() > 0) {
+      CodeMirror cm = getCmFromSide(getStartSide());
+      cm.scrollToLine(getStartLine() - 1);
       cm.focus();
     } else {
       cmA.setCursor(Pos.create(0));
@@ -327,266 +142,71 @@
   }
 
   @Override
-  protected void onUnload() {
-    super.onUnload();
+  void registerCmEvents(final CodeMirror cm) {
+    super.registerCmEvents(cm);
 
-    removeKeyHandlerRegistrations();
-    if (commentManager != null) {
-      CallbackGroup group = new CallbackGroup();
-      commentManager.saveAllDrafts(group);
-      group.done();
-    }
-    if (resizeHandler != null) {
-      resizeHandler.removeHandler();
-      resizeHandler = null;
-    }
-    if (cmA != null) {
-      cmA.getWrapperElement().removeFromParent();
-    }
-    if (cmB != null) {
-      cmB.getWrapperElement().removeFromParent();
-    }
-    if (prefsAction != null) {
-      prefsAction.hide();
-    }
-
-    Window.enableScrolling(true);
-    Gerrit.setHeaderVisible(true);
-    JumpKeys.enable(true);
-  }
-
-  private void removeKeyHandlerRegistrations() {
-    for (HandlerRegistration h : handlers) {
-      h.removeHandler();
-    }
-    handlers.clear();
-  }
-
-  private void registerCmEvents(final CodeMirror cm) {
-    cm.on("cursorActivity", updateActiveLine(cm));
-    cm.on("focus", updateActiveLine(cm));
     KeyMap keyMap = KeyMap.create()
-        .on("A", upToChange(true))
-        .on("U", upToChange(false))
-        .on("'['", header.navigate(Direction.PREV))
-        .on("']'", header.navigate(Direction.NEXT))
-        .on("R", header.toggleReviewed())
-        .on("O", commentManager.toggleOpenBox(cm))
-        .on("Enter", commentManager.toggleOpenBox(cm))
-        .on("N", maybeNextVimSearch(cm))
-        .on("E", openEditScreen(cm))
-        .on("P", chunkManager.diffChunkNav(cm, Direction.PREV))
         .on("Shift-A", diffTable.toggleA())
-        .on("Shift-M", header.reviewedAndNext())
-        .on("Shift-N", maybePrevVimSearch(cm))
-        .on("Shift-P", commentManager.commentNav(cm, Direction.PREV))
-        .on("Shift-O", commentManager.openCloseAll(cm))
         .on("Shift-Left", moveCursorToSide(cm, DisplaySide.A))
-        .on("Shift-Right", moveCursorToSide(cm, DisplaySide.B))
-        .on("I", new Runnable() {
-          @Override
-          public void run() {
-            switch (getIntraLineStatus()) {
-              case OFF:
-              case OK:
-                toggleShowIntraline();
-                break;
-              default:
-                break;
-            }
-          }
-        })
-        .on("','", new Runnable() {
-          @Override
-          public void run() {
-            prefsAction.show();
-          }
-        })
-        .on("Shift-/", new Runnable() {
-          @Override
-          public void run() {
-            new ShowHelpCommand().onKeyPress(null);
-          }
-        })
-        .on("Space", new Runnable() {
-          @Override
-          public void run() {
-            cm.vim().handleKey("<C-d>");
-          }
-        })
-        .on("Shift-Space", new Runnable() {
-          @Override
-          public void run() {
-            cm.vim().handleKey("<C-u>");
-          }
-        })
-        .on("Ctrl-F", new Runnable() {
-          @Override
-          public void run() {
-            cm.vim().handleKey("/");
-          }
-        })
-        .on("Ctrl-A", new Runnable() {
-          @Override
-          public void run() {
-            cm.execCommand("selectAll");
-          }
-        });
-    if (revision.get() != 0) {
-      cm.on("beforeSelectionChange", onSelectionChange(cm));
-      cm.on("gutterClick", onGutterClick(cm));
-      keyMap.on("C", commentManager.insertNewDraft(cm));
-    }
+        .on("Shift-Right", moveCursorToSide(cm, DisplaySide.B));
     cm.addKeyMap(keyMap);
-    if (renderEntireFile()) {
-      cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
-    }
-  }
-
-  private BeforeSelectionChangeHandler onSelectionChange(final CodeMirror cm) {
-    return new BeforeSelectionChangeHandler() {
-      private InsertCommentBubble bubble;
-
-      @Override
-      public void handle(CodeMirror cm, Pos anchor, Pos head) {
-        if (anchor.equals(head)) {
-          if (bubble != null) {
-            bubble.setVisible(false);
-          }
-          return;
-        } else if (bubble == null) {
-          init(anchor);
-        } else {
-          bubble.setVisible(true);
-        }
-        bubble.position(cm.charCoords(head, "local"));
-      }
-
-      private void init(Pos anchor) {
-        bubble = new InsertCommentBubble(commentManager, cm);
-        add(bubble);
-        cm.addWidget(anchor, bubble.getElement());
-      }
-    };
+    maybeRegisterRenderEntireFileKeyMap(cm);
   }
 
   @Override
   public void registerKeys() {
     super.registerKeys();
 
-    keysNavigation.add(new UpToChangeCommand(revision, 0, 'u'));
-    keysNavigation.add(
+    getKeysNavigation().add(
         new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_LEFT, PatchUtil.C.focusSideA()),
         new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_RIGHT, PatchUtil.C.focusSideB()));
-    keysNavigation.add(
-        new NoOpKeyCommand(0, 'j', PatchUtil.C.lineNext()),
-        new NoOpKeyCommand(0, 'k', PatchUtil.C.linePrev()));
-    keysNavigation.add(
-        new NoOpKeyCommand(0, 'n', PatchUtil.C.chunkNext2()),
-        new NoOpKeyCommand(0, 'p', PatchUtil.C.chunkPrev2()));
-    keysNavigation.add(
-        new NoOpKeyCommand(KeyCommand.M_SHIFT, 'n', PatchUtil.C.commentNext()),
-        new NoOpKeyCommand(KeyCommand.M_SHIFT, 'p', PatchUtil.C.commentPrev()));
-    keysNavigation.add(
-        new NoOpKeyCommand(KeyCommand.M_CTRL, 'f', Gerrit.C.keySearch()));
-
-    keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
-    keysAction.add(new NoOpKeyCommand(0, 'e', PatchUtil.C.openEditScreen()));
-    keysAction.add(new NoOpKeyCommand(0, KeyCodes.KEY_ENTER,
-        PatchUtil.C.expandComment()));
-    keysAction.add(new NoOpKeyCommand(0, 'o', PatchUtil.C.expandComment()));
-    keysAction.add(new NoOpKeyCommand(
-        KeyCommand.M_SHIFT, 'o', PatchUtil.C.expandAllCommentsOnCurrentLine()));
-    if (Gerrit.isSignedIn()) {
-      keysAction.add(new KeyCommand(0, 'r', PatchUtil.C.toggleReviewed()) {
-        @Override
-        public void onKeyPress(KeyPressEvent event) {
-          header.toggleReviewed().run();
-        }
-      });
-    }
-    keysAction.add(new KeyCommand(
-        KeyCommand.M_SHIFT, 'm', PatchUtil.C.markAsReviewedAndGoToNext()) {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        header.reviewedAndNext().run();
-      }
-    });
-    keysAction.add(new KeyCommand(0, 'a', PatchUtil.C.openReply()) {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        upToChange(true).run();
-      }
-    });
-    keysAction.add(new KeyCommand(
+    getKeysAction().add(new KeyCommand(
         KeyCommand.M_SHIFT, 'a', PatchUtil.C.toggleSideA()) {
       @Override
       public void onKeyPress(KeyPressEvent event) {
         diffTable.toggleA().run();
       }
     });
-    keysAction.add(new KeyCommand(0, ',', PatchUtil.C.showPreferences()) {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        prefsAction.show();
-      }
-    });
-    if (getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF
-        || getIntraLineStatus() == DiffInfo.IntraLineStatus.OK) {
-      keysAction.add(new KeyCommand(0, 'i', PatchUtil.C.toggleIntraline()) {
-        @Override
-        public void onKeyPress(KeyPressEvent event) {
-          toggleShowIntraline();
-        }
-      });
-    }
 
-    if (Gerrit.isSignedIn()) {
-      keysAction.add(new NoOpKeyCommand(0, 'c', PatchUtil.C.commentInsert()));
-      keysComment = new KeyCommandSet(PatchUtil.C.commentEditorSet());
-      keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 's',
-          PatchUtil.C.commentSaveDraft()));
-      keysComment.add(new NoOpKeyCommand(0, KeyCodes.KEY_ESCAPE,
-          PatchUtil.C.commentCancelEdit()));
-    } else {
-      keysComment = null;
-    }
+    registerHandlers();
+  }
 
-    removeKeyHandlerRegistrations();
-    handlers.add(GlobalKey.add(this, keysAction));
-    handlers.add(GlobalKey.add(this, keysNavigation));
-    if (keysComment != null) {
-      handlers.add(GlobalKey.add(this, keysComment));
-    }
-    handlers.add(ShowHelpCommand.addFocusHandler(new FocusHandler() {
+  @Override
+  FocusHandler getFocusHandler() {
+    return new FocusHandler() {
       @Override
       public void onFocus(FocusEvent event) {
         cmB.focus();
       }
-    }));
+    };
   }
 
   private void display(final CommentsCollections comments) {
+    final DiffInfo diff = getDiff();
     setThemeStyles(prefs.theme().isDark());
     setShowIntraline(prefs.intralineDifference());
     if (prefs.showLineNumbers()) {
-      diffTable.addStyleName(DiffTable.style.showLineNumbers());
+      diffTable.addStyleName(Resources.I.diffTableStyle().showLineNumbers());
     }
 
-    cmA = newCM(diff.metaA(), diff.textA(), diffTable.cmA);
-    cmB = newCM(diff.metaB(), diff.textB(), diffTable.cmB);
+    cmA = newCm(diff.metaA(), diff.textA(), diffTable.cmA);
+    cmB = newCm(diff.metaB(), diff.textB(), diffTable.cmB);
+
+    boolean reviewingBase = base == null;
+    getDiffTable().setUpBlameIconA(cmA, reviewingBase,
+        reviewingBase ? revision : base, path);
+    getDiffTable().setUpBlameIconB(cmB, revision, path);
 
     cmA.extras().side(DisplaySide.A);
     cmB.extras().side(DisplaySide.B);
     setShowTabs(prefs.showTabs());
 
-    chunkManager = new ChunkManager(this, cmA, cmB, diffTable.scrollbar);
-    skipManager = new SkipManager(this, commentManager);
+    chunkManager = new SideBySideChunkManager(this, cmA, cmB, diffTable.scrollbar);
 
     operation(new Runnable() {
       @Override
       public void run() {
-        // Estimate initial CM3 height, fixed up in onShowView.
+        // Estimate initial CodeMirror height, fixed up in onShowView.
         int height = Window.getClientHeight()
             - (Gerrit.getHeaderFooterHeight() + 18);
         cmA.setHeight(height);
@@ -601,26 +221,16 @@
     registerCmEvents(cmA);
     registerCmEvents(cmB);
     scrollSynchronizer = new ScrollSynchronizer(diffTable, cmA, cmB,
-            chunkManager.getLineMapper());
+            chunkManager.lineMapper);
 
-    prefsAction = new PreferencesAction(this, prefs);
-    header.init(prefsAction, getLinks(), diff.sideBySideWebLinks());
+    setPrefsAction(new PreferencesAction(this, prefs));
+    header.init(getPrefsAction(), getUnifiedDiffLink(), diff.sideBySideWebLinks());
     scrollSynchronizer.setAutoHideDiffTableHeader(prefs.autoHideDiffTableHeader());
 
-    if (prefs.syntaxHighlighting() && fileSize.compareTo(FileSize.SMALL) > 0) {
-      Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
-        @Override
-        public boolean execute() {
-          if (prefs.syntaxHighlighting() && isAttached()) {
-            setSyntaxHighlighting(prefs.syntaxHighlighting());
-          }
-          return false;
-        }
-      }, 250);
-    }
+    setupSyntaxHighlighting();
   }
 
-  private List<InlineHyperlink> getLinks() {
+  private List<InlineHyperlink> getUnifiedDiffLink() {
     InlineHyperlink toUnifiedDiffLink = new InlineHyperlink();
     toUnifiedDiffLink.setHTML(
         new ImageResourceRenderer().render(Gerrit.RESOURCES.unifiedDiff()));
@@ -630,90 +240,41 @@
     return Collections.singletonList(toUnifiedDiffLink);
   }
 
-  private CodeMirror newCM(
+  @Override
+  CodeMirror newCm(
       DiffInfo.FileMeta meta,
       String contents,
       Element parent) {
     return CodeMirror.create(parent, Configuration.create()
-      .set("readOnly", true)
       .set("cursorBlinkRate", prefs.cursorBlinkRate())
       .set("cursorHeight", 0.85)
-      .set("lineNumbers", prefs.showLineNumbers())
-      .set("tabSize", prefs.tabSize())
-      .set("mode", fileSize == FileSize.SMALL ? getContentType(meta) : null)
-      .set("lineWrapping", prefs.lineWrapping())
-      .set("scrollbarStyle", "overlay")
-      .set("styleSelectedText", true)
-      .set("showTrailingSpace", prefs.showWhitespaceErrors())
+      .set("inputStyle", "textarea")
       .set("keyMap", "vim_ro")
+      .set("lineNumbers", prefs.showLineNumbers())
+      .set("matchBrackets", prefs.matchBrackets())
+      .set("lineWrapping", prefs.lineWrapping())
+      .set("mode", getFileSize() == FileSize.SMALL ? getContentType(meta) : null)
+      .set("readOnly", true)
+      .set("scrollbarStyle", "overlay")
+      .set("showTrailingSpace", prefs.showWhitespaceErrors())
+      .set("styleSelectedText", true)
+      .set("tabSize", prefs.tabSize())
       .set("theme", prefs.theme().name().toLowerCase())
       .set("value", meta != null ? contents : "")
       .set("viewportMargin", renderEntireFile() ? POSITIVE_INFINITY : 10));
   }
 
-  DiffInfo.IntraLineStatus getIntraLineStatus() {
-    return diff.intralineStatus();
-  }
-
-  boolean renderEntireFile() {
-    return prefs.renderEntireFile() && canRenderEntireFile(prefs);
-  }
-
-  boolean canRenderEntireFile(DiffPreferences prefs) {
-    // CodeMirror is too slow to layout an entire huge file.
-    return fileSize.compareTo(FileSize.HUGE) < 0
-        || (prefs.context() != WHOLE_FILE_CONTEXT && prefs.context() < 100);
-  }
-
-  String getContentType() {
-    return getContentType(diff.metaB());
-  }
-
-  void setThemeStyles(boolean d) {
-    if (d) {
-      diffTable.addStyleName(DiffTable.style.dark());
-    } else {
-      diffTable.removeStyleName(DiffTable.style.dark());
-    }
-  }
-
-  void setShowTabs(boolean show) {
-    cmA.extras().showTabs(show);
-    cmB.extras().showTabs(show);
-  }
-
-  void setLineLength(int columns) {
-    cmA.extras().lineLength(columns);
-    cmB.extras().lineLength(columns);
-  }
-
+  @Override
   void setShowLineNumbers(boolean b) {
+    super.setShowLineNumbers(b);
+
     cmA.setOption("lineNumbers", b);
     cmB.setOption("lineNumbers", b);
-    if (b) {
-      diffTable.addStyleName(DiffTable.style.showLineNumbers());
-    } else {
-      diffTable.removeStyleName(DiffTable.style.showLineNumbers());
-    }
   }
 
-  void setShowIntraline(boolean b) {
-    if (b && getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF) {
-      reloadDiffInfo();
-    } else if (b) {
-      diffTable.removeStyleName(DiffTable.style.noIntraline());
-    } else {
-      diffTable.addStyleName(DiffTable.style.noIntraline());
-    }
-  }
-
-  private void toggleShowIntraline() {
-    prefs.intralineDifference(!prefs.intralineDifference());
-    setShowIntraline(prefs.intralineDifference());
-    prefsAction.update();
-  }
-
+  @Override
   void setSyntaxHighlighting(boolean b) {
+    final DiffInfo diff = getDiff();
     if (b) {
       injectMode(diff, new AsyncCallback<Void>() {
         @Override
@@ -735,39 +296,27 @@
     }
   }
 
-  void setContext(final int context) {
-    operation(new Runnable() {
-      @Override
-      public void run() {
-        skipManager.removeAll();
-        skipManager.render(context, diff);
-        updateRenderEntireFile();
-      }
-    });
-  }
-
+  @Override
   void setAutoHideDiffHeader(boolean hide) {
     scrollSynchronizer.setAutoHideDiffTableHeader(hide);
   }
 
-  private void render(DiffInfo diff) {
-    header.setNoDiff(diff);
-    chunkManager.render(diff);
-  }
-
   CodeMirror otherCm(CodeMirror me) {
     return me == cmA ? cmB : cmA;
   }
 
+  @Override
   CodeMirror getCmFromSide(DisplaySide side) {
     return side == DisplaySide.A ? cmA : cmB;
   }
 
-  LineOnOtherInfo lineOnOther(DisplaySide side, int line) {
-    return chunkManager.getLineMapper().lineOnOther(side, line);
+  @Override
+  int getCmLine(int line, DisplaySide side) {
+    return line;
   }
 
-  private Runnable updateActiveLine(final CodeMirror cm) {
+  @Override
+  Runnable updateActiveLine(final CodeMirror cm) {
     final CodeMirror other = otherCm(cm);
     return new Runnable() {
       @Override
@@ -804,50 +353,6 @@
     };
   }
 
-  private GutterClickHandler onGutterClick(final CodeMirror cm) {
-    return new GutterClickHandler() {
-      @Override
-      public void handle(CodeMirror instance, final int line, String gutter,
-          NativeEvent clickEvent) {
-        if (clickEvent.getButton() == NativeEvent.BUTTON_LEFT
-            && !clickEvent.getMetaKey()
-            && !clickEvent.getAltKey()
-            && !clickEvent.getCtrlKey()
-            && !clickEvent.getShiftKey()) {
-          cm.setCursor(Pos.create(line));
-          Scheduler.get().scheduleDeferred(new ScheduledCommand() {
-            @Override
-            public void execute() {
-              commentManager.newDraft(cm, line + 1);
-            }
-          });
-        }
-      }
-    };
-  }
-
-  private Runnable upToChange(final boolean openReplyBox) {
-    return new Runnable() {
-      @Override
-      public void run() {
-        CallbackGroup group = new CallbackGroup();
-        commentManager.saveAllDrafts(group);
-        group.done();
-        group.addListener(new GerritCallback<Void>() {
-          @Override
-          public void onSuccess(Void result) {
-            String b = base != null ? base.getId() : null;
-            String rev = revision.getId();
-            Gerrit.display(
-              PageLinks.toChange(changeId, b, rev),
-              new ChangeScreen(changeId, b, rev, openReplyBox,
-                  FileTable.Mode.REVIEW));
-          }
-        });
-      }
-    };
-  }
-
   private Runnable moveCursorToSide(final CodeMirror cmSrc, DisplaySide sideDst) {
     final CodeMirror cmDst = getCmFromSide(sideDst);
     if (cmDst == cmSrc) {
@@ -872,156 +377,13 @@
     };
   }
 
-  private Runnable maybePrevVimSearch(final CodeMirror cm) {
-    return new Runnable() {
-      @Override
-      public void run() {
-        if (cm.vim().hasSearchHighlight()) {
-          cm.vim().handleKey("N");
-        } else {
-          commentManager.commentNav(cm, Direction.NEXT).run();
-        }
-      }
-    };
-  }
-
-  private Runnable maybeNextVimSearch(final CodeMirror cm) {
-    return new Runnable() {
-      @Override
-      public void run() {
-        if (cm.vim().hasSearchHighlight()) {
-          cm.vim().handleKey("n");
-        } else {
-          chunkManager.diffChunkNav(cm, Direction.NEXT).run();
-        }
-      }
-    };
-  }
-
-  private int adjustCommitMessageLine(int line) {
-    /* When commit messages are shown in the side-by-side screen they include
-      a header block that looks like this:
-
-      1 Parent:     deadbeef (Parent commit title)
-      2 Author:     A. U. Thor <author@example.com>
-      3 AuthorDate: 2015-02-27 19:20:52 +0900
-      4 Commit:     A. U. Thor <author@example.com>
-      5 CommitDate: 2015-02-27 19:20:52 +0900
-      6 [blank line]
-      7 Commit message title
-      8
-      9 Commit message body
-     10 ...
-     11 ...
-
-    If the commit is a merge commit, both parent commits are listed in the
-    first two lines instead of a 'Parent' line:
-
-      1 Merge Of:   deadbeef (Parent 1 commit title)
-      2             beefdead (Parent 2 commit title)
-
-    */
-
-    // Offset to compensate for header lines until the blank line
-    // after 'CommitDate'
-    int offset = 6;
-
-    // Adjust for merge commits, which have two parent lines
-    if (diff.textB().startsWith("Merge")) {
-      offset += 1;
-    }
-
-    // If the cursor is inside the header line, reset to the first line of the
-    // commit message. Otherwise if the cursor is on an actual line of the commit
-    // message, adjust the line number to compensate for the header lines, so the
-    // focus is on the correct line.
-    if (line <= offset) {
-      return 1;
-    } else {
-      return line - offset;
-    }
-  }
-
-  private Runnable openEditScreen(final CodeMirror cm) {
-    return new Runnable() {
-      @Override
-      public void run() {
-        LineHandle handle = cm.extras().activeLine();
-        int line = cm.getLineNumber(handle) + 1;
-        if (Patch.COMMIT_MSG.equals(path)) {
-          line = adjustCommitMessageLine(line);
-        }
-        String token = Dispatcher.toEditScreen(revision, path, line);
-        if (!Gerrit.isSignedIn()) {
-          Gerrit.doSignIn(token);
-        } else {
-          Gerrit.display(token);
-        }
-      }
-    };
-  }
-
-  void updateRenderEntireFile() {
-    cmA.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
-    cmB.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
-
-    boolean entireFile = renderEntireFile();
-    if (entireFile) {
-      cmA.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
-      cmB.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
-    }
-    cmA.setOption("viewportMargin", entireFile ? POSITIVE_INFINITY : 10);
-    cmB.setOption("viewportMargin", entireFile ? POSITIVE_INFINITY : 10);
-  }
-
-  void resizeCodeMirror() {
-    int hdr = header.getOffsetHeight() + diffTable.getHeaderHeight();
-    cmA.adjustHeight(hdr);
-    cmB.adjustHeight(hdr);
-  }
-
   void syncScroll(DisplaySide masterSide) {
     if (scrollSynchronizer != null) {
       scrollSynchronizer.syncScroll(masterSide);
     }
   }
 
-  private String getContentType(DiffInfo.FileMeta meta) {
-    if (prefs.syntaxHighlighting() && meta != null
-        && meta.contentType() != null) {
-     ModeInfo m = ModeInfo.findMode(meta.contentType(), path);
-     return m != null ? m.mime() : null;
-   }
-   return null;
-  }
-
-  private void injectMode(DiffInfo diffInfo, AsyncCallback<Void> cb) {
-    new ModeInjector()
-      .add(getContentType(diffInfo.metaA()))
-      .add(getContentType(diffInfo.metaB()))
-      .inject(cb);
-  }
-
-  String getPath() {
-    return path;
-  }
-
-  DiffPreferences getPrefs() {
-    return prefs;
-  }
-
-  ChunkManager getChunkManager() {
-    return chunkManager;
-  }
-
-  CommentManager getCommentManager() {
-    return commentManager;
-  }
-
-  SkipManager getSkipManager() {
-    return skipManager;
-  }
-
+  @Override
   void operation(final Runnable apply) {
     cmA.operation(new Runnable() {
       @Override
@@ -1036,70 +398,33 @@
     });
   }
 
-  private void prefetchNextFile() {
-    String nextPath = header.getNextPath();
-    if (nextPath != null) {
-      DiffApi.diff(revision, nextPath)
-        .base(base)
-        .wholeFile()
-        .intraline(prefs.intralineDifference())
-        .ignoreWhitespace(prefs.ignoreWhitespace())
-        .get(new AsyncCallback<DiffInfo>() {
-          @Override
-          public void onSuccess(DiffInfo info) {
-            new ModeInjector()
-              .add(getContentType(info.metaA()))
-              .add(getContentType(info.metaB()))
-              .inject(CallbackGroup.<Void> emptyCallback());
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-          }
-        });
-    }
+  @Override
+  CodeMirror[] getCms() {
+    return new CodeMirror[]{cmA, cmB};
   }
 
-  void reloadDiffInfo() {
-    final int id = ++reloadVersionId;
-    DiffApi.diff(revision, path)
-      .base(base)
-      .wholeFile()
-      .intraline(prefs.intralineDifference())
-      .ignoreWhitespace(prefs.ignoreWhitespace())
-      .get(new GerritCallback<DiffInfo>() {
-        @Override
-        public void onSuccess(DiffInfo diffInfo) {
-          if (id == reloadVersionId && isAttached()) {
-            diff = diffInfo;
-            operation(new Runnable() {
-              @Override
-              public void run() {
-                skipManager.removeAll();
-                chunkManager.reset();
-                diffTable.scrollbar.removeDiffAnnotations();
-                setShowIntraline(prefs.intralineDifference());
-                render(diff);
-                chunkManager.adjustPadding();
-                skipManager.render(prefs.context(), diff);
-              }
-            });
-          }
-        }
-      });
+  @Override
+  SideBySideTable getDiffTable() {
+    return diffTable;
   }
 
-  private static FileSize bucketFileSize(DiffInfo diff) {
-    FileMeta a = diff.metaA();
-    FileMeta b = diff.metaB();
-    FileSize[] sizes = FileSize.values();
-    for (int i = sizes.length - 1; 0 <= i; i--) {
-      FileSize s = sizes[i];
-      if ((a != null && s.lines <= a.lines())
-          || (b != null && s.lines <= b.lines())) {
-        return s;
-      }
-    }
-    return FileSize.SMALL;
+  @Override
+  SideBySideChunkManager getChunkManager() {
+    return chunkManager;
+  }
+
+  @Override
+  SideBySideCommentManager getCommentManager() {
+    return commentManager;
+  }
+
+  @Override
+  boolean isSideBySide() {
+    return true;
+  }
+
+  @Override
+  String getLineNumberClassName() {
+    return LINE_NUMBER_CLASSNAME;
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
index a4c2eb9..55c9de0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
@@ -25,6 +25,6 @@
   </ui:style>
   <g:FlowPanel styleName='{style.sbs}'>
     <d:Header ui:field='header'/>
-    <d:DiffTable ui:field='diffTable'/>
+    <d:SideBySideTable ui:field='diffTable'/>
   </g:FlowPanel>
 </ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java
new file mode 100644
index 0000000..11b7e7c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java
@@ -0,0 +1,275 @@
+// Copyright (C) 2013 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.diff;
+
+import static com.google.gerrit.client.diff.DisplaySide.A;
+import static com.google.gerrit.client.diff.DisplaySide.B;
+
+import com.google.gerrit.client.diff.DiffInfo.Region;
+import com.google.gerrit.client.diff.DiffInfo.Span;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.EventListener;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.CodeMirror.LineClassWhere;
+import net.codemirror.lib.Configuration;
+import net.codemirror.lib.LineWidget;
+import net.codemirror.lib.Pos;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Colors modified regions for {@link SideBySide}. */
+class SideBySideChunkManager extends ChunkManager {
+  private static final String DATA_LINES = "_cs2h";
+  private static double guessedLineHeightPx = 15;
+  private static final JavaScriptObject focusA = initOnClick(A);
+  private static final JavaScriptObject focusB = initOnClick(B);
+  private static native JavaScriptObject initOnClick(DisplaySide s) /*-{
+    return $entry(function(e){
+      @com.google.gerrit.client.diff.SideBySideChunkManager::focus(
+        Lcom/google/gwt/dom/client/NativeEvent;
+        Lcom/google/gerrit/client/diff/DisplaySide;)(e,s)
+    });
+  }-*/;
+
+  private static void focus(NativeEvent event, DisplaySide side) {
+    Element e = Element.as(event.getEventTarget());
+    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
+      EventListener l = DOM.getEventListener(e);
+      if (l instanceof SideBySide) {
+        ((SideBySide) l).getCmFromSide(side).focus();
+        event.stopPropagation();
+      }
+    }
+  }
+
+  static void focusOnClick(Element e, DisplaySide side) {
+    onClick(e, side == A ? focusA : focusB);
+  }
+
+  private final SideBySide host;
+  private final CodeMirror cmA;
+  private final CodeMirror cmB;
+
+  private List<DiffChunkInfo> chunks;
+  private List<LineWidget> padding;
+  private List<Element> paddingDivs;
+
+  SideBySideChunkManager(SideBySide host,
+      CodeMirror cmA,
+      CodeMirror cmB,
+      Scrollbar scrollbar) {
+    super(scrollbar);
+
+    this.host = host;
+    this.cmA = cmA;
+    this.cmB = cmB;
+  }
+
+  @Override
+  DiffChunkInfo getFirst() {
+    return !chunks.isEmpty() ? chunks.get(0) : null;
+  }
+
+  @Override
+  void reset() {
+    super.reset();
+
+    for (LineWidget w : padding) {
+      w.clear();
+    }
+  }
+
+  @Override
+  void render(DiffInfo diff) {
+    super.render();
+
+    chunks = new ArrayList<>();
+    padding = new ArrayList<>();
+    paddingDivs = new ArrayList<>();
+
+    String diffColor = diff.metaA() == null || diff.metaB() == null
+        ? SideBySideTable.style.intralineBg()
+        : SideBySideTable.style.diff();
+
+    for (Region current : Natives.asList(diff.content())) {
+      if (current.ab() != null) {
+        lineMapper.appendCommon(current.ab().length());
+      } else if (current.skip() > 0) {
+        lineMapper.appendCommon(current.skip());
+      } else if (current.common()) {
+        lineMapper.appendCommon(current.b().length());
+      } else {
+        render(current, diffColor);
+      }
+    }
+
+    if (paddingDivs.isEmpty()) {
+      paddingDivs = null;
+    }
+  }
+
+  void adjustPadding() {
+    if (paddingDivs != null) {
+      double h = cmB.extras().lineHeightPx();
+      for (Element div : paddingDivs) {
+        int lines = div.getPropertyInt(DATA_LINES);
+        div.getStyle().setHeight(lines * h, Unit.PX);
+      }
+      for (LineWidget w : padding) {
+        w.changed();
+      }
+      paddingDivs = null;
+      guessedLineHeightPx = h;
+    }
+  }
+
+  private void render(Region region, String diffColor) {
+    int startA = lineMapper.getLineA();
+    int startB = lineMapper.getLineB();
+
+    JsArrayString a = region.a();
+    JsArrayString b = region.b();
+    int aLen = a != null ? a.length() : 0;
+    int bLen = b != null ? b.length() : 0;
+
+    String color = a == null || b == null
+        ? diffColor
+        : SideBySideTable.style.intralineBg();
+
+    colorLines(cmA, color, startA, aLen);
+    colorLines(cmB, color, startB, bLen);
+    markEdit(cmA, startA, a, region.editA());
+    markEdit(cmB, startB, b, region.editB());
+    addPadding(cmA, startA + aLen - 1, bLen - aLen);
+    addPadding(cmB, startB + bLen - 1, aLen - bLen);
+    addGutterTag(region, startA, startB);
+    lineMapper.appendReplace(aLen, bLen);
+
+    int endA = lineMapper.getLineA() - 1;
+    int endB = lineMapper.getLineB() - 1;
+    if (aLen > 0) {
+      addDiffChunk(cmB, endA, aLen, bLen > 0);
+    }
+    if (bLen > 0) {
+      addDiffChunk(cmA, endB, bLen, aLen > 0);
+    }
+  }
+
+  private void addGutterTag(Region region, int startA, int startB) {
+    if (region.a() == null) {
+      scrollbar.insert(cmB, startB, region.b().length());
+    } else if (region.b() == null) {
+      scrollbar.delete(cmA, cmB, startA, region.a().length());
+    } else {
+      scrollbar.edit(cmB, startB, region.b().length());
+    }
+  }
+
+  private void markEdit(CodeMirror cm, int startLine,
+      JsArrayString lines, JsArray<Span> edits) {
+    if (lines == null || edits == null) {
+      return;
+    }
+
+    EditIterator iter = new EditIterator(lines, startLine);
+    Configuration bg = Configuration.create()
+        .set("className", SideBySideTable.style.intralineBg())
+        .set("readOnly", true);
+
+    Configuration diff = Configuration.create()
+        .set("className", SideBySideTable.style.diff())
+        .set("readOnly", true);
+
+    Pos last = Pos.create(0, 0);
+    for (Span span : Natives.asList(edits)) {
+      Pos from = iter.advance(span.skip());
+      Pos to = iter.advance(span.mark());
+      if (from.line() == last.line()) {
+        getMarkers().add(cm.markText(last, from, bg));
+      } else {
+        getMarkers().add(cm.markText(Pos.create(from.line(), 0), from, bg));
+      }
+      getMarkers().add(cm.markText(from, to, diff));
+      last = to;
+      colorLines(cm, LineClassWhere.BACKGROUND,
+          SideBySideTable.style.diff(),
+          from.line(), to.line());
+    }
+  }
+
+  /**
+   * Insert a new padding div below the given line.
+   *
+   * @param cm parent CodeMirror to add extra space into.
+   * @param line line to put the padding below.
+   * @param len number of lines to pad. Padding is inserted only if
+   *        {@code len >= 1}.
+   */
+  private void addPadding(CodeMirror cm, int line, final int len) {
+    if (0 < len) {
+      Element pad = DOM.createDiv();
+      pad.setClassName(SideBySideTable.style.padding());
+      pad.setPropertyInt(DATA_LINES, len);
+      pad.getStyle().setHeight(guessedLineHeightPx * len, Unit.PX);
+      focusOnClick(pad, cm.side());
+      paddingDivs.add(pad);
+      padding.add(cm.addLineWidget(
+        line == -1 ? 0 : line,
+        pad,
+        Configuration.create()
+          .set("coverGutter", true)
+          .set("noHScroll", true)
+          .set("above", line == -1)));
+    }
+  }
+
+  private void addDiffChunk(CodeMirror cmToPad, int lineOnOther,
+      int chunkSize, boolean edit) {
+    chunks.add(new DiffChunkInfo(host.otherCm(cmToPad).side(),
+        lineOnOther - chunkSize + 1, lineOnOther, edit));
+  }
+
+  @Override
+  Runnable diffChunkNav(final CodeMirror cm, final Direction dir) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        int line = cm.extras().hasActiveLine()
+            ? cm.getLineNumber(cm.extras().activeLine())
+            : 0;
+        int res = Collections.binarySearch(
+                chunks,
+                new DiffChunkInfo(cm.side(), line, 0, false),
+                getDiffChunkComparator());
+        diffChunkNavHelper(chunks, host, res, dir);
+      }
+    };
+  }
+
+  @Override
+  int getCmLine(int line, DisplaySide side) {
+    return line;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java
new file mode 100644
index 0000000..db72864
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java
@@ -0,0 +1,163 @@
+// Copyright (C) 2013 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.diff;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Timer;
+
+import net.codemirror.lib.CodeMirror;
+
+import java.util.PriorityQueue;
+
+/**
+ * LineWidget attached to a CodeMirror container.
+ *
+ * When a comment is placed on a line a CommentWidget is created on both sides.
+ * The group tracks all comment boxes on that same line, and also includes an
+ * empty padding element to keep subsequent lines vertically aligned.
+ */
+class SideBySideCommentGroup extends CommentGroup
+    implements Comparable<SideBySideCommentGroup> {
+  static void pair(SideBySideCommentGroup a, SideBySideCommentGroup b) {
+    a.peers.add(b);
+    b.peers.add(a);
+  }
+
+  private final Element padding;
+  private final PriorityQueue<SideBySideCommentGroup> peers;
+
+  SideBySideCommentGroup(SideBySideCommentManager manager, CodeMirror cm, DisplaySide side,
+      int line) {
+    super(manager, cm, side, line);
+
+    padding = DOM.createDiv();
+    padding.setClassName(SideBySideTable.style.padding());
+    SideBySideChunkManager.focusOnClick(padding, cm.side());
+    getElement().appendChild(padding);
+    peers = new PriorityQueue<>();
+  }
+
+  SideBySideCommentGroup getPeer() {
+    return peers.peek();
+  }
+
+  @Override
+  void remove(DraftBox box) {
+    super.remove(box);
+
+    if (getBoxCount() == 0 && peers.size() == 1
+        && peers.peek().peers.size() > 1) {
+      SideBySideCommentGroup peer = peers.peek();
+      peer.peers.remove(this);
+      detach();
+      if (peer.getBoxCount() == 0 && peer.peers.size() == 1
+          && peer.peers.peek().getBoxCount() == 0) {
+        peer.detach();
+      } else {
+        peer.resize();
+      }
+    } else {
+      resize();
+    }
+  }
+
+  @Override
+  void init(DiffTable parent) {
+    if (getLineWidget() == null) {
+      attach(parent);
+    }
+    for (CommentGroup peer : peers) {
+      if (peer.getLineWidget() == null) {
+        peer.attach(parent);
+      }
+    }
+  }
+
+  @Override
+  void handleRedraw() {
+    getLineWidget().onRedraw(new Runnable() {
+      @Override
+      public void run() {
+        if (canComputeHeight() && peers.peek().canComputeHeight()) {
+          if (getResizeTimer() != null) {
+            getResizeTimer().cancel();
+            setResizeTimer(null);
+          }
+          adjustPadding(SideBySideCommentGroup.this, peers.peek());
+        } else if (getResizeTimer() == null) {
+          setResizeTimer(new Timer() {
+            @Override
+            public void run() {
+              if (canComputeHeight() && peers.peek().canComputeHeight()) {
+                cancel();
+                setResizeTimer(null);
+                adjustPadding(SideBySideCommentGroup.this, peers.peek());
+              }
+            }
+          });
+          getResizeTimer().scheduleRepeating(5);
+        }
+      }
+    });
+  }
+
+  @Override
+  void resize() {
+    if (getLineWidget() != null) {
+      adjustPadding(this, peers.peek());
+    }
+  }
+
+  private int computeHeight() {
+    if (getComments().isVisible()) {
+      // Include margin-bottom: 5px from CSS class.
+      return getComments().getOffsetHeight() + 5;
+    }
+    return 0;
+  }
+
+  private static void adjustPadding(SideBySideCommentGroup a, SideBySideCommentGroup b) {
+    int apx = a.computeHeight();
+    int bpx = b.computeHeight();
+    for (SideBySideCommentGroup otherPeer : a.peers) {
+      if (otherPeer != b) {
+        bpx += otherPeer.computeHeight();
+      }
+    }
+    for (SideBySideCommentGroup otherPeer : b.peers) {
+      if (otherPeer != a) {
+        apx += otherPeer.computeHeight();
+      }
+    }
+    int h = Math.max(apx, bpx);
+    a.padding.getStyle().setHeight(Math.max(0, h - apx), Unit.PX);
+    b.padding.getStyle().setHeight(Math.max(0, h - bpx), Unit.PX);
+    a.getLineWidget().changed();
+    b.getLineWidget().changed();
+    a.updateSelection();
+    b.updateSelection();
+  }
+
+  @Override
+  public int compareTo(SideBySideCommentGroup o) {
+    if (side == o.side) {
+      return line - o.line;
+    }
+    throw new IllegalStateException(
+        "Cannot compare SideBySideCommentGroup with different sides");
+  }
+}
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
new file mode 100644
index 0000000..bcb7dac
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
@@ -0,0 +1,131 @@
+// Copyright (C) 2013 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.diff;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.PatchSet;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.TextMarker.FromTo;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.SortedMap;
+
+/** Tracks comment widgets for {@link SideBySide}. */
+class SideBySideCommentManager extends CommentManager {
+  SideBySideCommentManager(SideBySide host,
+      PatchSet.Id base, PatchSet.Id revision,
+      String path,
+      CommentLinkProcessor clp,
+      boolean open) {
+    super(host, base, revision, path, clp, open);
+  }
+
+  @Override
+  SortedMap<Integer, CommentGroup> getMapForNav(DisplaySide side) {
+    return map(side);
+  }
+
+  @Override
+  void clearLine(DisplaySide side, int line, CommentGroup group) {
+    super.clearLine(side, line, group);
+  }
+
+  @Override
+  void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int line) {
+    if (!Gerrit.isSignedIn()) {
+      signInCallback(cm).run();
+    } else {
+      insertNewDraft(cm.side(), line);
+    }
+  }
+
+  @Override
+  CommentGroup getCommentGroupOnActiveLine(CodeMirror cm) {
+    CommentGroup group = null;
+    if (cm.extras().hasActiveLine()) {
+      group = map(cm.side())
+          .get(cm.getLineNumber(cm.extras().activeLine()) + 1);
+    }
+    return group;
+  }
+
+  @Override
+  Collection<Integer> getLinesWithCommentGroups() {
+    return sideB.tailMap(1).keySet();
+  }
+
+  @Override
+  String getTokenSuffixForActiveLine(CodeMirror cm) {
+    return (cm.side() == DisplaySide.A ? "a" : "")
+        + (cm.getLineNumber(cm.extras().activeLine()) + 1);
+  }
+
+  @Override
+  void newDraft(CodeMirror cm) {
+    int line = cm.getLineNumber(cm.extras().activeLine()) + 1;
+    if (cm.somethingSelected()) {
+      FromTo fromTo = adjustSelection(cm);
+      addDraftBox(cm.side(), CommentInfo.create(
+              getPath(),
+              getStoredSideFromDisplaySide(cm.side()),
+              getParentNumFromDisplaySide(cm.side()),
+              line,
+              CommentRange.create(fromTo))).setEdit(true);
+      cm.setCursor(fromTo.to());
+      cm.setSelection(cm.getCursor());
+    } else {
+      insertNewDraft(cm.side(), line);
+    }
+  }
+
+  @Override
+  CommentGroup group(DisplaySide side, int line) {
+    CommentGroup existing = map(side).get(line);
+    if (existing != null) {
+      return existing;
+    }
+
+    SideBySideCommentGroup newGroup = newGroup(side, line);
+    Map<Integer, CommentGroup> map =
+        side == DisplaySide.A ? sideA : sideB;
+    Map<Integer, CommentGroup> otherMap =
+        side == DisplaySide.A ? sideB : sideA;
+    map.put(line, newGroup);
+    int otherLine = host.lineOnOther(side, line - 1).getLine() + 1;
+    existing = map(side.otherSide()).get(otherLine);
+    CommentGroup otherGroup;
+    if (existing != null) {
+      otherGroup = existing;
+    } else {
+      otherGroup = newGroup(side.otherSide(), otherLine);
+      otherMap.put(otherLine, otherGroup);
+    }
+    SideBySideCommentGroup.pair(newGroup, (SideBySideCommentGroup) otherGroup);
+
+    if (isAttached()) {
+      newGroup.init(host.getDiffTable());
+      otherGroup.handleRedraw();
+    }
+    return newGroup;
+  }
+
+  private SideBySideCommentGroup newGroup(DisplaySide side, int line) {
+    return new SideBySideCommentGroup(this, host.getCmFromSide(side), side, line);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
new file mode 100644
index 0000000..2296796
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2013 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.diff;
+
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.HTMLPanel;
+
+/**
+ * A table with one row and two columns to hold the two CodeMirrors displaying
+ * the files to be compared.
+ */
+class SideBySideTable extends DiffTable {
+  interface Binder extends UiBinder<HTMLPanel, SideBySideTable> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  interface DiffTableStyle extends CssResource {
+    String intralineBg();
+    String diff();
+    String hideA();
+    String hideB();
+    String padding();
+  }
+
+  private SideBySide parent;
+  @UiField Element cmA;
+  @UiField Element cmB;
+  @UiField static DiffTableStyle style;
+
+  private boolean visibleA;
+
+  SideBySideTable(SideBySide parent, PatchSet.Id base, PatchSet.Id revision,
+      String path) {
+    super(parent, base, revision, path);
+
+    initWidget(uiBinder.createAndBindUi(this));
+    this.visibleA = true;
+    this.parent = parent;
+  }
+
+  @Override
+  boolean isVisibleA() {
+    return visibleA;
+  }
+
+  void setVisibleA(boolean show) {
+    visibleA = show;
+    if (show) {
+      removeStyleName(style.hideA());
+      parent.syncScroll(DisplaySide.B); // match B's viewport
+    } else {
+      addStyleName(style.hideA());
+    }
+  }
+
+  Runnable toggleA() {
+    return new Runnable() {
+      @Override
+      public void run() {
+        setVisibleA(!isVisibleA());
+      }
+    };
+  }
+
+  void setVisibleB(boolean show) {
+    if (show) {
+      removeStyleName(style.hideB());
+      parent.syncScroll(DisplaySide.A); // match A's viewport
+    } else {
+      addStyleName(style.hideB());
+    }
+  }
+
+  @Override
+  void setHideEmptyPane(boolean hide) {
+    if (getChangeType() == ChangeType.ADDED) {
+      setVisibleA(!hide);
+    } else if (getChangeType() == ChangeType.DELETED) {
+      setVisibleB(!hide);
+    }
+  }
+
+  @Override
+  SideBySide getDiffScreen() {
+    return parent;
+  }
+
+  @Override
+  int getHeaderHeight() {
+    int h = patchSetSelectBoxA.getOffsetHeight();
+    if (hasHeader()) {
+      h += diffHeaderRow.getOffsetHeight();
+    }
+    return h;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml
new file mode 100644
index 0000000..b2e3f43
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 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.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:d='urn:import:com.google.gerrit.client.diff'>
+  <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
+  <ui:style gss='false' type='com.google.gerrit.client.diff.SideBySideTable.DiffTableStyle'>
+    @external .CodeMirror, .CodeMirror-selectedtext;
+    @external .CodeMirror-linenumber;
+    @external .CodeMirror-overlayscroll-vertical, .CodeMirror-scroll;
+    @external .CodeMirror-dialog-bottom;
+    @external .CodeMirror-cursor;
+
+    @external .dark, .noIntraline, .showLineNumbers;
+
+    .difftable .patchSetNav,
+    .difftable .CodeMirror {
+      -webkit-touch-callout: none;
+      -webkit-user-select: none;
+      -khtml-user-select: none;
+      -moz-user-select: none;
+      -ms-user-select: none;
+    }
+
+    .difftable .CodeMirror pre {
+      overflow: visible;
+      border-right: 0;
+      width: auto;
+    }
+
+    /* Preserve space for underscores. If this changes
+     * see ChunkManager.addPadding() and adjust there.
+     */
+    .difftable .CodeMirror pre,
+    .difftable .CodeMirror pre span {
+      padding-bottom: 1px;
+    }
+
+    .hideA .psNavA,
+    .hideA .a {
+      display: none;
+    }
+
+    .hideB .psNavB,
+    .hideB .b {
+      display: none;
+    }
+
+    .table {
+      width: 100%;
+      table-layout: fixed;
+      border-spacing: 0;
+    }
+    .table td { padding: 0 }
+    .a, .b { width: 50% }
+    .hideA .psNavB, .hideA .b { width: 100% }
+    .hideB .psNavA, .hideB .a { width: 100% }
+
+    /* Hide scrollbars on A, B controls both views. */
+    .a .CodeMirror-scroll { margin-right: -36px; }
+    .a .CodeMirror-overlayscroll-vertical { display: none !important; }
+
+    .showLineNumbers .b { border-left: none; }
+    .b { border-left: 1px solid #ddd; }
+
+    .a .diff { background-color: #faa; }
+    /* Set min-width for lineWrapping to make sure it gets enough width
+       before lineWrapping and to make sure it dosent do a ugly line wrap */
+    .b .diff { background-color: #9f9; min-width: 60em; }
+    .a .intralineBg { background-color: #fee; }
+    .b .intralineBg { background-color: #dfd; }
+    .noIntraline .a .intralineBg { background-color: #faa; }
+    .noIntraline .b .intralineBg { background-color: #9f9; }
+
+    .dark .a .diff { background-color: #400; }
+    .dark .b .diff { background-color: #444; }
+
+    .dark .a .intralineBg { background-color: #888; }
+    .dark .b .intralineBg { background-color: #bbb; }
+    .dark .noIntraline .a .intralineBg { background-color: #400; }
+    .dark .noIntraline .b .intralineBg { background-color: #444; }
+
+    .patchSetNav, .diff_header {
+      background-color: #f7f7f7;
+      line-height: 1;
+    }
+
+    .difftable .CodeMirror-selectedtext {
+      background-color: inherit !important;
+    }
+    .difftable .CodeMirror-linenumber {
+      height: 1.11em;
+      cursor: pointer;
+    }
+    .difftable .CodeMirror div.CodeMirror-cursor {
+      border-left: 2px solid black;
+    }
+    .difftable .CodeMirror-dialog-bottom {
+      border-top: 0;
+      border-left: 1px solid #000;
+      border-bottom: 1px solid #000;
+      background-color: #f7f7f7;
+      top: 0;
+      right: 0;
+      bottom: auto;
+      left: auto;
+    }
+    .showLineNumbers .padding {
+      margin-left: 21px;
+      border-left: 2px solid #d64040;
+    }
+  </ui:style>
+  <g:HTMLPanel styleName='{style.difftable}'>
+    <table class='{style.table}'>
+      <tr ui:field='patchSetNavRow' class='{style.patchSetNav}'>
+        <td ui:field='patchSetNavCellA' class='{style.psNavA}'>
+          <d:PatchSetSelectBox ui:field='patchSetSelectBoxA' />
+        </td>
+        <td ui:field='patchSetNavCellB' class='{style.psNavB}'>
+          <d:PatchSetSelectBox ui:field='patchSetSelectBoxB' />
+        </td>
+      </tr>
+      <tr ui:field='diffHeaderRow' class='{res.diffTableStyle.diffHeader}'>
+        <td colspan='2'><pre ui:field='diffHeaderText' /></td>
+      </tr>
+      <tr>
+        <td ui:field='cmA' class='{style.a}' />
+        <td ui:field='cmB' class='{style.b}' />
+      </tr>
+    </table>
+    <g:FlowPanel ui:field='widgets' visible='false'/>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
index 258eec6..5f86955 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
@@ -34,7 +34,6 @@
 import net.codemirror.lib.TextMarker;
 import net.codemirror.lib.TextMarker.FromTo;
 
-/** The Widget that handles expanding of skipped lines */
 class SkipBar extends Composite {
   interface Binder extends UiBinder<HTMLPanel, SkipBar> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
@@ -45,9 +44,9 @@
     String noExpand();
   }
 
-  @UiField(provided=true) Anchor skipNum;
-  @UiField(provided=true) Anchor upArrow;
-  @UiField(provided=true) Anchor downArrow;
+  @UiField(provided = true) Anchor skipNum;
+  @UiField(provided = true) Anchor upArrow;
+  @UiField(provided = true) Anchor downArrow;
   @UiField SkipBarStyle style;
 
   private final SkipManager manager;
@@ -133,7 +132,10 @@
 
   void expandBefore(int cnt) {
     expandSideBefore(cnt);
-    otherBar.expandSideBefore(cnt);
+
+    if (otherBar != null) {
+      otherBar.expandSideBefore(cnt);
+    }
   }
 
   private void expandSideBefore(int cnt) {
@@ -177,26 +179,40 @@
   void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
     expandAll();
     updateSelection();
-    otherBar.updateSelection();
+    if (otherBar != null) {
+      otherBar.expandAll();
+      otherBar.updateSelection();
+    }
+    cm.refresh();
     cm.focus();
   }
 
   private void expandAll() {
     expandSideAll();
-    otherBar.expandSideAll();
+    if (otherBar != null) {
+      otherBar.expandSideAll();
+    }
     manager.remove(this, otherBar);
   }
 
   @UiHandler("upArrow")
   void onExpandBefore(@SuppressWarnings("unused") ClickEvent e) {
     expandBefore(NUM_ROWS_TO_EXPAND);
+    if (otherBar != null) {
+      otherBar.expandBefore(NUM_ROWS_TO_EXPAND);
+    }
+    cm.refresh();
     cm.focus();
   }
 
   @UiHandler("downArrow")
   void onExpandAfter(@SuppressWarnings("unused") ClickEvent e) {
     expandAfter();
-    otherBar.expandAfter();
+
+    if (otherBar != null) {
+      otherBar.expandAfter();
+    }
+    cm.refresh();
     cm.focus();
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
index 5376588..cf23694 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
@@ -26,16 +26,16 @@
 import java.util.List;
 import java.util.Set;
 
-/** Collapses common regions with {@link SkipBar} for {@link SideBySide}. */
+/** Collapses common regions with {@link SkipBar} for {@link SideBySide}
+ *  and {@link Unified}. */
 class SkipManager {
-  private final SideBySide host;
-  private final CommentManager commentManager;
-  private Set<SkipBar> skipBars;
+  private final Set<SkipBar> skipBars;
+  private final DiffScreen host;
   private SkipBar line0;
 
-  SkipManager(SideBySide host, CommentManager commentManager) {
+  SkipManager(DiffScreen host) {
     this.host = host;
-    this.commentManager = commentManager;
+    this.skipBars = new HashSet<>();
   }
 
   void render(int context, DiffInfo diff) {
@@ -43,10 +43,10 @@
       return;
     }
 
-    JsArray<Region> regions = diff.content();
     List<SkippedLine> skips = new ArrayList<>();
     int lineA = 0;
     int lineB = 0;
+    JsArray<Region> regions = diff.content();
     for (int i = 0; i < regions.length(); i++) {
       Region current = regions.get(i);
       if (current.ab() != null || current.common() || current.skip() > 0) {
@@ -69,33 +69,58 @@
         lineB += current.b() != null ? current.b().length() : 0;
       }
     }
-    skips = commentManager.splitSkips(context, skips);
+    skips = host.getCommentManager().splitSkips(context, skips);
+    renderSkips(skips, lineA, lineB);
+  }
 
+  private void renderSkips(List<SkippedLine> skips, int lineA, int lineB) {
     if (!skips.isEmpty()) {
-      CodeMirror cmA = host.getCmFromSide(DisplaySide.A);
+      boolean isSideBySide = host.isSideBySide();
+      CodeMirror cmA = null;
+      if (isSideBySide) {
+        cmA = host.getCmFromSide(DisplaySide.A);
+      }
       CodeMirror cmB = host.getCmFromSide(DisplaySide.B);
 
-      skipBars = new HashSet<>();
       for (SkippedLine skip : skips) {
-        SkipBar barA = newSkipBar(cmA, DisplaySide.A, skip);
+        SkipBar barA = null;
         SkipBar barB = newSkipBar(cmB, DisplaySide.B, skip);
-        SkipBar.link(barA, barB);
-        skipBars.add(barA);
         skipBars.add(barB);
+        if (isSideBySide) {
+          barA = newSkipBar(cmA, DisplaySide.A, skip);
+          SkipBar.link(barA, barB);
+          skipBars.add(barA);
+        }
 
         if (skip.getStartA() == 0 || skip.getStartB() == 0) {
-          barA.upArrow.setVisible(false);
+          if (isSideBySide) {
+            barA.upArrow.setVisible(false);
+          }
           barB.upArrow.setVisible(false);
-          line0 = barB;
+          setLine0(barB);
         } else if (skip.getStartA() + skip.getSize() == lineA
             || skip.getStartB() + skip.getSize() == lineB) {
-          barA.downArrow.setVisible(false);
+          if (isSideBySide) {
+            barA.downArrow.setVisible(false);
+          }
           barB.downArrow.setVisible(false);
         }
       }
     }
   }
 
+  private SkipBar newSkipBar(CodeMirror cm, DisplaySide side,
+      SkippedLine skip) {
+    int start = host.getCmLine(
+        side == DisplaySide.A ? skip.getStartA() : skip.getStartB(), side);
+    int end = start + skip.getSize() - 1;
+
+    SkipBar bar = new SkipBar(this, cm);
+    host.getDiffTable().add(bar);
+    bar.collapse(start, end, true);
+    return bar;
+  }
+
   void ensureFirstLineIsVisible() {
     if (line0 != null) {
       line0.expandBefore(1);
@@ -104,11 +129,10 @@
   }
 
   void removeAll() {
-    if (skipBars != null) {
+    if (!skipBars.isEmpty()) {
       for (SkipBar bar : skipBars) {
         bar.expandSideAll();
       }
-      skipBars = null;
       line0 = null;
     }
   }
@@ -116,21 +140,16 @@
   void remove(SkipBar a, SkipBar b) {
     skipBars.remove(a);
     skipBars.remove(b);
-    if (line0 == a || line0 == b) {
-      line0 = null;
-    }
-    if (skipBars.isEmpty()) {
-      skipBars = null;
+    if (getLine0() == a || getLine0() == b) {
+      setLine0(null);
     }
   }
 
-  private SkipBar newSkipBar(CodeMirror cm, DisplaySide side, SkippedLine skip) {
-    int start = side == DisplaySide.A ? skip.getStartA() : skip.getStartB();
-    int end = start + skip.getSize() - 1;
+  SkipBar getLine0() {
+    return line0;
+  }
 
-    SkipBar bar = new SkipBar(this, cm);
-    host.diffTable.add(bar);
-    bar.collapse(start, end, true);
-    return bar;
+  void setLine0(SkipBar bar) {
+    line0 = bar;
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
new file mode 100644
index 0000000..a231580
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
@@ -0,0 +1,396 @@
+// Copyright (C) 2013 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.diff;
+
+import static java.lang.Double.POSITIVE_INFINITY;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.diff.UnifiedChunkManager.LineRegionInfo;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.projects.ConfigInfoCache;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.ImageResourceRenderer;
+import com.google.gwt.user.client.ui.InlineHTML;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.CodeMirror.LineHandle;
+import net.codemirror.lib.Configuration;
+import net.codemirror.lib.Pos;
+import net.codemirror.lib.ScrollInfo;
+
+import java.util.Collections;
+import java.util.List;
+
+public class Unified extends DiffScreen {
+  interface Binder extends UiBinder<FlowPanel, Unified> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField(provided = true)
+  UnifiedTable diffTable;
+
+  private CodeMirror cm;
+
+  private UnifiedChunkManager chunkManager;
+  private UnifiedCommentManager commentManager;
+
+  private boolean autoHideDiffTableHeader;
+
+  public Unified(
+      PatchSet.Id base,
+      PatchSet.Id revision,
+      String path,
+      DisplaySide startSide,
+      int startLine) {
+    super(base, revision, path, startSide, startLine, DiffView.UNIFIED_DIFF);
+
+    diffTable = new UnifiedTable(this, base, revision, path);
+    add(uiBinder.createAndBindUi(this));
+    addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
+  }
+
+  @Override
+  ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback(
+      final CommentsCollections comments) {
+    return new ScreenLoadCallback<ConfigInfoCache.Entry>(Unified.this) {
+      @Override
+      protected void preDisplay(ConfigInfoCache.Entry result) {
+        commentManager = new UnifiedCommentManager(
+            Unified.this,
+            base, revision, path,
+            result.getCommentLinkProcessor(),
+            getChangeStatus().isOpen());
+        setTheme(result.getTheme());
+        display(comments);
+        header.setupPrevNextFiles(comments);
+      }
+    };
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+
+    operation(new Runnable() {
+      @Override
+      public void run() {
+        resizeCodeMirror();
+        cm.refresh();
+      }
+    });
+    setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
+    diffTable.refresh();
+
+    if (getStartLine() == 0) {
+      DiffChunkInfo d = chunkManager.getFirst();
+      if (d != null) {
+        if (d.isEdit() && d.getSide() == DisplaySide.A) {
+          setStartSide(DisplaySide.B);
+        } else {
+          setStartSide(d.getSide());
+        }
+        setStartLine(chunkManager.getCmLine(d.getStart(), d.getSide()) + 1);
+      }
+    }
+    if (getStartSide() != null && getStartLine() > 0) {
+      cm.scrollToLine(
+          chunkManager.getCmLine(getStartLine() - 1, getStartSide()));
+      cm.focus();
+    } else {
+      cm.setCursor(Pos.create(0));
+      cm.focus();
+    }
+    if (Gerrit.isSignedIn() && prefs.autoReview()) {
+      header.autoReview();
+    }
+    prefetchNextFile();
+  }
+
+  @Override
+  void registerCmEvents(final CodeMirror cm) {
+    super.registerCmEvents(cm);
+
+    cm.on("scroll", new Runnable() {
+      @Override
+      public void run() {
+        ScrollInfo si = cm.getScrollInfo();
+        if (autoHideDiffTableHeader) {
+          updateDiffTableHeader(si);
+        }
+      }
+    });
+    maybeRegisterRenderEntireFileKeyMap(cm);
+  }
+
+  @Override
+  public void registerKeys() {
+    super.registerKeys();
+
+    registerHandlers();
+  }
+
+  @Override
+  FocusHandler getFocusHandler() {
+    return new FocusHandler() {
+      @Override
+      public void onFocus(FocusEvent event) {
+        cm.focus();
+      }
+    };
+  }
+
+  private void display(final CommentsCollections comments) {
+    final DiffInfo diff = getDiff();
+    setThemeStyles(prefs.theme().isDark());
+    setShowIntraline(prefs.intralineDifference());
+    if (prefs.showLineNumbers()) {
+      diffTable.addStyleName(Resources.I.diffTableStyle().showLineNumbers());
+    }
+
+    cm = newCm(
+        diff.metaA() == null ? diff.metaB() : diff.metaA(),
+        diff.textUnified(),
+        diffTable.cm);
+    setShowTabs(prefs.showTabs());
+
+    chunkManager = new UnifiedChunkManager(this, cm, diffTable.scrollbar);
+
+    operation(new Runnable() {
+      @Override
+      public void run() {
+        // Estimate initial CodeMirror height, fixed up in onShowView.
+        int height = Window.getClientHeight()
+            - (Gerrit.getHeaderFooterHeight() + 18);
+        cm.setHeight(height);
+
+        render(diff);
+        commentManager.render(comments, prefs.expandAllComments());
+        skipManager.render(prefs.context(), diff);
+      }
+    });
+
+    registerCmEvents(cm);
+
+    setPrefsAction(new PreferencesAction(this, prefs));
+    header.init(getPrefsAction(), getSideBySideDiffLink(), diff.unifiedWebLinks());
+    setAutoHideDiffHeader(prefs.autoHideDiffTableHeader());
+
+    setupSyntaxHighlighting();
+  }
+
+  private List<InlineHyperlink> getSideBySideDiffLink() {
+    InlineHyperlink toSideBySideDiffLink = new InlineHyperlink();
+    toSideBySideDiffLink.setHTML(
+        new ImageResourceRenderer().render(Gerrit.RESOURCES.sideBySideDiff()));
+    toSideBySideDiffLink.setTargetHistoryToken(
+        Dispatcher.toSideBySide(base, revision, path));
+    toSideBySideDiffLink.setTitle(PatchUtil.C.sideBySideDiff());
+    return Collections.singletonList(toSideBySideDiffLink);
+  }
+
+  @Override
+  CodeMirror newCm(
+      DiffInfo.FileMeta meta,
+      String contents,
+      Element parent) {
+    JsArrayString gutters = JavaScriptObject.createArray().cast();
+    gutters.push(UnifiedTable.style.lineNumbersLeft());
+    gutters.push(UnifiedTable.style.lineNumbersRight());
+
+    return CodeMirror.create(parent, Configuration.create()
+        .set("cursorBlinkRate", prefs.cursorBlinkRate())
+        .set("cursorHeight", 0.85)
+        .set("gutters", gutters)
+        .set("inputStyle", "textarea")
+        .set("keyMap", "vim_ro")
+        .set("lineNumbers", false)
+        .set("lineWrapping", prefs.lineWrapping())
+        .set("matchBrackets", prefs.matchBrackets())
+        .set("mode", getFileSize() == FileSize.SMALL ? getContentType(meta) : null)
+        .set("readOnly", true)
+        .set("scrollbarStyle", "overlay")
+        .set("styleSelectedText", true)
+        .set("showTrailingSpace", prefs.showWhitespaceErrors())
+        .set("tabSize", prefs.tabSize())
+        .set("theme", prefs.theme().name().toLowerCase())
+        .set("value", meta != null ? contents : "")
+        .set("viewportMargin", renderEntireFile() ? POSITIVE_INFINITY : 10));
+  }
+
+  @Override
+  void setShowLineNumbers(boolean b) {
+    super.setShowLineNumbers(b);
+
+    cm.refresh();
+  }
+
+  private void setLineNumber(DisplaySide side, int cmLine, Integer line,
+      String styleName) {
+    SafeHtml html = SafeHtml.asis(line != null ? line.toString() : "&nbsp;");
+    InlineHTML gutter = new InlineHTML(html);
+    diffTable.add(gutter);
+    gutter.setStyleName(styleName);
+    cm.setGutterMarker(cmLine, side == DisplaySide.A
+        ? UnifiedTable.style.lineNumbersLeft()
+        : UnifiedTable.style.lineNumbersRight(), gutter.getElement());
+  }
+
+  void setLineNumber(DisplaySide side, int cmLine, int line) {
+    setLineNumber(side, cmLine, line, UnifiedTable.style.unifiedLineNumber());
+  }
+
+  void setLineNumberEmpty(DisplaySide side, int cmLine) {
+    setLineNumber(side, cmLine, null,
+        UnifiedTable.style.unifiedLineNumberEmpty());
+  }
+
+  @Override
+  void setSyntaxHighlighting(boolean b) {
+    final DiffInfo diff = getDiff();
+    if (b) {
+      injectMode(diff, new AsyncCallback<Void>() {
+        @Override
+        public void onSuccess(Void result) {
+          if (prefs.syntaxHighlighting()) {
+            cm.setOption("mode", getContentType(diff.metaA() == null
+                ? diff.metaB()
+                : diff.metaA()));
+          }
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+          prefs.syntaxHighlighting(false);
+        }
+      });
+    } else {
+      cm.setOption("mode", (String) null);
+    }
+  }
+
+  @Override
+  void setAutoHideDiffHeader(boolean autoHide) {
+    if (autoHide) {
+      updateDiffTableHeader(cm.getScrollInfo());
+    } else {
+      diffTable.setHeaderVisible(true);
+    }
+    autoHideDiffTableHeader = autoHide;
+  }
+
+  private void updateDiffTableHeader(ScrollInfo si) {
+    if (si.top() == 0) {
+      diffTable.setHeaderVisible(true);
+    } else if (si.top() > 0.5 * si.clientHeight()) {
+      diffTable.setHeaderVisible(false);
+    }
+  }
+
+  @Override
+  Runnable updateActiveLine(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        // The rendering of active lines has to be deferred. Reflow
+        // caused by adding and removing styles chokes Firefox when arrow
+        // key (or j/k) is held down. Performance on Chrome is fine
+        // without the deferral.
+        //
+        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+          @Override
+          public void execute() {
+            LineHandle handle =
+                cm.getLineHandleVisualStart(cm.getCursor("end").line());
+            cm.extras().activeLine(handle);
+          }
+        });
+      }
+    };
+  }
+
+  @Override
+  CodeMirror getCmFromSide(DisplaySide side) {
+    return cm;
+  }
+
+  @Override
+  int getCmLine(int line, DisplaySide side) {
+    return chunkManager.getCmLine(line, side);
+  }
+
+  LineRegionInfo getLineRegionInfoFromCmLine(int cmLine) {
+    return chunkManager.getLineRegionInfoFromCmLine(cmLine);
+  }
+
+  @Override
+  void operation(final Runnable apply) {
+    cm.operation(new Runnable() {
+      @Override
+      public void run() {
+        apply.run();
+      }
+    });
+  }
+
+  @Override
+  CodeMirror[] getCms() {
+    return new CodeMirror[] {cm};
+  }
+
+  @Override
+  UnifiedTable getDiffTable() {
+    return diffTable;
+  }
+
+  @Override
+  UnifiedChunkManager getChunkManager() {
+    return chunkManager;
+  }
+
+  @Override
+  UnifiedCommentManager getCommentManager() {
+    return commentManager;
+  }
+
+  @Override
+  boolean isSideBySide() {
+    return false;
+  }
+
+  @Override
+  String getLineNumberClassName() {
+    return UnifiedTable.style.unifiedLineNumber();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.ui.xml
new file mode 100644
index 0000000..85f46a6
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.ui.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 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.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:d='urn:import:com.google.gerrit.client.diff'>
+  <ui:style>
+    .unified {
+      margin-left: -5px;
+      margin-right: -5px;
+    }
+  </ui:style>
+  <g:FlowPanel styleName='{style.unified}'>
+    <d:Header ui:field='header'/>
+    <d:UnifiedTable ui:field='diffTable'/>
+  </g:FlowPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
new file mode 100644
index 0000000..74cf0df
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
@@ -0,0 +1,345 @@
+// Copyright (C) 2013 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.diff;
+
+import com.google.gerrit.client.diff.DiffInfo.Region;
+import com.google.gerrit.client.diff.DiffInfo.Span;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.EventListener;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.CodeMirror.LineClassWhere;
+import net.codemirror.lib.Configuration;
+import net.codemirror.lib.Pos;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/** Colors modified regions for {@link Unified}. */
+class UnifiedChunkManager extends ChunkManager {
+  private static final JavaScriptObject focus = initOnClick();
+  private static native JavaScriptObject initOnClick() /*-{
+    return $entry(function(e){
+      @com.google.gerrit.client.diff.UnifiedChunkManager::focus(
+        Lcom/google/gwt/dom/client/NativeEvent;)(e)
+    });
+  }-*/;
+
+  private List<UnifiedDiffChunkInfo> chunks;
+
+  @Override
+  DiffChunkInfo getFirst() {
+    return !chunks.isEmpty() ? chunks.get(0) : null;
+  }
+
+  private static void focus(NativeEvent event) {
+    Element e = Element.as(event.getEventTarget());
+    for (e = DOM.getParent(e); e !=